Handling Handoffs Between Agents in Swarm Group Chat: A Complete Guide

In AutoGen's Swarm group chat, agents transfer control exclusively through HandoffMessage objects, with the SwarmGroupChatManager selecting the next speaker by scanning the message thread backwards for the most recent handoff target.

The Swarm team implementation in the microsoft/autogen repository provides a lightweight, deterministic approach to handling handoffs between agents in Swarm group chat. Unlike other group chat patterns that rely on LLM-based speaker selection, Swarm routes conversations exclusively via explicit HandoffMessage declarations, making it ideal for deterministic workflows and human-in-the-loop scenarios.

Understanding the Swarm Handoff Architecture

Core Components for Agent Handoffs

The Handoff Lifecycle in Swarm Group Chat

  1. Initialization: The first agent in the participants list becomes the initial speaker. The Swarm constructor asserts this agent can emit HandoffMessage objects.

  2. Handoff Emission: An agent emits a HandoffMessage with target=<next_agent_name>. The manager validates the target exists in validate_group_state (lines 47-73).

  3. Speaker Selection: The manager scans the message thread backwards to find the latest HandoffMessage and sets _current_speaker accordingly in select_speaker (lines 82-98). If no handoff is present, the current speaker continues.

  4. State Persistence: save_state (lines 100-107) serializes the thread and speaker, while load_state (lines 108-113) restores execution context.

Implementing Agent Handoffs in Swarm Group Chat

Basic Automatic Handoffs Between Two Agents

The following example demonstrates a two-agent Swarm where Alice automatically hands off to Bob:

import asyncio
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import Swarm
from autogen_agentchat.conditions import MaxMessageTermination

async def main() -> None:
    client = OpenAIChatCompletionClient(model="gpt-4o")

    alice = AssistantAgent(
        "Alice",
        model_client=client,
        handoffs=["Bob"],                     # Alice must be able to hand off to Bob

        system_message="You are Alice; answer only about yourself."
    )
    bob = AssistantAgent(
        "Bob",
        model_client=client,
        system_message="You are Bob; your birthday is 1 Jan."
    )

    termination = MaxMessageTermination(3)    # stop after three messages

    swarm = Swarm([alice, bob], termination_condition=termination)

    async for msg in swarm.run_stream(task="When is Bob's birthday?"):
        print(msg)

asyncio.run(main())

Key implementation details:

  • Swarm([alice, bob]) creates a hand-off-driven team.
  • The first agent (alice) must have handoffs configured so it can emit a HandoffMessage.
  • The conversation ends when MaxMessageTermination fires.

Human-in-the-Loop Handoffs with HandoffTermination

For scenarios requiring human intervention, use HandoffTermination to pause execution when control transfers to a user:

import asyncio
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import Swarm
from autogen_agentchat.conditions import HandoffTermination, MaxMessageTermination
from autogen_agentchat.ui import Console
from autogen_agentchat.messages import HandoffMessage

async def main() -> None:
    client = OpenAIChatCompletionClient(model="gpt-4o")

    alice = AssistantAgent(
        "Alice",
        model_client=client,
        handoffs=["user"],                     # declares a human handoff target

        system_message="You are Alice; ask the user for help when needed."
    )
    termination = HandoffTermination(target="user") | MaxMessageTermination(3)
    swarm = Swarm([alice], termination_condition=termination)

    # 1️⃣ Alice asks the user a clarification question

    await Console(swarm.run_stream(task="Explain the concept of recursion."))

    # 2️⃣ Human replies via a hand‑off message

    await Console(
        swarm.run_stream(
            task=HandoffMessage(source="user", target="Alice",
                                 content="Recursion is a function calling itself.")
        )
    )

asyncio.run(main())

Key points:

  • HandoffTermination(target="user") stops the AI loop when a HandoffMessage targeting "user" appears.
  • The Console UI streams messages; the second call resumes the same Swarm instance with a handcrafted HandoffMessage.

Persisting and Restoring Swarm State

Swarm supports full serialization of conversation state, enabling pause-and-resume workflows:


# Assume `swarm` is a running Swarm instance

state = await swarm.save_state()          # serialises the whole chat manager

# …persist `state` to a DB or file…

# Later, recreate the same agents and load the saved state

new_swarm = Swarm([alice, bob], termination_condition=termination)
await new_swarm.load_state(state)         # resumes exactly where we left off

The saved JSON contains the message thread, the current turn index, and the name of the agent that will speak next (_current_speaker).

Core Source Files and Implementation Details

File Purpose Direct link
python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_swarm_group_chat.py Swarm class, manager implementation, speaker selection, validation, state persistence Swarm implementation
python/packages/autogen-agentchat/src/autogen_agentchat/messages.py Definition of HandoffMessage (target, optional context) HandoffMessage definition
python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.py SwarmManagerState model used for checkpointing SwarmManagerState
python/packages/autogen-agentchat/tests/test_group_chat.py Unit tests that illustrate hand‑off flows, state round‑tripping and optional team‑event emission Swarm hand‑off tests
python/packages/autogen-agentchat/src/autogen_agentchat/teams/__init__.py Exposes Swarm in the public package namespace Team package init

These files together form the complete implementation of how Swarm orchestrates hand‑offs between agents, validates targets, and preserves conversational state.

Summary

  • Swarm routes conversations exclusively via HandoffMessage objects, not LLM-based selection.
  • The SwarmGroupChatManager selects the next speaker by scanning backwards for the most recent handoff target in select_speaker.
  • Agents must declare their handoff capabilities via the handoffs parameter in AssistantAgent.
  • State persistence is handled through SwarmManagerState, enabling conversation checkpointing and resumption via save_state and load_state.
  • Human-in-the-loop workflows are supported via HandoffTermination targeting a user agent.

Frequently Asked Questions

How does Swarm differ from other AutoGen group chat implementations?

Unlike RoundRobinGroupChat or SelectorGroupChat, Swarm does not use an LLM to choose the next speaker. Instead, it relies exclusively on explicit HandoffMessage declarations from agents, making it deterministic and ideal for structured workflows where predictable routing is required.

Can an agent hand off to multiple targets in a single turn?

An agent emits a single HandoffMessage with one target field. However, the agent can be configured with multiple potential handoff targets in its handoffs list, and conditional logic within the agent can determine which specific target to specify for each turn based on conversation context.

What happens if an agent emits a HandoffMessage targeting an unregistered agent?

The SwarmGroupChatManager validates all handoff targets in validate_group_state. If a HandoffMessage targets an agent not present in the participants list, the validation will fail and raise an appropriate error before the next speaker selection occurs, preventing invalid routing.

How do I resume a Swarm conversation after a system restart?

Use await swarm.save_state() to serialize the current state (including the message thread and current speaker) to a JSON-compatible dictionary. Persist this state to a database or file. After recreating the agent instances, instantiate a new Swarm and call await new_swarm.load_state(state) to resume execution from the exact point of interruption.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →