Handoffs¶
Agent-to-agent routing for multi-agent systems.
Handoffs let you decompose complex tasks across specialized agents. A "router" agent receives the user's request, decides which specialist should handle it, and transfers control. The specialist processes the request and either responds directly or hands off to another agent. This pattern is fundamental to building scalable, maintainable multi-agent systems.
The Handoff Concept¶
flowchart TD
U[User] --> R[Router Agent]
R -->|transfer_to_coder| C[Coder Agent]
R -->|transfer_to_writer| W[Writer Agent]
R -->|transfer_to_researcher| RE[Researcher Agent]
C --> R2[Response]
W --> R2
RE --> R2
R2 --> U
Each handoff is a directed edge between two agents. The source agent receives a tool (e.g., transfer_to_coder) that, when called, switches execution to the target agent. The Runner handles the seamless transfer of conversation context.
The Handoff Dataclass¶
from dataclasses import dataclass
from typing import Callable
@dataclass
class Handoff:
source: Agent # Agent that initiates the handoff
target: Agent # Agent that receives control
description: str = "" # Description shown to the model
condition: Callable[[ToolContext, str], bool] | None = None # Optional conditional logic
input_filter: Callable[[HandoffData], HandoffData] | None = None # Optional input transformation
@property
def tool_name(self) -> str:
"""Tool name used to trigger this handoff: transfer_to_{target.name}"""
return f"transfer_to_{self.target.name}"
@property
def tool_description(self) -> str:
"""Description shown to the model for this handoff tool."""
return self.description or f"Transfer to {self.target.name}"
def should_handoff(self, ctx: ToolContext, user_input: str) -> bool:
"""Check if handoff should trigger. Always True if no condition is set."""
if self.condition is None:
return True
return self.condition(ctx, user_input)
Properties¶
| Property | Type | Description |
|---|---|---|
tool_name |
str |
Auto-generated as transfer_to_{target.name} |
tool_description |
str |
Falls back to "Transfer to {target.name}" if no description is provided |
Methods¶
| Method | Returns | Description |
|---|---|---|
should_handoff(ctx, user_input) |
bool |
Evaluates the condition callable, or returns True if no condition is set |
Creating Handoffs¶
Basic Handoff¶
The simplest handoff just defines a source and target:
from flux import Agent
from flux.handoffs.handoff import Handoff
coder = Agent(
name="coder",
instructions="You write Python code. Focus on clean, correct code.",
model="qwen2:1.5b",
)
writer = Agent(
name="writer",
instructions="You write clear, engaging content.",
model="qwen2:1.5b",
)
handoff = Handoff(
source=coder,
target=writer,
description="Transfer to the writer agent for content creation tasks",
)
Handoff with Description¶
Write descriptive tool descriptions
The description is shown to the model as the tool definition. A clear, specific description helps the model decide when to use the handoff:
handoff = Handoff(
source=router,
target=expert,
description=(
"Transfer to the expert agent when the user asks about "
"technical architecture, system design, or infrastructure."
),
)
The Router Pattern¶
The most common multi-agent pattern is a "router" agent whose sole job is to dispatch to specialists. The router has handoffs to each specialist but does not do any work itself.
from flux import Agent, Runner
from flux.handoffs.handoff import Handoff
from flux.models.ollama import OllamaModel
model = OllamaModel(model="qwen2:1.5b")
# Specialist agents
coder = Agent(
name="coder",
instructions="You write Python code. Use tools when needed.",
model=model,
)
writer = Agent(
name="writer",
instructions="You write clear, engaging content. Be creative.",
model=model,
)
researcher = Agent(
name="researcher",
instructions="You research topics and provide accurate information.",
model=model,
)
# Router agent
router = Agent(
name="router",
instructions=(
"You are a triage agent. Analyze the user's request and transfer "
"to the most appropriate specialist. Do not attempt to answer directly."
),
model=model,
handoffs=[
Handoff(source=router, target=coder, description="Transfer to the coder for programming tasks"),
Handoff(source=router, target=writer, description="Transfer to the writer for content creation"),
Handoff(source=router, target=researcher, description="Transfer to the researcher for information gathering"),
],
)
# Run
result = await Runner.run(router, "Write a Python function to sort a list")
print(f"Final agent: {result.last_agent.name}")
print(f"Output: {result.final_output}")
How Routing Works
When the router agent calls the transfer_to_coder tool, the Runner:
- Recognizes it as a handoff tool (matching
tool_name). - Checks the handoff's
condition(if any). - Applies the
input_filter(if any) to transform the conversation. - Switches the active agent to the target.
- Continues the run loop with the new agent.
sequenceDiagram
participant U as User
participant R as Router
participant Run as Runner
participant C as Coder
U->>R: "Write a hello world in Python"
R->>R: Analyzes request
R->>Run: Calls transfer_to_coder
Run->>Run: Matches handoff by tool_name
Run->>Run: Checks condition (if any)
Run->>C: Activates Coder agent
C->>C: Generates code
C->>U: "Here is your hello world..."
Condition-Based Handoffs¶
Add a condition callable to control when a handoff is allowed. The condition receives the ToolContext and the user's input text:
from flux.handoffs.handoff import Handoff
def is_coding_request(ctx, user_input: str) -> bool:
"""Only hand off for coding-related requests."""
coding_keywords = ["code", "python", "function", "script", "debug", "program"]
return any(kw in user_input.lower() for kw in coding_keywords)
handoff = Handoff(
source=router,
target=coder,
description="Transfer to the coder for programming tasks",
condition=is_coding_request,
)
The condition is evaluated before the handoff executes. If it returns False, the handoff is skipped and the current agent continues.
Combining Multiple Conditions¶
def is_data_task(ctx, user_input: str) -> bool:
keywords = ["data", "csv", "pandas", "analyze", "dataset"]
return any(kw in user_input.lower() for kw in keywords)
def has_file_context(ctx, user_input: str) -> bool:
# Check if context contains file information
return ctx is not None and hasattr(ctx, "file_path")
def combined_condition(ctx, user_input: str) -> bool:
return is_data_task(ctx, user_input) and has_file_context(ctx, user_input)
handoff = Handoff(
source=router,
target=data_agent,
description="Transfer to the data agent for data analysis tasks",
condition=combined_condition,
)
Input Filtering¶
The input_filter callable transforms the HandoffData before the target agent processes it. Use this to redact information, summarize context, or reshape the conversation history.
HandoffData¶
@dataclass
class HandoffData:
source_agent_name: str # Name of the agent handing off
target_agent_name: str # Name of the agent receiving
input_text: str # The user's input text
context: dict[str, Any] = field(default_factory=dict) # Additional context
Example: Redacting Sensitive Information¶
from flux.handoffs.handoff import Handoff, HandoffData
def redact_sensitive(data: HandoffData) -> HandoffData:
"""Redact potential secrets before handing off."""
text = data.input_text
# Simple redaction (replace with your actual logic)
for keyword in ["password", "secret", "api_key", "token"]:
text = text.replace(keyword, "[REDACTED]")
return HandoffData(
source_agent_name=data.source_agent_name,
target_agent_name=data.target_agent_name,
input_text=text,
context=data.context,
)
handoff = Handoff(
source=authenticated_agent,
target=public_agent,
description="Transfer to public agent after removing sensitive data",
input_filter=redact_sensitive,
)
Example: Adding Context¶
def enrich_context(data: HandoffData) -> HandoffData:
"""Add metadata about the source agent."""
return HandoffData(
source_agent_name=data.source_agent_name,
target_agent_name=data.target_agent_name,
input_text=data.input_text,
context={
**data.context,
"originated_from": data.source_agent_name,
"timestamp": time.time(),
},
)
handoff = Handoff(
source=triage_agent,
target=support_agent,
description="Transfer to support with context",
input_filter=enrich_context,
)
Handoff Error Handling¶
If a handoff fails (e.g., the target agent is not found), Flux raises a HandoffError:
from flux.exceptions import HandoffError
try:
result = await Runner.run(router, "Some request")
except HandoffError as e:
print(f"Handoff failed: {e}")
Best Practices¶
Give the router clear instructions
The router's system prompt should emphasize that it should not try to answer questions itself -- only dispatch to specialists. Vague router prompts lead to poor routing decisions.
Write descriptive handoff descriptions
The description is the tool definition the model sees. Make it specific enough that the model can distinguish between similar handoffs. Bad: "Transfer to agent." Good: "Transfer to the coder agent for Python, JavaScript, or any programming task."
Use conditions for precision
When multiple agents could handle similar requests, add conditions to narrow the decision space. This prevents unnecessary handoffs and reduces latency.
Keep handoff chains short
A -> B -> C is fine. A -> B -> C -> D -> E is a debugging nightmare. Prefer hub-and-spoke (router pattern) over long chains.
Use input filters to isolate agents
If the specialist agent should not see the full conversation history (for privacy, context window, or focus reasons), use input_filter to trim or transform the input.
Log handoffs for observability
Handoff events are emitted through the event bus (event type handoff). Subscribe to them to track routing patterns and diagnose issues in multi-agent systems.
Test each agent independently
Before testing the full multi-agent system, verify that each specialist works correctly on its own. Handoff bugs are often really agent configuration bugs.