Skip to main content

Handoffs or sub-agents?

Quick start

Give each target a name and a description (the model reads the description to decide when to hand off), then pass them to the triage agent as handoffs:
Each target is offered to the model as one tool, transfer_to_<name> (transfer_to_billing, transfer_to_tech-support). When the model calls it, the run goes on as the target in the same send() / stream() call. result.agentName names the agent that produced the final reply. A target must come from createAgent() and have a name (unique among the handoffs) and a description (or a description on handoff()); otherwise createAgent() throws LOUSHO_CONFIG_INVALID. A handoff tool may not share a name with one of the agent’s tools.

handoff() options

Wrap a target in handoff() to configure it:

Input filters

The target sees the conversation as inputFilter returns it, under its own system prompt (the system prompt of the agent that handed off is replaced, never kept). The filter gets { messages, from, to, args }; messages is the transcript so far without the system prompt, ending with the handoff call and its result. Two filters are built in:
  • handoffFilters.removeToolCalls: keeps user and assistant text, drops tool calls and tool results.
  • handoffFilters.lastUserMessage: keeps only the last user message.
What the filter returns becomes the run’s transcript: result.messages and a session’s history continue from it. A filter must keep at least one message. Approvals remembered with once() in the earlier part of the transcript are dropped at a handoff, so a target’s tools ask again.

What switches and what stays

The agent the run started with is the lead. A target gets none of the lead’s tools, sub-agents or memory slots; its own memory slots are not used either (as for a sub-agent). The lead’s output instruction is added to every target’s system prompt.

Sessions

In a session the active agent is the one the transcript last handed off to (the to of the last metadata.handoff marker), so the next session.send() / session.stream() runs that agent:
agent.send() without a session always starts at the lead. A target hands back with its own handoffs. Since the lead is created after its targets, give the target an array and add the lead to it afterwards (the array is read at every run):
Names identify agents: every agent reachable through handoffs must have its own name. If the agent a transcript names is no longer reachable, the lead runs.

Approvals and resuming after a handoff

A target’s tool that needs approval pauses the run in the lead’s approval store; agent.approvals.resolve() on the lead continues it as the target. A checkpointed run (send(message, { sessionId }) or a session with a store) that crashed after a handoff resumes as the target with agent.resume(id) or session.resume(). The drift check on resume (onAgentDrift, see Durable execution) compares with the target’s saved fingerprint. When a step has a handoff call next to a call that pauses for approval, the handoff waits: the run pauses as the agent that handed off, and the handoff happens once the approval is decided and the step’s other calls ran.

Events

A handoff is reported between the handoff call’s tool.start / tool.done and the target’s first step.start:
handoff carries from, to and toolCallId. A run has one run.start and one run.done however many handoffs it makes. See the event schema.

Limits

  • Only one handoff per model turn: when a turn calls several handoff tools, the first is honored and the others get a tool error.
  • maxHandoffs (a createAgent() option, default 5) caps the handoffs of one run, to stop agents handing a conversation back and forth. A handoff call over it gets a tool error and the agent answers itself. A run resumed after an approval or a crash counts again from zero.
  • Remote agents (remoteAgent()) cannot be handoff targets; use them as sub-agents.
  • A target does not hand back on its own at the end of its turn; give it a handoff to the lead (see Sessions).

The executor API

AgentExecutor.execute() takes handoffs (ResolvedHandoff[]: name, toolName, description, input, spec(input) resolving the target’s run configuration and its own handoffs, inputFilter, onHandoff) and maxHandoffs. createAgent() builds them; pass them yourself only when you drive the executor directly.