Handoff and safety
An AI agent that talks to your customers will meet questions it must not answer: a damage claim, a legal threat, a refund amount. K-Agent treats these as product guarantees enforced in code, so they hold no matter how a model behaves on a given day.
The never-handle list
Section titled “The never-handle list”guardrails.never_handle lists situations the agent must never handle itself. It is on by default and comes with localized defaults:
- damage, injury or accident claims;
- legal threats, or mentions of lawyers, police or authorities;
- refund or compensation amounts;
- complaints that name an employee;
- requests for another customer’s data.
The list is placed last in the prompt, after your instructions and knowledge, so nothing written earlier can talk the agent out of it. It comes with composure rules: take the customer seriously, stay calm, don’t apologize on the company’s behalf, don’t speculate about fault, don’t promise anything — and never make light of it.
What the agent does next depends on the tools that are actually available. With transfer_to_human it hands the conversation to your team. Without it, it tells the customer plainly that this can’t be handled here and that they should contact you directly — it never promises a follow-up that nobody will make.
Edit the list freely; if you empty it on purpose it stays empty. Per-request overrides can never change it.
Verbatim escalation reply
Section titled “Verbatim escalation reply”guardrails.escalation_reply is the exact reply a customer receives when the agent hands over a liability case (reason_type: "liability"). Write it once in Arabic and English, have it approved, and it is used word for word:
{ "guardrails": { "escalation_reply": { "ar": "شكرًا لتواصلك. حوّلنا طلبك إلى المسؤول المختص وسيتواصل معك قريبًا.", "en": "Thank you for telling us. We've passed this to the responsible manager, who will contact you shortly." } }}When transfer_to_human succeeds with reason_type: "liability" and a reply is set:
- tool calls still pending in that round are not executed;
- no further model call is made;
- the run ends
handed_offwith one final assistant message whose text is your reply, in the customer’s language, withmetadata.replaced: true; - the model’s own wording is kept only in the run steps, for review.
Text sent in earlier rounds stays as its own message. With conversation.stream_mode: "auto" (the default), replies are buffered one round at a time whenever an escalation reply is configured, so streamed text can never contradict the substitution. In live mode, message.completed carries replaced: true and is the text to show.
The same applies to one-shot ask calls: the run returns the reply and a handoff, and the handoff.requested webhook fires with the run_id.
Only say what you were told
Section titled “Only say what you were told”Every agent follows a grounding rule (shown in the prompt X-ray as grounding@1): prices, opening times, addresses, what a service includes — if it isn’t in the instructions or knowledge, the agent doesn’t know it. Instead of guessing, or promising to “check and get back to you” (which nobody would do), it hands the question to a person. When no handoff tool is available, it says plainly that the customer should contact you directly.
Handoffs
Section titled “Handoffs”A handoff (ho_…) transfers a conversation to your team. It can be requested by:
requested_by |
How |
|---|---|
agent |
The model calls transfer_to_human. |
api |
Your code calls POST /v1/sessions/{session}/handoff with {reason_type, summary}, or the end user asks through the widget. |
policy |
K-Agent itself — for example when a provider keeps failing, or a Free plan’s quota is used up. |
human |
Someone on your team takes over a live conversation (POST /v1/sessions/{session}/takeover), or simply replies in it. |
Every handoff has a reason_type — customer_requested, out_of_scope, complaint, liability or technical — and a short summary that records the topic, not an accusation. Liability handoffs get priority: "high" and are sorted first in the Handoff Desk.
Truthful tool results
Section titled “Truthful tool results”transfer_to_human reports exactly what happened, and the agent may only tell the customer what the result supports. It never says “transferred” unless a handoff was recorded.
status |
Meaning |
|---|---|
queued |
A handoff was created. Your team will pick it up; the AI stays quiet in this session. |
recorded |
One-shot call: the handoff was recorded for your team (there is no session to pause). |
recorded_closed |
Recorded outside business hours. The result includes next_open_local, e.g. Sunday 09:00 (Asia/Riyadh), and the agent tells the customer when a person will reply — never “shortly”. |
already |
A handoff is already open for this conversation. |
failed |
It could not be recorded. The agent must not claim a transfer. |
For anonymous widget visitors, the agent also says they will see the reply in the chat if they keep the page open or come back on the same device.
Human mode
Section titled “Human mode”While an agent-, API- or human-requested handoff is open, the session is in human mode (session.mode: "human"):
- new messages are stored and shown to your team, but no AI runs;
POST …/messagesreturns200withrun: null;- your team replies with
role: "human_agent", and the reply reaches the widget and your session stream at once.
Technical handoffs don’t mute. A handoff caused by a provider failure, an interrupted run or an exhausted quota is recorded (row, handoff.requested webhook, outcome handed_off, notice to the customer) but the session stays in agent mode, so the next message is answered normally. A session has at most one open technical handoff.
Expiry
Section titled “Expiry”A handoff that nobody resolves must not mute a conversation forever:
handoff.ttl_hours(1–720, default 24) sets the window;handoff.never_expire: trueturns expiry off on purpose.- The window slides: claiming the handoff, or any message from your team (public reply or internal note), pushes
expires_atto at least now + TTL. - On expiry, the handoff becomes
expired, the session returns toagentmode, and yourhandoff.expire_messageis posted as a public system notice. No AI reply starts on its own; the next customer message is answered by the agent. - With
handoff.on_expire: "close"the session is closed instead; a new message from the customer reopens it.
Resolving
Section titled “Resolving”POST /v1/handoffs/{handoff}/resolve with {"action": "release" | "close", "note": "…"} ends a handoff. On release, the agent takes over again: your team’s public replies become part of its history, and the optional note is given to it once as an internal note it must not quote to the customer.
Business hours
Section titled “Business hours”With business_hours.enabled, the agent knows whether your team is online, in the agent’s time zone (locale.timezone):
handoff.outside_hours: "record"(default) still records handoffs out of hours, with the honestrecorded_closedwording."withhold"removestransfer_to_humanwhile you are closed.business_hours.outside_hours_ai: "answer"(default) keeps the agent answering out of hours;"message_only"sends yourout_of_hours_messageword for word, with no AI call.- Spans can run past midnight (
"open": "20:00", "close": "02:00"), days you don’t list are closed, and the default schedule is Sunday to Thursday, 09:00–17:00.
Never silent
Section titled “Never silent”Every run ends with exactly one terminal outcome. When the provider fails, K-Agent retries with backoff, then tries your fallback_models, then sends your fallback.message and hands the session to your team (fallback.handoff_on_failure, on by default). If a Free plan runs out of AI conversations, sessions get a short notice and a technical handoff — the customer never sees a billing error.
Events
Section titled “Events”| Event | When |
|---|---|
handoff.requested |
A handoff was recorded (any source). |
handoff.assigned |
Someone on your team claimed it. |
handoff.unclaimed |
Still unclaimed after handoff.notify.unclaimed_reminder_minutes (default 10). |
handoff.resolved |
Released to the agent or closed. |
handoff.expired |
The TTL ran out. |
session.updated |
The session’s mode or status changed. |
message.created |
A new message, including your team’s public replies (with role and author). |
They arrive on webhooks and on the session’s event stream. Handoffs from the dashboard’s test panel are sandboxed: they never reach the Desk, webhooks or alerts.
Learn more
Section titled “Learn more”- Work the queue in the Handoff Desk guide.
- See every related setting in the settings reference.