InkdownInkdown
Start writing

Study

59 filesยท8 subfolders

Shared Workspace

Study
core

06_HANDOFFS

Shared from "Study" on Inkdown

Handoffs - Comprehensive Deep Dive

Overview

Handoffs are the mechanism that enables multi-agent workflows in the OpenAI Agents SDK. A handoff occurs when one agent delegates a task to another agent. Think of handoffs as "transfer of control" or "referral" - just like a doctor might refer you to a specialist, agents can hand off tasks to other specialized agents.

Core Concepts

What is a Handoff?

A handoff is when an agent decides that another agent is better suited to handle the current task and transfers control to that agent. The handoff includes:

  • The target agent (who to hand off to)
  • The context/information to pass along
  • Optional filtering of what information to pass
  • Optional custom logic for the handoff
Why Handoffs Matter
  1. Specialization - Different agents can specialize in different domains
  2. Modularity - Build complex workflows from simple, focused agents
programming-language-concepts.md
zero-language-explanation.md
DB
01-introduction.md
02-relational-databases.md
03-database-design.md
04-indexing.md
05-transactions-acid.md
06-nosql-databases.md
07-query-optimization.md
08-replication-ha.md
09-sharding-partitioning.md
10-caching-strategies.md
11-cap-theorem.md
12-connection-pooling.md
13-backup-recovery.md
14-monitoring.md
15-database-selection.md
README.md
JS
Event loop
Merlin Backend
01-Orchestration.md
02-DeepResearch.md
03-Search.md
04-Scraping.md
05-Streaming.md
06-MultiProviderLLM.md
07-MemoryAndContext.md
08-ErrorHandling.md
09-RateLimiting.md
10-TaskQueue.md
11-SecurityAndAuth.md
Orchestration-2nd-draft
OpenAI Agents Python
00_OVERVIEW.md
01_AGENT_SYSTEM.md
02_RUNNER_SYSTEM.md
03_TOOL_SYSTEM.md
04_ITEMS_SYSTEM.md
05_GUARDRAILS.md
06_HANDOFFS.md
07_MEMORY_SESSIONS.md
08_MODEL_PROVIDERS.md
09_SANDBOX_SYSTEM.md
10_TRACING.md
11_RUN_STATE.md
12_CONTEXT.md
13_LIFECYCLE_HOOKS.md
14_CONFIGURATION.md
15_ERROR_HANDLING.md
16_STREAMING.md
17_EXTENSIONS.md
18_MCP_INTEGRATION.md
19_BEST_PRACTICES.md
20_ARCHITECTURE_PATTERNS.md
opencode-study
context-handling
core
Python
Alembic
Basics
sqlalchemy - fastapi
SQLAlchemy overview
tweets
system_design_for_agentic_apps.md
  • Efficiency - Route tasks to the most capable agent
  • Scalability - Add new capabilities by adding new agents
  • Clarity - Clear separation of concerns
  • Handoff vs Agent as Tool

    Handoffs and "agents as tools" both involve multiple agents, but they work differently:

    Handoffs:

    • The new agent receives the full conversation history
    • The new agent takes over the conversation
    • The original agent is no longer involved
    • Good for: Domain specialization, expertise routing

    Agent as Tool:

    • The new agent receives generated input (not full history)
    • The new agent runs as a tool and returns
    • The original agent continues the conversation
    • Good for: Subtasks, specific queries, calculations

    Handoff Class

    Handoff Structure
    Python
    @dataclass
    class Handoff(Generic[TContext, TAgent]):
        tool_name: str
        """The name of the tool that represents the handoff."""
        
        tool_description: str
        """Description of the handoff tool for the LLM."""
        
        input_json_schema: dict[str, Any]
        """JSON schema for handoff arguments."""
        
        on_invoke_handoff: Callable[[RunContextWrapper, str], Awaitable[TAgent]]
        """Function that invokes the handoff."""
        
        agent_name: str
        """Name of the target agent."""
        
        input_filter: HandoffInputFilter | None
        """Optional filter for input passed to next agent."""
        
        nest_handoff_history: bool | None
        """Override for nested handoff history behavior."""
        
        strict_json_schema: bool
        """Whether input schema is in strict mode."""
        
        is_enabled: bool | Callable
        """Whether the handoff is enabled (can be dynamic)."""
    Creating Handoffs

    The simplest way is using the handoff() helper:

    Python
    from agents import Agent, handoff
    
    specialist = Agent(
        name="specialist",
        instructions="Handle specialized technical issues",
        handoff_description="Technical support specialist for debugging",
    )
    
    generalist = Agent(
        name="generalist",
        instructions="Handle general queries and delegate when needed",
        handoffs=[handoff(specialist)],
    )

    What happens:

    1. The handoff() function creates a Handoff object
    2. It generates a tool name (e.g., transfer_to_specialist)
    3. It generates a tool description
    4. It creates an empty JSON schema (no arguments by default)
    5. It sets up the invocation function to return the specialist agent

    Handoff Configuration

    Custom Tool Name

    Override the default tool name:

    Python
    handoff(
        specialist,
        tool_name_override="get_technical_help",
    )
    Custom Tool Description

    Override the default description:

    Python
    handoff(
        specialist,
        tool_description_override="Get help with technical problems and debugging",
    )
    Handoff with Input

    Pass structured input to the handoff:

    Python
    from dataclasses import dataclass
    
    @dataclass
    class HandoffInput:
        topic: str
        urgency: str
    
    def on_handoff(context: RunContextWrapper, input: HandoffInput) -> None:
        """Called when handoff is invoked."""
        print(f"Handing off for topic: {input.topic}, urgency: {input.urgency}")
    
    handoff(
        specialist,
        on_handoff=on_handoff,
        input_type=HandoffInput,
    )

    How it works:

    1. The LLM generates JSON arguments matching HandoffInput
    2. The arguments are validated against the schema
    3. The on_handoff function is called with the validated input
    4. The function can perform side effects (logging, tracking, etc.)
    5. The specialist agent is still returned
    Handoff Without Input

    Handoff without structured input (simpler):

    Python
    def on_handoff(context: RunContextWrapper) -> None:
        """Called when handoff is invoked."""
        print("Handing off to specialist")
    
    handoff(
        specialist,
        on_handoff=on_handoff,
    )
    Input Filter

    Filter what information is passed to the next agent:

    Python
    from agents import HandoffInputData
    
    def filter_handoff_input(data: HandoffInputData) -> HandoffInputData:
        """Filter input passed to next agent."""
        # Only pass the last 5 items
        return data.clone(new_items=data.new_items[-5:])
    
    handoff(
        specialist,
        input_filter=filter_handoff_input,
    )

    HandoffInputData structure:

    Python
    @dataclass
    class HandoffInputData:
        input_history: str | tuple[TResponseInputItem, ...]
        """The input history before the handoff."""
        
        pre_handoff_items: tuple[RunItem, ...]
        """Items generated before the handoff turn."""
        
        new_items: tuple[RunItem, ...]
        """Items generated during the current turn (including handoff)."""
        
        run_context: RunContextWrapper | None
        """Run context at handoff time."""
        
        input_items: tuple[RunItem, ...] | None
        """Items to include in next agent's input (optional override)."""
    Nested Handoff History

    Control how conversation history is passed:

    Python
    handoff(
        specialist,
        nest_handoff_history=True,  # Collapse history into summary
    )

    Options:

    • None (default) - Use run-level configuration
    • True - Collapse history into a single summary message
    • False - Pass full conversation history

    When to use:

    • True - For deep handoff chains to reduce token usage
    • False - When the next agent needs full context
    Dynamic Enablement

    Handoffs can be dynamically enabled or disabled:

    Python
    def is_handoff_enabled(
        context: RunContextWrapper,
        agent: Agent,
    ) -> bool:
        """Check if handoff should be enabled."""
        return context.context.user_tier == "premium"
    
    handoff(
        specialist,
        is_enabled=is_handoff_enabled,
    )

    Use cases:

    • Feature flags
    • User tier-based access
    • Context-dependent availability
    • A/B testing

    Handoff Execution Flow

    Complete Handoff Flow
    Python
    # 1. Model decides to hand off
    tool_call = ResponseFunctionToolCall(
        id="call_123",
        name="transfer_to_specialist",
        arguments='{}',
    )
    
    # 2. Find the handoff
    handoff = find_handoff(tool_call.name)
    
    # 3. Check if enabled
    if not is_handoff_enabled(handoff, context):
        skip_handoff(tool_call)
        return
    
    # 4. Parse arguments (if input_type is set)
    if handoff.input_type is not None:
        validated_input = validate_arguments(tool_call.arguments, handoff.input_type)
        await handoff.on_invoke_handoff(context, validated_input)
    else:
        await handoff.on_invoke_handoff(context, None)
    
    # 5. Get the target agent
    target_agent = await handoff.on_invoke_handoff(context, arguments)
    
    # 6. Build handoff input data
    handoff_input = build_handoff_input_data(
        current_state,
        handoff,
        tool_call,
    )
    
    # 7. Apply input filter (if configured)
    if handoff.input_filter:
        filtered_input = await handoff.input_filter(handoff_input)
    else:
        filtered_input = apply_default_filter(handoff_input)
    
    # 8. Handle nested history
    if should_nest_history(handoff, run_config):
        filtered_input = nest_handoff_history(filtered_input)
    
    # 9. Switch to target agent
    current_agent = target_agent
    
    # 10. Prepare input for next turn
    next_turn_input = prepare_handoff_input(filtered_input)
    
    # 11. Continue with next turn
    continue_execution(next_turn_input)

    Handoff History Management

    Default Behavior (Full History)

    By default, the full conversation history is passed to the next agent:

    Python
    # Agent A: "Hello"
    # Agent A: "How can I help?"
    # User: "I have a technical problem"
    # Agent A: [hands off to Agent B]
    # Agent B sees: ["Hello", "How can I help?", "I have a technical problem"]

    Pros:

    • Full context preserved
    • No information loss
    • Agent can reference earlier conversation

    Cons:

    • Higher token usage
    • Potential for confusion in deep chains
    • Slower for long conversations
    Nested History (Collapsed)

    When nest_handoff_history=True, history is collapsed:

    Python
    # Agent A: "Hello"
    # Agent A: "How can I help?"
    # User: "I have a technical problem"
    # Agent A: [hands off to Agent B]
    # Agent B sees: [{"type": "assistant", "content": "Summary: User has a technical problem"}]

    Pros:

    • Lower token usage
    • Cleaner context for deep chains
    • Faster for long conversations

    Cons:

    • Some detail lost in summary
    • Agent can't reference specific earlier messages
    • Depends on quality of summary
    Custom History Mapping

    Provide a custom function to transform history:

    Python
    def custom_history_mapper(
        history: list[TResponseInputItem],
    ) -> list[TResponseInputItem]:
        """Custom history transformation."""
        # Keep only user and assistant messages
        filtered = [
            item for item in history
            if item.get("role") in ["user", "assistant"]
        ]
        
        # Add a summary at the beginning
        filtered.insert(0, {
            "type": "assistant",
            "content": "Context: Technical support conversation",
        })
        
        return filtered
    
    config = RunConfig(
        handoff_history_mapper=custom_history_mapper,
    )
    Input Filter for Fine-Grained Control

    Use input filters for precise control:

    Python
    def precise_filter(data: HandoffInputData) -> HandoffInputData:
        """Precise control over what's passed."""
        # Keep only the last user message and the handoff
        relevant_items = [
            item for item in data.new_items
            if isinstance(item, (MessageOutputItem, HandoffCallItem))
        ]
        
        return data.clone(new_items=tuple(relevant_items))

    Handoff Patterns

    1. Triage Pattern

    One agent routes to specialists:

    Python
    triage_agent = Agent(
        name="triage",
        instructions="Determine the type of issue and route to appropriate specialist",
        handoffs=[
            handoff(billing_agent),
            handoff(technical_agent),
            handoff(sales_agent),
        ],
    )

    When to use:

    • Customer support systems
    • Help desk workflows
    • Multi-domain support
    2. Supervisor Pattern

    One agent supervises and delegates:

    Python
    supervisor = Agent(
        name="supervisor",
        instructions="Review work and delegate to appropriate specialists",
        handoffs=[
            handoff(coder_agent),
            handoff(researcher_agent),
            handoff(writer_agent),
        ],
    )

    When to use:

    • Complex task breakdown
    • Project management
    • Quality assurance
    3. Escalation Pattern

    Escalate from general to specialist:

    Python
    level1_agent = Agent(
        name="level1",
        instructions="Handle basic issues",
        handoffs=[handoff(level2_agent)],
    )
    
    level2_agent = Agent(
        name="level2",
        instructions="Handle intermediate issues",
        handoffs=[handoff(level3_agent)],
    )
    
    level3_agent = Agent(
        name="level3",
        instructions="Handle complex issues",
    )

    When to use:

    • Tiered support systems
    • Escalation workflows
    • Progressive problem solving
    4. Collaboration Pattern

    Agents collaborate on different aspects:

    Python
    researcher = Agent(
        name="researcher",
        instructions="Research the topic",
        handoffs=[handoff(writer)],
    )
    
    writer = Agent(
        name="writer",
        instructions="Write the content based on research",
        handoffs=[handoff(reviewer)],
    )
    
    reviewer = Agent(
        name="reviewer",
        instructions="Review and approve the content",
    )

    When to use:

    • Content creation workflows
    • Multi-stage processes
    • Quality assurance pipelines
    5. Context Switching Pattern

    Switch context based on user request:

    Python
    general_agent = Agent(
        name="general",
        instructions="Handle general queries",
        handoffs=[
            handoff(coding_agent),
            handoff(writing_agent),
            handoff(math_agent),
        ],
    )

    When to use:

    • General-purpose assistants
    • Multi-domain bots
    • Context-aware routing

    Handoff and Sessions

    Session Continuity

    Handoffs maintain session continuity:

    Python
    session = SQLiteSession(db_path="conversations.db")
    
    result = await Runner.run(
        triage_agent,
        input,
        session=session,
    )
    
    # The session includes all agents' conversation history
    # Handoffs don't break session continuity
    Session History with Handoffs

    The session tracks which agent generated each item:

    Python
    items = await session.load_items(conversation_id)
    
    for item in items:
        print(f"Agent: {item.agent.name}")
        print(f"Content: {item.raw_item}")
    Compaction with Handoffs

    For long conversations with many handoffs:

    Python
    settings = SessionSettings(
        max_items=50,
        compaction_enabled=True,
    )
    
    result = await Runner.run(
        triage_agent,
        input,
        session=session,
        session_settings=settings,
    )

    Handoff Tracing

    Handoff Spans

    Handoffs create trace spans:

    Python
    from agents import handoff_span
    
    async def my_handoff_function(context, input):
        with handoff_span(name="my_handoff"):
            # Handoff logic
            return specialist_agent
    Handoff Events

    Handoffs emit events during streaming:

    Python
    async for event in Runner.run_streamed(agent, input):
        if isinstance(event, AgentUpdatedStreamEvent):
            print(f"Agent changed to: {event.agent.name}")

    Handoff Best Practices

    1. Clear Handoff Descriptions

    Write clear descriptions for the LLM:

    Python
    # Good
    specialist = Agent(
        name="billing_specialist",
        instructions="Handle billing and payment issues",
        handoff_description=(
            "Billing specialist for handling payment issues, "
            "refunds, subscription management, and account balance inquiries"
        ),
    )
    
    # Avoid
    specialist = Agent(
        name="billing",
        instructions="Handle billing",
        handoff_description="Billing",  # Too vague
    )
    2. Appropriate Handoff Granularity

    Design handoffs at the right level:

    Python
    # Good - domain-level handoffs
    handoffs=[
        handoff(billing_agent),
        handoff(technical_agent),
        handoff(sales_agent),
    ]
    
    # Avoid - too granular
    handoffs=[
        handoff(billing_refund_agent),
        handoff(billing_payment_agent),
        handoff(billing_subscription_agent),
        # Too many similar agents
    ]
    3. Use Input Filters for Context

    Use input filters to control context:

    Python
    # Good - filter to relevant context
    def filter_context(data):
        return data.clone(new_items=data.new_items[-5:])
    
    # Avoid - passing everything
    # No filter means full history is passed
    4. Handle Handoff Failures

    Handle cases where handoffs might fail:

    Python
    # Good - provide fallback
    generalist = Agent(
        name="generalist",
        instructions="Handle general queries",
        handoffs=[handoff(specialist)],
    )
    
    # If specialist fails, generalist can still respond
    
    # Avoid - single point of failure
    # If only specialist is available and it fails, user gets no response
    5. Test Handoff Paths

    Test all possible handoff paths:

    Python
    # Test each handoff
    test_triage_to_billing()
    test_triage_to_technical()
    test_triage_to_sales()
    
    # Test handoff chains
    test_billing_to_specialist()
    test_technical_to_level2()

    Common Handoff Issues

    1. Handoff Loops

    Problem: Agents hand off back and forth infinitely.

    Solution: Use handoff history tracking:

    Python
    def prevent_loops(data: HandoffInputData) -> HandoffInputData:
        """Prevent handoff loops."""
        # Check if we're handing back to the same agent
        recent_agents = [item.agent.name for item in data.new_items[-5:]]
        if current_agent.name in recent_agents:
            # Don't hand off
            return data.clone(new_items=data.new_items)
        return data
    2. Lost Context

    Problem: Important context is lost in handoff.

    Solution: Use appropriate history management:

    Python
    # For critical context, don't nest
    handoff(specialist, nest_handoff_history=False)
    
    # Or use custom mapper
    def preserve_context(history):
        # Always keep user messages
        return [item for item in history if item["role"] == "user"]
    3. Ambiguous Handoffs

    Problem: LLM unsure which handoff to use.

    Solution: Clear descriptions and maybe input types:

    Python
    handoff(
        billing_agent,
        tool_description_override=(
            "Use this for billing, payments, refunds, "
            "and subscription issues only. "
            "NOT for technical support or sales inquiries."
        ),
    )
    4. Too Many Handoffs

    Problem: Excessive handoffs slow down response.

    Solution: Consolidate similar agents:

    Python
    # Before - too many
    handoffs=[
        handoff(billing_refund),
        handoff(billing_payment),
        handoff(billing_subscription),
    ]
    
    # After - consolidated
    handoffs=[handoff(billing_agent)]

    Handoff vs Agent as Tool Decision

    When to Use Handoffs

    Use handoffs when:

    • The new agent needs full conversation context
    • The new agent takes over the conversation
    • Domain expertise routing is needed
    • Long-running specialized tasks
    When to Use Agent as Tool

    Use agent as tool when:

    • The subtask is self-contained
    • The parent agent should continue the conversation
    • The subtask returns specific data
    • Parallel execution of multiple subtasks
    Example Comparison

    Handoff:

    Python
    # User: "Help me debug my code"
    # Generalist: [hands off to coder]
    # Coder: Takes over, debugs, provides solution
    # Conversation continues with coder

    Agent as Tool:

    Python
    # User: "Help me debug my code"
    # Generalist: Calls coder_tool("debug my code")
    # Coder: Debugs and returns "Fixed: changed line 42"
    # Generalist: "I fixed your code by changing line 42"
    # Conversation continues with generalist

    Summary

    Handoffs enable powerful multi-agent workflows. Key takeaways:

    1. Handoffs transfer control from one agent to another
    2. handoff() helper creates handoff objects
    3. Tool name/description can be customized
    4. Structured input allows passing data to handoffs
    5. Input filters control what information is passed
    6. Nested history collapses conversation to save tokens
    7. Dynamic enablement allows context-dependent handoffs
    8. Full history preserves complete context
    9. Collapsed history reduces token usage
    10. Custom mappers provide fine-grained control
    11. Triage pattern routes to specialists
    12. Supervisor pattern delegates tasks
    13. Escalation pattern moves up tiers
    14. Collaboration pattern chains agents
    15. Context switching changes domains
    16. Sessions maintain continuity across handoffs
    17. Tracing tracks handoff events
    18. Clear descriptions help the LLM choose correctly
    19. Appropriate granularity avoids too many agents
    20. vs Agent as Tool - different use cases

    Handoffs are essential for building sophisticated multi-agent systems with clear separation of concerns.