Skip to content

Agent Handoffs

Overview

Agent handoffs enable peer-to-peer collaboration where agents explicitly transfer control to other agents based on their expertise boundaries. Unlike the supervisor pattern where a central coordinator makes routing decisions, handoffs give individual agents the autonomy to decide when and to whom to transfer conversations.

Architecture

When to Use

Use agent handoffs when:

  • Clear role boundaries: Each agent has well-defined expertise areas
  • Service scenarios: Customer service, triage, support workflows
  • Agent autonomy: Agents can assess their own capabilities
  • Peer collaboration: Direct agent-to-agent communication is natural
  • Linear handoff chains: Expected flow is mostly sequential transfers

Supervisor vs Handoffs

AspectSupervisor PatternHandoff Pattern
Decision MakingCentral supervisor decides routingIndividual agents decide handoffs
StructureHierarchical (supervisor + workers)Peer-to-peer (equal agents)
Routing MechanismSupervisor analyzes task + progressAgents use tools to signal handoff
CommunicationAll through supervisorDirect agent-to-agent
Agent AutonomyLow (supervisor controls)High (agents control)
Best ForComplex multi-step tasksService/triage scenarios
ExampleResearch → Code → ReviewSales → Support → Billing

Key Components

1. State Schema

python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
import operator

class HandoffState(TypedDict):
    messages: Annotated[list, add_messages]      # Conversation history
    task: str                                     # User's request
    current_agent: str                            # Currently active agent
    handoff_target: str                           # Agent to hand off to
    context: Annotated[list[dict], operator.add]  # Accumulated work
    handoff_history: Annotated[list[str], operator.add]  # Handoff chain
    iteration: int                                # Handoff count
    max_iterations: int                           # Safety limit
    final_result: str                             # Final response

2. Handoff Tools

Handoff tools enable agents to explicitly transfer control:

python
from langgraph_ollama_local.patterns.handoffs import create_handoff_tool

# Create handoff tools
handoff_to_support = create_handoff_tool(
    target_agent="support",
    description="Transfer to support for technical issues or bugs",
)

handoff_to_billing = create_handoff_tool(
    target_agent="billing",
    description="Transfer to billing for payment or invoice questions",
)

handoff_to_sales = create_handoff_tool(
    target_agent="sales",
    description="Transfer to sales for product questions or purchases",
)

Tool Behavior:

  • Tool name format: handoff_to_<agent>
  • Takes reason parameter: Brief explanation for the handoff
  • Returns confirmation message
  • Signals routing system to transfer control

3. Agent Nodes

Each agent can:

  1. Process requests in their domain
  2. Use handoff tools when requests are outside their expertise
python
from langgraph_ollama_local.patterns.handoffs import create_handoff_agent_node

sales_agent = create_handoff_agent_node(
    llm,
    agent_name="sales",
    agent_role="Handle product inquiries, pricing, and sales",
    handoff_tools=[handoff_to_support, handoff_to_billing],
)

support_agent = create_handoff_agent_node(
    llm,
    agent_name="support",
    agent_role="Handle technical issues and troubleshooting",
    handoff_tools=[handoff_to_billing, handoff_to_sales],
)

billing_agent = create_handoff_agent_node(
    llm,
    agent_name="billing",
    agent_role="Handle payments, invoices, and refunds",
    handoff_tools=[handoff_to_sales, handoff_to_support],
)

Agent Behavior:

  • Receives task and previous context
  • Decides whether to handle or hand off
  • If handling: Provides response, sets handoff_target = ""
  • If handing off: Uses handoff tool, sets handoff_target = <agent>

4. Routing Logic

Routing is based on agent decisions:

python
from langgraph_ollama_local.patterns.handoffs import route_handoffs

def route_handoffs(state):
    """
    Route based on handoff decisions.

    - If handoff_target is set → route to that agent
    - If no handoff → agent completed task → go to completion
    - If at max iterations → force completion
    """
    if state["iteration"] >= state["max_iterations"]:
        return "complete"

    if state["handoff_target"]:
        return state["handoff_target"]  # Route to specified agent

    return "complete"  # No handoff means task complete

5. Graph Construction

python
from langgraph.graph import StateGraph, START, END
from langgraph_ollama_local.patterns.handoffs import create_handoff_graph

# Method 1: Using the builder function
graph = create_handoff_graph(
    llm,
    agents={
        "sales": ("Handle sales inquiries", [handoff_to_support, handoff_to_billing]),
        "support": ("Handle technical issues", [handoff_to_billing, handoff_to_sales]),
        "billing": ("Handle payments", [handoff_to_sales, handoff_to_support]),
    },
    entry_agent="sales",  # First agent handles all requests
    max_iterations=10,
)

# Method 2: Manual construction
workflow = StateGraph(HandoffState)

# Add agent nodes
workflow.add_node("sales", sales_agent)
workflow.add_node("support", support_agent)
workflow.add_node("billing", billing_agent)
workflow.add_node("complete", complete_node)

# Entry point
workflow.add_edge(START, "sales")

# Conditional edges for each agent
for agent_name in ["sales", "support", "billing"]:
    workflow.add_conditional_edges(
        agent_name,
        route_handoffs,
        {
            "sales": "sales",
            "support": "support",
            "billing": "billing",
            "complete": "complete",
        },
    )

# End
workflow.add_edge("complete", END)

graph = workflow.compile()

Usage

Basic Usage

python
from langgraph_ollama_local import LocalAgentConfig
from langgraph_ollama_local.patterns.handoffs import (
    create_handoff_graph,
    create_handoff_tool,
    run_handoff_conversation,
)

config = LocalAgentConfig()
llm = config.create_chat_client()

# Create handoff tools
handoff_to_support = create_handoff_tool("support", "Technical issues")
handoff_to_billing = create_handoff_tool("billing", "Payment questions")
handoff_to_sales = create_handoff_tool("sales", "Product questions")

# Build graph
graph = create_handoff_graph(
    llm,
    agents={
        "sales": (
            "Handle product inquiries and sales",
            [handoff_to_support, handoff_to_billing],
        ),
        "support": (
            "Handle technical issues",
            [handoff_to_billing, handoff_to_sales],
        ),
        "billing": (
            "Handle payments and invoices",
            [handoff_to_sales, handoff_to_support],
        ),
    },
    entry_agent="sales",
)

# Run conversation
result = run_handoff_conversation(
    graph,
    "My app keeps crashing when I export data",
    entry_agent="sales",
    max_iterations=5,
)

print(result["final_result"])
print("Handoff chain:", " -> ".join(result["handoff_history"]))

Manual Invocation

python
from langchain_core.messages import HumanMessage

initial_state = {
    "messages": [HumanMessage(content="I need help with my invoice")],
    "task": "I need help with my invoice",
    "current_agent": "sales",
    "handoff_target": "",
    "context": [],
    "handoff_history": [],
    "iteration": 0,
    "max_iterations": 5,
    "final_result": "",
}

result = graph.invoke(initial_state)

# Access results
print("Final result:", result["final_result"])
print("Agents involved:", [c["agent"] for c in result["context"]])
print("Handoffs:", result["handoff_history"])
print("Total iterations:", result["iteration"])

Best Practices

1. Define Clear Agent Boundaries

python
# Good: Clear, distinct roles
agents = {
    "sales": ("Product questions, pricing, purchases", [...]),
    "support": ("Technical issues, troubleshooting, how-to", [...]),
    "billing": ("Payments, invoices, refunds, account billing", [...]),
}

# Bad: Overlapping responsibilities
agents = {
    "general": ("Handle everything", [...]),
    "specialist": ("Also handle everything", [...]),
}

2. Provide Handoff Guidance

python
# Good: Specific, actionable descriptions
handoff_to_support = create_handoff_tool(
    "support",
    "Transfer for: app crashes, error messages, login issues, feature not working",
)

# Bad: Vague descriptions
handoff_to_support = create_handoff_tool(
    "support",
    "Transfer for support stuff",
)

3. Set Reasonable Iteration Limits

python
# Good: Prevents infinite loops, allows necessary handoffs
graph = create_handoff_graph(llm, agents, max_iterations=5)

# Bad: Too low (prevents necessary handoffs)
graph = create_handoff_graph(llm, agents, max_iterations=1)

# Bad: Too high (allows excessive handoffs)
graph = create_handoff_graph(llm, agents, max_iterations=50)

Common Patterns

Customer Service Triage

python
# Entry: Sales handles all requests, hands off as needed
agents = {
    "sales": ("Product info, pricing", [to_support, to_billing]),
    "support": ("Technical help", [to_billing, to_sales]),
    "billing": ("Payments, refunds", [to_sales, to_support]),
}
entry_agent = "sales"

Technical Support Escalation

python
# Entry: L1 support, escalates to L2/L3
agents = {
    "l1_support": ("Basic troubleshooting", [to_l2, to_billing]),
    "l2_support": ("Advanced troubleshooting", [to_l3, to_l1]),
    "l3_support": ("Engineering escalation", [to_l2]),
    "billing": ("Account issues", [to_l1]),
}
entry_agent = "l1_support"

Medical Triage

python
# Entry: Intake nurse, routes to specialists
agents = {
    "intake": ("Initial assessment", [to_general, to_urgent, to_mental]),
    "general": ("General medicine", [to_specialist]),
    "urgent": ("Urgent care", [to_emergency]),
    "mental": ("Mental health", [to_crisis]),
}
entry_agent = "intake"

Common Pitfalls

PitfallSolution
Infinite handoff loopsSet max_iterations to reasonable value (5-10)
Unclear handoff reasonsProvide specific tool descriptions
Lost contextSystem preserves context automatically
Too many handoffsReview agent roles for overlap
Wrong entry agentChoose triage/generalist as entry
Agents don't complete tasksInstruct agents to avoid unnecessary handoffs

Quiz

Test your understanding of agent handoffs:

Knowledge Check

What is the key difference between supervisor and handoff patterns?

AHandoffs are inherently faster than supervisor patterns
BIn handoffs, agents decide routing themselves; in supervisor, a central coordinator decides
CHandoffs always use fewer LLM calls
DSupervisor pattern requires more agents to function

Knowledge Check

What is the purpose of handoff tools in the agent handoff pattern?

ATo call external APIs and fetch data
BTo enable agents to explicitly transfer control to other peer agents
CTo validate and approve agent responses before output
DTo store and retrieve conversation history

Knowledge Check

What scenario is best suited for the handoff pattern over the supervisor pattern?

AComplex research and analysis tasks requiring iteration
BCustomer service triage with clear expertise boundaries between agents
CTasks requiring multiple review cycles
DMulti-step code generation and review pipelines

Knowledge Check

What happens when an agent sets handoff_target to an empty string?

AThe system throws an error
BThe supervisor takes over routing
CThe task is considered complete and routes to completion
DThe conversation continues with the same agent

Knowledge Check

Why is setting a reasonable max_iterations limit important in the handoff pattern?

ATo reduce API costs
BTo prevent infinite handoff loops between agents
CTo improve response quality
DTo ensure all agents get called at least once