sequenceDiagram
participant U as User
participant M as Main Agent
participant W as Weather Agent
participant T as get_weather Tool
U->>M: "What's the weather in Tokyo?"
M->>M: Decides: weather question
M->>M: Calls transfer_to_weather_specialist
Note over M: Handoff occurs
M->>W: Control transferred
W->>T: Calls get_weather("Tokyo")
T->>W: "72°F, sunny"
W->>U: "The weather in Tokyo is 72°F and sunny!"
5 Agent Handoff
One agent can’t do everything. Let’s enable routing to specialists.
Note
Code Reference: code/v0.2/src/agentsilex/
agent.pyrunner.py
5.1 The Problem
A general assistant might need to handle:
- Weather questions → Weather specialist
- Math problems → Calculator specialist
- Code questions → Coding specialist
Instead of one bloated agent with all tools, we use specialized sub-agents and route between them.
5.2 The Handoff Class
First, we wrap agents as handoff targets (agent.py):
HANDOFF_TOOL_PREFIX = "transfer_to_"
def as_valid_tool_name(name: str, prefix: str | None = None) -> str:
"""Sanitize agent name for use as tool name."""
sanitized = re.sub(r"[^a-zA-Z0-9_.:+-]", "_", name)
if sanitized and not re.match(r"^[a-zA-Z_]", sanitized):
sanitized = "_" + sanitized
if prefix:
sanitized = prefix + sanitized
return sanitized[:64] if sanitized else "_unnamed"
class Handoff:
def __init__(self, agent: "Agent"):
self.agent = agent
@property
def name(self):
return as_valid_tool_name(self.agent.name, prefix="transfer_to_")
@property
def description(self):
return f"Handoff to the {self.agent.name} agent to handle the request. {self.agent.instructions}"
@property
def parameters_specification(self):
return {}Key points:
- Tool name is
transfer_to_{agent_name} - Description includes the agent’s instructions (so LLM knows when to use it)
- No parameters needed — just the handoff action
5.3 AgentHandoffs: Managing Handoff Targets
class AgentHandoffs:
def __init__(self, handoffs: List["Agent"]):
self.handoffs: List[Handoff] = [Handoff(agent) for agent in handoffs]
self.registry: Dict[str, Handoff] = {
handoff.name: handoff for handoff in self.handoffs
}
def get_specification(self):
"""Generate tool specs for all handoff agents."""
spec = []
for handoff in self.handoffs:
spec.append({
"type": "function",
"function": {
"name": handoff.name,
"description": handoff.description,
"parameters": handoff.parameters_specification,
},
})
return spec
def handoff_agent(self, call_spec) -> Tuple["Agent", Dict]:
"""Execute handoff and return new agent + response message."""
handoff = self.registry[call_spec.function.name]
handoff_response = {
"role": "tool",
"tool_call_id": call_spec.id,
"content": json.dumps({"assistant": handoff.agent.name}),
}
return handoff.agent, handoff_responseSimilar pattern to ToolsSet:
- Registry for lookup
get_specification()for LLMhandoff_agent()returns both the new agent and a tool response
5.4 Updated Agent Class
class Agent:
def __init__(
self,
name: str,
model: Any,
instructions: str,
tools: List[FunctionTool] | None = None,
handoffs: List["Agent"] | None = None, # NEW
):
self.name = name
self.model = model
self.instructions = instructions
self.tools = tools or []
self.tools_set = ToolsSet(self.tools)
self.handoffs = AgentHandoffs(handoffs or []) # NEW
def get_system_prompt(self):
return {"role": "system", "content": self.instructions}Agents can now have handoffs — a list of other agents they can transfer to.
5.5 Updated Runner
The Runner now handles both tools and handoffs:
class Runner:
def __init__(self, session: Session):
self.session = session
def run(self, agent: Agent, prompt: str) -> RunResult:
current_agent = agent # Track which agent is active
msg = user_msg(prompt)
self.session.add_new_messages([msg])
loop_count = 0
should_stop = False
while loop_count < 10 and not should_stop:
dialogs = self.session.get_dialogs()
# Combine regular tools + handoff tools
tools_spec = (
current_agent.tools_set.get_specification()
+ current_agent.handoffs.get_specification()
)
# System prompt depends on current agent
complete_dialogs = [current_agent.get_system_prompt()] + dialogs
response = completion(
model=current_agent.model,
messages=complete_dialogs,
tools=tools_spec if tools_spec else None,
)
response_message = response.choices[0].message
self.session.add_new_messages([response_message])
if not response_message.tool_calls:
should_stop = True
return RunResult(final_output=response_message.content)
# Execute regular function calls first
tools_response = [
current_agent.tools_set.execute_function_call(call_spec)
for call_spec in response_message.tool_calls
if not call_spec.function.name.startswith(HANDOFF_TOOL_PREFIX)
]
self.session.add_new_messages(tools_response)
# Then handle agent handoffs
handoff_responses = [
call_spec
for call_spec in response_message.tool_calls
if call_spec.function.name.startswith(HANDOFF_TOOL_PREFIX)
]
if handoff_responses:
# If multiple handoffs, pick the first one
agent_spec = handoff_responses[0]
current_agent, handoff_response = current_agent.handoffs.handoff_agent(
agent_spec
)
self.session.add_new_messages([handoff_response])
loop_count += 1
return RunResult(final_output="Error: Exceeded max iterations")Key changes:
current_agenttracking — Which agent is currently active- Combined tool specs — Regular tools + handoff tools
- Two-phase processing — Execute tools first, then handle handoffs
- Agent switch —
current_agentchanges when handoff occurs
5.6 Example: Multi-Agent System
from agentsilex import Agent, Runner, Session, tool
# Specialist agents
@tool
def get_weather(city: str) -> str:
"""Get weather for a city."""
return f"Weather in {city}: 72°F, sunny"
weather_agent = Agent(
name="weather_specialist",
model="gpt-4o",
instructions="You are a weather expert. Always use the get_weather tool.",
tools=[get_weather],
)
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
math_agent = Agent(
name="math_specialist",
model="gpt-4o",
instructions="You are a math expert. Use the calculate tool for computations.",
tools=[calculate],
)
# Main router agent
main_agent = Agent(
name="assistant",
model="gpt-4o",
instructions="""You are a helpful assistant.
- For weather questions, transfer to weather_specialist
- For math questions, transfer to math_specialist
- For other questions, answer directly""",
handoffs=[weather_agent, math_agent],
)
# Run
session = Session()
runner = Runner(session)
result = runner.run(main_agent, "What's the weather in Tokyo?")
# Flow: main_agent → transfer_to_weather_specialist → weather_agent → get_weather → response5.7 What the LLM Sees
When main_agent runs, it sees these tools:
[
{
"type": "function",
"function": {
"name": "transfer_to_weather_specialist",
"description": "Handoff to the weather_specialist agent... You are a weather expert...",
"parameters": {}
}
},
{
"type": "function",
"function": {
"name": "transfer_to_math_specialist",
"description": "Handoff to the math_specialist agent... You are a math expert...",
"parameters": {}
}
}
]The LLM decides which specialist to call based on the user’s request.
5.8 Handoff Flow Diagram
5.9 Key Design Decisions
| Decision | Why |
|---|---|
transfer_to_ prefix |
Easy to identify handoffs vs regular tools |
| Handoffs as tools | Reuse existing tool infrastructure |
| First handoff wins | Simplicity when multiple are called |
| System prompt per agent | Each agent has its own personality |
TipCheckpoint
cd code/v0.2You now have multi-agent handoffs! Agents can route to specialists.