Agent Handoff Tutorial¶
What is Agent Handoff?¶
Agent Handoff is a coordination mechanism that allows one agent to transfer control and delegate work to another agent in a multi-agent system. Think of it like a relay race where each runner (agent) completes their leg and then passes the baton to the next runner.
In AgentFlow, handoff enables: - Dynamic Delegation: Agents can decide at runtime which specialist to invoke - Seamless Transitions: Control flows naturally between agents without manual intervention - Collaborative Workflows: Multiple agents work together, each contributing their expertise
The handoff system uses a simple naming convention: tools named transfer_to_<agent_name> are automatically detected as handoff tools. When an LLM calls such a tool, the framework intercepts it and navigates the graph to the target agent.
Benefits and When to Use¶
Benefits¶
1. Separation of Concerns Each agent focuses on what it does best. A research agent gathers information, a writing agent creates content, and a coordinator orchestrates the workflow.
2. Modularity and Reusability Agents are independent modules that can be: - Developed and tested separately - Reused across different workflows - Modified without affecting other agents
3. Clear Workflow Structure Handoffs make agent collaboration explicit: - Easy to trace which agent handled what - Obvious delegation points in the workflow - Self-documenting agent interactions
4. Flexibility - Add new specialist agents without restructuring existing code - Change routing logic without modifying agent implementations - Adapt workflows dynamically based on context
When to Use Handoff¶
Complex Multi-Step Workflows
Task Specialization - Different agents have different tools and expertise - Tasks naturally decompose into specialized subtasks - Quality improves when experts handle their domain
Conditional Routing - Route to different agents based on request type - Escalate to specialized agents when needed - Return to coordinator for final synthesis
Code Example Walkthrough¶
Let's break down the complete example from handoff_multi_agent.py to understand how to build a multi-agent handoff system.
Code Example Walkthrough¶
Let's break down the complete example from handoff_multi_agent.py to understand how to build a multi-agent handoff system.
Part 1: Setup and Imports¶
from dotenv import load_dotenv
from litellm import completion
from agentflow.adapters.llm.model_response_converter import ModelResponseConverter
from agentflow.checkpointer import InMemoryCheckpointer
from agentflow.graph import StateGraph, ToolNode
from agentflow.prebuilt.tools import create_handoff_tool
from agentflow.state import AgentState, Message
from agentflow.utils.constants import END
from agentflow.utils.converter import convert_messages
load_dotenv()
checkpointer = InMemoryCheckpointer()
What's happening: - Import necessary modules for building the multi-agent system - Load environment variables (API keys for LLM) - Initialize an in-memory checkpointer for state persistence
The create_handoff_tool is the key import that enables agent-to-agent transfers.
Part 2: Define Regular Tools¶
def get_weather(
location: str,
tool_call_id: str | None = None,
state: AgentState | None = None,
) -> str:
"""Get the current weather for a specific location."""
return f"The weather in {location} is sunny, 25°C"
def search_web(query: str, tool_call_id: str | None = None) -> str:
"""Search the web for information."""
return f"Search results for '{query}': Found relevant information"
def write_document(content: str, title: str, tool_call_id: str | None = None) -> str:
"""Write a document with the given content and title."""
return f"Document '{title}' written successfully"
What's happening:
- Define regular tools that agents will use for their work
- get_weather: Provides weather information
- search_web: Simulates web search functionality
- write_document: Creates documents
Note the optional parameters tool_call_id and state - these are automatically injected by the framework when needed (dependency injection feature).
Part 3: Create Tool Nodes with Handoff Tools¶
# Coordinator has access to handoff tools for delegation
coordinator_tools = ToolNode([
create_handoff_tool("researcher", "Transfer to research specialist"),
create_handoff_tool("writer", "Transfer to writing specialist"),
get_weather, # Also has regular tools
])
# Researcher can search and handoff to writer or coordinator
researcher_tools = ToolNode([
search_web,
create_handoff_tool("coordinator", "Transfer back to coordinator"),
create_handoff_tool("writer", "Transfer to writer with findings"),
])
# Writer can create documents and handoff back to coordinator
writer_tools = ToolNode([
write_document,
create_handoff_tool("coordinator", "Transfer back to coordinator"),
])
What's happening:
- Each agent gets its own ToolNode containing both regular tools and handoff tools
- create_handoff_tool("agent_name", "description") creates a tool that transfers control to that agent
- The description helps the LLM understand when to use each handoff tool
Key pattern: Each agent has: 1. Regular tools for its specialized work 2. Handoff tools to delegate to other agents
Part 4: Define Agent Functions¶
Coordinator Agent¶
def coordinator_agent(state: AgentState):
"""Coordinator agent that delegates tasks to specialized agents."""
prompts = """
You are a coordinator agent. Your job is to:
1. Understand user requests
2. Delegate tasks to specialized agents:
- Use transfer_to_researcher for investigation
- Use transfer_to_writer for content creation
3. You can also check weather using get_weather tool
Always explain your decision to delegate.
"""
messages = convert_messages(
system_prompts=[{"role": "system", "content": prompts}],
state=state,
)
# Check if last message is a tool result
if state.context and len(state.context) > 0 and state.context[-1].role == "tool":
# Final response without tools
response = completion(model="gemini/gemini-2.0-flash-exp", messages=messages)
else:
# Regular response with tools available
tools = coordinator_tools.all_tools_sync()
response = completion(
model="gemini/gemini-2.0-flash-exp",
messages=messages,
tools=tools
)
return ModelResponseConverter(response, converter="litellm")
What's happening: 1. System Prompt: Clearly defines the agent's role and available tools 2. Convert Messages: Prepares messages in the format expected by the LLM 3. Conditional Tool Usage: - If last message is a tool result, make a final response without offering tools - Otherwise, provide tools so LLM can call them 4. LLM Call: Uses LiteLLM to call the model with tools 5. Response Conversion: Converts LLM response to AgentFlow format
Pattern: This structure is repeated for all agents with different prompts and tools.
Researcher Agent¶
def researcher_agent(state: AgentState):
"""Researcher agent that performs detailed investigation."""
prompts = """
You are a research specialist. Your job is to:
1. Investigate topics using the search_web tool
2. Gather comprehensive information
3. Transfer to writer agent if content needs creation
4. Transfer back to coordinator if task is complete
Be thorough in your research.
"""
# ... (same structure as coordinator)
What's happening:
- Researcher focuses on investigation using search_web tool
- Can delegate to writer for content creation
- Can return to coordinator when research is complete
Writer Agent¶
def writer_agent(state: AgentState):
"""Writer agent that creates content and documents."""
prompts = """
You are a writing specialist. Your job is to:
1. Create clear, engaging content
2. Use write_document tool to save content
3. Transfer back to coordinator when complete
Focus on clarity and structure.
"""
# ... (same structure as coordinator)
What's happening:
- Writer specializes in content creation
- Uses write_document tool
- Returns to coordinator after completion
Part 5: Define Routing Logic¶
def should_continue_coordinator(state: AgentState) -> str:
"""Route from coordinator to tools or end."""
if not state.context or len(state.context) == 0:
return "coordinator_tools"
last_message = state.context[-1]
# If agent wants to call tools, route to tool node
if (
hasattr(last_message, "tools_calls")
and last_message.tools_calls
and len(last_message.tools_calls) > 0
and last_message.role == "assistant"
):
return "coordinator_tools"
# If tool results came back, return to agent for processing
if last_message.role == "tool":
return "coordinator"
# Otherwise, we're done
return END
What's happening: 1. Empty Context: If no messages yet, go to tools 2. Agent Called Tools: If the agent made tool calls, route to the tool node for execution 3. Tool Results Returned: If tools executed, return to agent to process results 4. No More Actions: If agent didn't call tools and it's not processing results, we're done
Pattern: This same logic is used for all agents (should_continue_researcher, should_continue_writer).
Critical insight: The routing function creates the agent ↔ tools loop:
Part 6: Build the Graph¶
graph = StateGraph()
# Add all nodes
graph.add_node("coordinator", coordinator_agent)
graph.add_node("coordinator_tools", coordinator_tools)
graph.add_node("researcher", researcher_agent)
graph.add_node("researcher_tools", researcher_tools)
graph.add_node("writer", writer_agent)
graph.add_node("writer_tools", writer_tools)
# Set entry point
graph.set_entry_point("coordinator")
What's happening:
- Create a StateGraph instance
- Add each agent and its corresponding tool node
- Set the coordinator as the entry point (first agent to handle requests)
Pattern: For each agent, we add two nodes: - The agent node (runs the LLM) - The tool node (executes tools)
Pattern: For each agent, we add two nodes: - The agent node (runs the LLM) - The tool node (executes tools)
Part 7: Add Conditional Edges¶
# Add edges for coordinator
graph.add_conditional_edges(
"coordinator",
should_continue_coordinator,
{
"coordinator_tools": "coordinator_tools",
END: END,
},
)
# Add edges for researcher
graph.add_conditional_edges(
"researcher",
should_continue_researcher,
{
"researcher_tools": "researcher_tools",
END: END,
},
)
# Add edges for writer
graph.add_conditional_edges(
"writer",
should_continue_writer,
{
"writer_tools": "writer_tools",
END: END,
},
)
What's happening:
- add_conditional_edges defines routing logic from each agent
- The routing function (e.g., should_continue_coordinator) returns a key
- The path map (dictionary) determines where to go next
- Each agent can either:
- Go to its tool node to execute tools
- Go to END when done
Critical note: Notice we DON'T add explicit edges from tool nodes back to agents!
Why? Because handoff tools automatically handle navigation:
- Regular tools → return results → routing function routes back to agent
- Handoff tools → return Command(goto=target_agent) → graph navigates to that agent
This is the magic of handoff: when transfer_to_researcher is called, the framework automatically navigates to the researcher agent without needing explicit edges.
Part 8: Compile and Run¶
# Compile the graph
app = graph.compile(checkpointer=checkpointer)
# Run the example
if __name__ == "__main__":
# Create input message
inp = {
"messages": [
Message.text_message(
"Please research quantum computing and write a brief article about it."
)
]
}
# Configure execution
config = {
"thread_id": "handoff-demo-001",
"recursion_limit": 15
}
# Invoke the graph
result = app.invoke(inp, config=config)
# Display results
for msg in result["messages"]:
print(f"[{msg.role}] {msg.text()[:200]}...")
What's happening: 1. Compile: Converts the graph definition into an executable workflow 2. Create Input: Wrap user message in the expected format 3. Configure: Set thread ID for state persistence and recursion limit 4. Invoke: Execute the graph synchronously 5. Display: Show the conversation history
Expected Flow:
1. User → "Research quantum computing and write about it"
2. Coordinator → Analyzes request → Calls transfer_to_researcher
3. [HANDOFF] → Graph navigates to researcher agent
4. Researcher → Calls search_web → Processes results → Calls transfer_to_writer
5. [HANDOFF] → Graph navigates to writer agent
6. Writer → Calls write_document → Creates content → Calls transfer_to_coordinator
7. [HANDOFF] → Graph navigates back to coordinator
8. Coordinator → Provides final summary → Done
Understanding the Execution Flow¶
1. Initial Request Processing¶
- User sends: "Research quantum computing and write about it" - Graph starts at entry point (coordinator) - Coordinator receives the request2. Coordinator Decision¶
- Coordinator's LLM analyzes the request - Determines research is needed - Callstransfer_to_researcher tool
3. Handoff Detection¶
- Tool node receives the tool call - Pattern matching detectstransfer_to_* prefix
- Instead of executing, returns a navigation command
- Graph automatically routes to researcher agent
4. Researcher Execution¶
- Researcher agent now has control - Usessearch_web tool to gather information
- Processes the search results
- Calls transfer_to_writer to delegate content creation
5. Second Handoff¶
- Another handoff is detected - Graph navigates to writer agent - Context and state are preserved6. Writer Execution¶
- Writer creates content usingwrite_document tool
- After completion, transfers back to coordinator
- Another handoff navigation occurs
7. Final Response¶
- Coordinator receives control again - Synthesizes the work done by specialists - Provides final response to user - No more tool calls, so routing goes to ENDKey Concepts Explained¶
Handoff vs Regular Tool Calls¶
Regular Tool:
def search_web(query: str) -> str:
# Does actual work
return "search results"
# When called:
Agent → calls search_web → Tool executes → Returns result → Agent processes result
Handoff Tool:
transfer_to_researcher = create_handoff_tool("researcher")
# When called:
Agent → calls transfer_to_researcher → Handoff detected → Command(goto="researcher")
→ Graph navigates to researcher agent
The Agent-Tool Loop¶
Each agent follows this loop:
1. Agent (LLM) thinks and decides what to do
2. If needs tools → calls tool(s)
3. Tool node executes tools
4. If regular tool → return result → back to agent (step 1)
5. If handoff tool → navigate to target agent
6. If no tools → END
State Preservation¶
Throughout all handoffs, the state is preserved: - All messages in the conversation history - Context from previous agents - Tool call results
This allows each agent to see what previous agents did and build upon their work.
Common Patterns¶
Pattern 1: Hub and Spoke¶
- Coordinator delegates to specialists
- Specialists work independently
- All return to coordinator for synthesis
Pattern 2: Sequential Pipeline¶
- Each agent does one step
- Passes result to next agent
- Linear workflow
Pattern 3: Conditional Routing¶
- Router analyzes request
- Routes to appropriate specialist based on criteria
- Different paths for different request types
Tips and Best Practices¶
1. Clear Agent Roles¶
Define clear responsibilities in system prompts:
prompts = """
You are a researcher. Your ONLY job is:
- Investigate using search_web tool
- Gather comprehensive information
- Transfer to writer when done
Do NOT write content yourself - that's the writer's job.
"""
2. Explicit Handoff Instructions¶
Tell agents exactly when to hand off:
prompts = """
After gathering information, ALWAYS transfer to the writer agent
using transfer_to_writer. Do not try to write content yourself.
"""
3. Set Recursion Limits¶
Prevent infinite loops:
4. Log Handoffs¶
Enable logging to see handoff flow:
import logging
logging.basicConfig(level=logging.INFO)
# You'll see:
# INFO: Handoff detected: transfer_to_researcher -> researcher
5. Handle Edge Cases¶
Add error handling for when agents get stuck:
def should_continue(state: AgentState) -> str:
# Check if we've been here too many times
if state.step_count > 10:
return END
# ... normal logic
Troubleshooting¶
Handoff Not Working¶
Problem: Agent calls handoff tool but nothing happens
Solutions:
1. Check tool name follows pattern: transfer_to_<agent_name>
2. Verify target agent exists in graph with exact name
3. Enable logging to see if handoff is detected
4. Check routing logic includes target agent in path map
Agent Loops Forever¶
Problem: Agents keep handing off to each other
Solutions:
1. Set lower recursion_limit in config
2. Add termination conditions in routing functions
3. Review agent prompts - make completion criteria clear
4. Add state checks to detect loops
Wrong Agent Receives Control¶
Problem: Handoff goes to unexpected agent
Solutions:
1. Verify tool name spelling matches agent node name exactly
2. Check add_node("agent_name", ...) uses same name
3. Review routing function logic
4. Enable debug logging to trace execution
Summary¶
Agent Handoff enables building sophisticated multi-agent systems where:
- Agents specialize in specific tasks
- Handoff tools enable dynamic delegation
- Graph automatically routes based on handoff calls
- State is preserved across all transfers
- Workflow emerges from agent decisions
The key insight: handoff tools don't execute - they navigate. This makes multi-agent collaboration feel natural and intuitive.
Start with the example in examples/handoff/handoff_multi_agent.py, modify the agents and tools for your use case, and build complex workflows with ease!
See Also¶
- Handoff Concept - Core concepts and minimal examples
- Tool Nodes - Working with tools in graphs
- Control Flow - Understanding graph routing
- Command - Navigation commands in graphs