Handoff and Escalation
Also known as: agent transfer, swarm handoff, human escalation
Let an agent transfer the whole conversation to a better-suited agent or a human, carrying forward what it already learned.
The problem
Two dead ends recur in deployed agents. One: the agent is the wrong specialist and either forces an answer outside its competence or tells the user to start over somewhere else. Two: the agent is genuinely stuck — the user is frustrated, the tools cannot do what is being asked — and it keeps trying because it has no way to stop. Both end with a user repeating everything they already said to someone new.
The pattern
Model transfer as an ordinary tool call. Any agent can hand the conversation to a named peer or to a human queue, passing a structured summary of what has been established, what was tried, and what remains open. The receiving party starts with that summary rather than the raw transcript. Define explicit escalation triggers so handoff happens on a rule, not only on the model's judgment.
Use when
- You run several specialists and requests legitimately cross their boundaries
- A human fallback exists and some cases must reach it — refunds past a limit, complaints, safety
- Conversations can start in one domain and move to another mid-thread
- You need an auditable record of why a case left automated handling
Avoid when
- A single agent covers the whole domain — transfer adds a failure mode for nothing
- There is no human queue and no better peer, making escalation a dead end that looks like progress
- Handoffs would be so frequent that the user experiences constant restarts
Tradeoffs
GainUsers are not asked to repeat themselves, and hard cases reach someone who can help
CostSummarization loses detail; the receiver sometimes needs the original transcript anyway
GainRule-based triggers make escalation predictable and auditable
CostTriggers tuned loosely escalate everything; tuned tightly they never fire
GainTransfer logs are the clearest signal of where your agents are weak
CostHandoff loops — A transfers to B, B transfers back to A — must be detected and cut
Handoff differs from routing in what moves. Routing decides where a request starts; handoff moves an in-progress conversation, along with everything already established in it.
Transfer as a tool, with a structured payload
The payload is what makes a handoff feel seamless rather than like a restart. Require the transferring agent to state the request in its own words, the facts it verified, the actions it already took, and the specific reason it is transferring. That last field is the one that makes the log useful later.
class Handoff(BaseModel):
to: Literal["billing", "technical", "account", "human"]
reason: str # why this agent cannot continue
user_wants: str # the request, restated
established: list[str] # verified facts — ids, dates, amounts
already_tried: list[str] # so the receiver does not repeat it
open_questions: list[str]
TRANSFER_TOOL = {
"name": "transfer",
"description": (
"Hand this conversation to another specialist or to a human. "
"Use when the request is outside your domain, when you have tried "
"twice without progress, or when policy requires human review. "
"Summarize accurately — the receiver sees only your summary."
),
"parameters": Handoff.model_json_schema(),
}Rules, not just judgment
Leaving escalation entirely to the model produces both failure directions: agents that never escalate because they always believe one more attempt will work, and agents that escalate the moment anything is mildly hard. Encode the non-negotiable triggers in code — refund thresholds, repeated tool failure, explicit user request, detected distress — and let the model's judgment handle the rest.
def must_escalate(conv: Conversation, pending: ToolCall | None) -> str | None:
if conv.user_asked_for_human:
return "user_requested"
if pending and pending.name == "issue_refund" and pending.args["amount"] > 500_00:
return "refund_over_threshold"
if conv.consecutive_tool_failures >= 3:
return "repeated_tool_failure"
if conv.turns >= 12 and not conv.progress_markers:
return "no_progress"
if conv.sentiment_score < -0.7:
return "user_distress"
return NoneDetect handoff loops. Track the chain of agents in the conversation and cut over to a human when any agent appears twice — otherwise billing and technical will bounce a genuinely cross-cutting case between them until the user gives up.
Receiving well
The receiving agent opens with an acknowledgement built from the payload, not a fresh greeting. "I can see you've been trying to update the card on invoice 4471 and the address validation failed twice — let me take it from here" is the difference between a handoff and a restart. Keep the full transcript available on demand, but do not paste it into the receiver's context by default; that would undo the point of summarizing.
- Transfer is a tool with a structured payload, not a special control-flow escape
- Encode hard escalation triggers in code; leave the judgment calls to the model
- Detect and break handoff loops by tracking the agent chain
- The receiver opens by acknowledging what was already established
- Keep the transcript retrievable on demand, but hand over the summary by default
Review transferred conversations weekly. Every transfer is a labelled example of something an agent could not do — it is the highest-signal backlog of tool gaps and prompt fixes you will get, and it comes for free.