# How to Implement Multi-Agent Handoffs in LiveKit Agents

> Implement seamless multi agent handoffs in LiveKit Agents. Learn how function tools trigger transitions and preserve chat history with AgentHandoff events for smooth agent-to-agent communication.

- Repository: [LiveKit/agents](https://github.com/livekit/agents)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/livekit/agents/blob/main/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.

```python
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`](https://github.com/livekit/agents/blob/main/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`](https://github.com/livekit/agents/blob/main/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`](https://github.com/livekit/agents/blob/main/livekit/agents/llm/chat_context.py) (lines 5-11), preserves transition metadata:

```python
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`](https://github.com/livekit/agents/blob/main/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.

```python
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.

```python
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.

```python
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`](https://github.com/livekit/agents/blob/main/livekit/agents/voice/agent_session.py)** (lines 1180-1190): Coordinates activity switches via `_update_activity`, creates `AgentHandoff` objects, and manages the session's `ChatContext` during transitions.

- **[`livekit/agents/voice/run_result.py`](https://github.com/livekit/agents/blob/main/livekit/agents/voice/run_result.py)** (lines 35-45): Records `AgentHandoffEvent` instances for runtime introspection and test verification through the `_agent_handoff` method.

- **[`livekit/agents/llm/chat_context.py`](https://github.com/livekit/agents/blob/main/livekit/agents/llm/chat_context.py)** (lines 5-11): Defines the `AgentHandoff` Pydantic model that structures handoff metadata within the chat log.

- **[`examples/voice_agents/multi_agent.py`](https://github.com/livekit/agents/blob/main/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 `Agent` instances, detected by `AgentSession._run_function_tool`.
- **History preservation** occurs automatically through `ChatContext` inheritance, with an `AgentHandoff` item inserted to mark the transition.
- **Runtime observability** is provided via `RunResult` events, enabling test assertions with `is_agent_handoff` helpers.
- **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.