A handoff transfers control from one agent to another as a callable tool — for example, routing from triage to billing specialists; the Handoff dataclass declares targets via the routing agent's handoffs= list and manages context, filtering, and dynamic enabling. Handoff mechanics include JSON schema exposure to the model, optional input filtering before delegation, conditional history nesting, and weak references to prevent circular dependencies.
A handoff is a directed transfer of control from one agent to another, exposing it as a tool the LLM can call — for example, a triage agent routing requests to billing or account-management specialists. The two multi-agent orchestration patterns are handoffs (specialist takes over the conversation) and agents-as-tools (orchestrator stays in control and calls specialists as tools).[1][2] An agent's outgoing handoff targets are declared via the handoffs= list on the routing agent; the Runner automatically executes those handoffs and subsequent tool calls.[2] handoff_description on a specialist Agent gives the routing agent context about when to delegate to that specialist.[2]
The Handoff dataclass in src/agents/handoffs/__init__.py stores a weak reference (_agent_ref) to the target agent when constructed via handoff(), preventing circular strong references.[1] Handoff.is_enabled accepts either a bool or a callable (RunContextWrapper, AgentBase) -> bool | Awaitable[bool], allowing dynamic enable/disable decisions based on runtime context or state.[1] Handoff.strict_json_schema defaults to True; the SDK strongly recommends keeping it True because it increases the likelihood of correct JSON input from the model.[1] Handoff.input_json_schema is the JSON schema exposed to the model as the handoff tool's parameters; it only describes the structured payload passed to on_invoke_handoff and does not replace the next agent's main input.[1]
Handoff.default_tool_name() derives the tool name from the agent name using transform_string_function_style(f"transfer_to_{agent.name}"), so an agent named "Billing Support" becomes transfer_to_billing_support.[1] Handoff.default_tool_description() combines the agent name with its handoff_description field to form the tool description shown to the model.[1]
HandoffInputFilter is a type alias for Callable[[HandoffInputData], MaybeAwaitable[HandoffInputData]] — the filter can be sync or async.[1] Handoff.input_filter (type HandoffInputFilter) receives the full conversation history including the trigger item and handoff tool output; the next agent receives input_items when set, otherwise new_items, enabling filtering without losing session history.[1] In streaming mode, Handoff.input_filter changes are not streamed — items generated before the handoff will already have been streamed. Server-managed conversations (conversation_id, previous_response_id, or auto_previous_response_id) do not support handoff input filters.[1] HandoffHistoryMapper is a type alias for Callable[[list[TResponseInputItem]], list[TResponseInputItem]], used to map a previous transcript to the nested summary payload.[1] Handoff.nest_handoff_history overrides the run-level nest_handoff_history setting for a single handoff; server-managed conversations automatically disable nested handoff history with a warning.[1]
The handoff() helper in src/agents/handoffs/__init__.py always returns the specific agent captured at call time; on_handoff is for side effects or bookkeeping, not dynamic destination selection.[1] _invoke_handoff_with_redaction in src/agents/handoffs/__init__.py intercepts ModelBehaviorError exceptions that carry redacted data, nulls out the context and input, and re-raises via _raise_data_redacted_error to prevent sensitive data from leaking in tracebacks.[1] HandoffInputData.clone() creates a copy with specified fields overridden and preserves internal _nested_history_owned_items from the original, ensuring history ownership is not lost on copy.[1]
Sources