Definition
The active agent transfers conversation control to another agent, who then owns the rest of the interaction.
Category: Control structure
When to use
Support / after-sales triage, domain expert switching, multi-turn workflows where a specialist needs to question the user directly.
When not to use
When the subtask is just a one-off query, or when handoff criteria are vague and the conversation keeps bouncing.
How to implement
- Each agent declares which
handoff_intentsit accepts. - A handoff is not a function call — it changes which agent is active.
- Pass a trimmed context across: user goal, completed items, open items, permission boundary.
- Set a maximum handoff count and detect loops.
- Log each handoff with
from / to / reason / context_digest.
Minimal pseudocode
TypeScript
async function maybeHandoff(state: SessionState) {
const decision = await router.classify(state.lastMessage, state.activeAgent);
if (decision.type === "handoff") {
assert(!state.handoffHistory.includesLoop(decision.to));
return {
...state,
activeAgent: decision.to,
handoffHistory: [...state.handoffHistory, decision]
};
}
return state;
}
Recommended trace events
handoff.requestedhandoff.acceptedhandoff.rejectedhandoff.loop_detected
Common failure modes
- Handoff reasons aren't recorded → debugging is impossible.
- Full context is transferred, polluting the receiving agent.
- Loops between agents.
- The user has no idea which agent is in front of them.
Implementation checklist
- Input/output schemas defined.
- Each agent's permission boundary defined.
- Every agent call carries a run id / trace id.
- Failure, timeout, cancel, and retry strategies defined.
- Context passed is the minimum required, not the full history.
- High-risk actions are gated by approval or a verifier.