ESC

Type to search the knowledge base.

Agent Handoffs

A handoff passes the conversation to another agent with a slimmer prompt and different tools. What to copy, what to drop, and how to avoid ping-pong.

advanced5 min read
  • agents
  • handoff
  • orchestration

Support hands you to billing. Billing hands you back to support. Support apologizes. That is not orchestration. That is a missing stop rule. An agent handoff is a tool that swaps the active prompt and the tool list. The receiving agent becomes the speaker. If you do not record the path, the model will bounce until the step cap or the bill does.

A second persona is not a substitute for a better tool. Use a handoff when the user-facing specialist should own the rest of the turn. If you only need a lookup, call a specialist as a tool and keep the same speaker. That split is the whole design.

Key takeaways

  • A handoff is a tool call: new instructions, new tools, new speaker.
  • Copy a summary plus artifact ids, not the full dump, unless the specialist must quote.
  • Record from → to on the run. A second visit to the same agent is a cycle unless you allow it.
  • The specialist must finish, ask the user, or escalate to a human. Bouncing back is not the default.
  • Need a subtask, not a new speaker? Do not hand off. Delegate.

An agent handoff is a tool

In the OpenAI Agents SDK, a handoff is exposed to the model as something like transfer_to_refund_agent. Your own loop can do the same: the model emits a structured transfer, your runtime loads another system prompt and allowlist, and the next completion uses that agent.

type AgentId = 'triage' | 'billing' | 'refunds';

type HandoffArgs = {
  target: AgentId;
  reason: string;
  artifacts: { orderId?: string; ticketId?: string };
};

type Run = {
  agent: AgentId;
  path: AgentId[];
  messages: Array<{ role: 'system' | 'user' | 'assistant' | 'tool'; content: string }>;
};

const MAX_VISITS = 1;

export function applyHandoff(run: Run, args: HandoffArgs): Run {
  const visits = run.path.filter((id) => id === args.target).length;
  if (visits >= MAX_VISITS) {
    throw new Error(`handoff cycle: ${[...run.path, args.target].join(' -> ')}`);
  }
  if (args.target === run.agent) {
    throw new Error('handoff to self');
  }

  const brief = [
    `Handoff from ${run.agent}. Reason: ${args.reason}.`,
    `Artifacts: ${JSON.stringify(args.artifacts)}`,
    'Do not transfer back unless the user asks for a different department.',
    'If you cannot finish, escalate to a human. Do not apologize-loop.',
  ].join('\n');

  return {
    agent: args.target,
    path: [...run.path, args.target],
    messages: [
      { role: 'system', content: specialistPrompt(args.target) },
      { role: 'user', content: brief },
    ],
  };
}

Notice what did not copy: the triage novel, the old tool schemas, the ten “let me check” assistant turns. Those tokens teach the specialist to behave like triage.

Copy a brief, not a dump

The receiving window is a budget. Context engineering decides what survives.

Take Drop
User intent in one paragraph Full chat dump by default
Ids: order, ticket, customer Raw tool JSON from unrelated lookups
Policy quotes the specialist must follow The previous agent’s chain-of-thought
Files the user already attached The previous tool list

If billing needs the exact refund clause, pass that quote as an artifact. Do not pass 40 turns of “hi / thanks / still there?” so the model can “have context.” That is how you pay for noise and still miss the SKU.

When you do need history (the user already answered “which card”), use an input filter: keep the last user messages plus the artifact bag. SDKs that hand off the entire transcript are a default, not a requirement. Override them.

Stop ping-pong

Cycles come from symmetric tools. If billing has transfer_to_support and support has transfer_to_billing, the model will use both when it is unsure. Fix the graph, not the adjectives.

  1. Acyclic by default. Refunds may receive from triage. Refunds may escalate to a human. Refunds may not transfer to triage.
  2. Visit cap. One visit per agent per run unless you store a reason to re-enter (“user changed the order id”).
  3. Terminal specialists. The last agent in the path has no peer handoff, or only escalate_to_human.
  4. Same question twice. If the user intent embedding (or a cheap classifier) matches the turn that caused the previous transfer, refuse the second hop and ask a clarifying question instead.

Ping-pong looks like progress in a trace viewer. Count hops. If hop 3 is a repeat of hop 1, fail the run.

When a handoff is the wrong primitive

A handoff takes over the microphone. Subagents report back. Multi-agent systems mix both; most products need neither.

Use a handoff when:

  • The user should talk to a different policy and a different tool set (billing vs shipping).
  • You want a slimmer prompt so the specialist stops seeing refund tools.
  • The trace should show a named owner for the rest of the conversation.

Keep one agent when:

  • You are about to invent five Slack personas.
  • The “specialist” is one SQL query.
  • You cannot write a stop rule.

The conversation UI still needs a single thread. Tell the user who is speaking if policy requires it (“Billing team now”). Do not role-play a roomful of names. That burns tokens and confuses conversation state.

Interview answer (out loud)

“An agent handoff is a tool that replaces the active prompt and tools so a specialist becomes the speaker. I transfer a summary plus artifact ids, not the full transcript. I record the path and forbid cycles: specialists finish, ask, or escalate to a human. If I only need a subtask, I call a specialist as a tool and keep one speaker. Five personas with no stop rule is ping-pong.”

Further reading (primary sources)

Related guides