How to Implement Multi-Agent Handoffs in LiveKit Agents
Multi-agent handoffs in LiveKit Agents are triggered when a function tool returns an Agent instance, automatically transitioning conversation control while preserving chat history through an AgentHandoff event recorded in the ChatContext.
The livekit/agents framework supports complex voice workflows by allowing seamless transfers between specialized agents within a single session. When one agent completes its task, it can delegate to another agent with different capabilities without dropping the conversation context or requiring the user to reconnect. This architecture enables modular AI systems where each agent handles specific domains while maintaining continuity.
How Multi-Agent Handoffs Work
The handoff mechanism operates through three coordinated steps within the AgentSession lifecycle. Understanding this flow is essential for implementing reliable agent transitions.
Step 1: Function Tool Returns an Agent
The handoff initiates when a decorated function tool returns an Agent subclass instance instead of a standard value. In livekit/agents/voice/agent_session.py, the _run_function_tool method inspects the return value and schedules a handoff when it detects an Agent type.
from livekit.agents import Agent, function_tool, RunContext
class IntroAgent(Agent):
@function_tool
async def information_gathered(self, ctx: RunContext, name: str, location: str):
"""Called when user supplies required information."""
# Returning an Agent instance triggers the handoff
return StoryAgent(name=name, location=location)
This pattern appears in the reference implementation at examples/voice_agents/multi_agent.py (lines 82-90), where the introductory agent gathers user data before transferring control to a specialized story-telling agent.
Step 2: AgentSession Creates the Handoff Record
When a handoff is detected, AgentSession._update_activity (lines 1180-1190 in livekit/agents/voice/agent_session.py) constructs an AgentHandoff object and inserts it into the session's ChatContext. The data model, defined in livekit/agents/llm/chat_context.py (lines 5-11), preserves transition metadata:
class AgentHandoff(BaseModel):
id: str = Field(default_factory=lambda: utils.shortuuid("item_"))
type: Literal["agent_handoff"] = "agent_handoff"
old_agent_id: str | None = None
new_agent_id: str
created_at: float = Field(default_factory=time.time)
This insertion ensures the transition appears in the conversation history, allowing downstream agents to understand when and why control transferred.
Step 3: RunResult Records the Event
The low-level RunResult class captures the handoff for runtime introspection and testing. In livekit/agents/voice/run_result.py (lines 35-45), the _agent_handoff method appends an AgentHandoffEvent to the run-time event list, making the transition observable through the test framework's assertion helpers.
Building a Multi-Agent Workflow
Implementing production handoffs requires configuring both the source agent that initiates the transfer and the target agent that receives control.
Source Agent Implementation
The source agent defines the handoff logic within its function tools. Use the chat_ctx parameter to preserve conversation history, or pass None to start fresh.
from livekit.agents.voice import AgentSession
class IntroAgent(Agent):
async def on_enter(self):
# Prompt user immediately upon activation
self.session.generate_reply()
@function_tool
async def gather_info(self, ctx: RunContext, name: str, location: str):
"""Transition to story agent when info is collected."""
story_agent = StoryAgent(
name=name,
location=location,
chat_ctx=self.chat_ctx # Preserve existing history
)
return story_agent # Triggers automatic handoff
Target Agent Configuration
The receiving agent inherits the ChatContext unless explicitly overridden, and can configure distinct STT, LLM, or TTS services.
class StoryAgent(Agent):
def __init__(self, name: str, location: str, *, chat_ctx=None):
super().__init__(
instructions=(
f"Tell a personalized story for {name} from {location}. "
"Maintain interactive dialogue."
),
chat_ctx=chat_ctx,
llm=openai.realtime.RealtimeModel(voice="echo"),
tts=None,
)
async def on_enter(self):
# Generate welcome message upon handoff completion
self.session.generate_reply()
Verifying Handoffs in Tests
The framework provides assertion helpers through RunResult.expect to validate handoff behavior in unit tests.
async def test_handoff_flow():
result = await run_session(
agent=IntroAgent(),
# STT, LLM, TTS configuration...
)
# Assert that a handoff to StoryAgent occurred
result.expect.next_event().is_agent_handoff(new_agent_type=StoryAgent)
# Verify the old agent ID was recorded
handoff_event = result.events[0]
assert handoff_event.old_agent_id == "IntroAgent_001"
Key Source Files and Architecture
Understanding the implementation requires familiarity with these core files:
-
livekit/agents/voice/agent_session.py(lines 1180-1190): Coordinates activity switches via_update_activity, createsAgentHandoffobjects, and manages the session'sChatContextduring transitions. -
livekit/agents/voice/run_result.py(lines 35-45): RecordsAgentHandoffEventinstances for runtime introspection and test verification through the_agent_handoffmethod. -
livekit/agents/llm/chat_context.py(lines 5-11): Defines theAgentHandoffPydantic model that structures handoff metadata within the chat log. -
examples/voice_agents/multi_agent.py(lines 82-90): Reference implementation demonstrating a complete handoff from an information-gathering agent to a specialized content agent.
Summary
- Handoffs are triggered when function tools return
Agentinstances, detected byAgentSession._run_function_tool. - History preservation occurs automatically through
ChatContextinheritance, with anAgentHandoffitem inserted to mark the transition. - Runtime observability is provided via
RunResultevents, enabling test assertions withis_agent_handoffhelpers. - Service isolation allows each agent to configure independent STT, LLM, and TTS providers while sharing conversational state.
Frequently Asked Questions
How is conversation history preserved during a handoff?
The incoming agent receives the previous agent's ChatContext by default when you pass it through the constructor. This context contains all prior user messages, function calls, and the AgentHandoff event itself. If you omit chat_ctx or pass None, the new agent starts with a blank slate.
Can I test multi-agent handoffs without connecting to LiveKit?
Yes. The run_session test helper and RunResult.expect API allow you to simulate complete voice sessions offline. Use result.expect.next_event().is_agent_handoff() to assert that your function tools correctly return agent instances and that the transition event propagates through the event system.
What happens if a function tool returns None instead of an Agent?
Returning None or any non-Agent value allows the current agent to retain control of the session. The handoff mechanism only activates when the return value is an instance of the Agent class, making transitions optional based on runtime logic.
Can the new agent use different AI models than the previous agent?
Absolutely. Each Agent instance configures its own llm, stt, and tts services during initialization. When the handoff occurs, the AgentSession tears down the old agent's services and initializes the new agent's preferred providers, enabling you to switch from high-latency reasoning models to realtime conversational models mid-session.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →