Chapter 7

Handoff and group chat orchestration

Choose handoff and group collaboration patterns when one agent is not enough.

Objective

After this chapter, you will be able to choose handoff and group-collaboration patterns when one agent is not enough, then build those patterns with the Python Microsoft Agent Framework orchestration APIs.

Concept/Theory

Chapter 2 introduced two multi-agent patterns that sit between fixed workflows and fully open-ended planning: handoff and group chat. Both use more than one agent, but they solve different coordination problems.

Handoff is routing by ownership transfer. One active agent works on the task until context shows that another specialist should own the next turn. Microsoft Learn describes handoff orchestration as agents transferring control based on context or user request, using a mesh topology rather than a central coordinator.Microsoft Learn

Group chat is collaboration through a shared conversation. Multiple agents see the same thread, and an orchestrator decides who should speak next and when the conversation should stop. Microsoft Learn frames group chat as a collaborative conversation pattern for iterative refinement, review, and debate.Microsoft Learn

The design question is therefore not "how many agents?" but "who controls the next step?" Choose handoff when one specialist should own the task at a time. Choose group chat when the value comes from multiple perspectives reacting to one another.

In MAF

This chapter targets agent-framework==1.9.0 and agent-framework-orchestrations==1.9.0. Import the orchestration builders from agent_framework.orchestrations; that public path re-exports the installed orchestration package.

HandoffBuilder

HandoffBuilder builds a handoff workflow from real Agent instances. You register participants, declare allowed source-to-target handoffs with add_handoff(...), optionally set the start agent, and then call build(). During execution, the framework injects handoff tools so the active agent can transfer control to an allowed target.

A critical Python requirement in 1.9.0: handoff participants must be actual Agent instances constructed with require_per_service_call_history_persistence=True. Do not pass arbitrary run-capable objects to handoff, even though other orchestration builders may accept them.

GroupChatBuilder

GroupChatBuilder builds a star-shaped group conversation around an orchestrator. For deterministic turns, pass a selection_func that receives GroupChatState and returns the next participant name. For model-selected turns, use an orchestrator agent or custom orchestrator. There is no Python RoundRobinGroupChatManager in 1.9.0; round-robin behavior is implemented as a selection function.

Key Python orchestration builders and parameters for Chapter 7
API What it implements Key parameters / methods Use it when...
HandoffBuilder Specialist routing and ownership transfer participants, add_handoff(source, targets, description=...), with_start_agent(...), termination_condition The active agent should transfer the next turn to a better-suited specialist.
GroupChatBuilder Shared multi-agent conversation participants, selection_func, orchestrator_agent, orchestrator, max_rounds, termination_condition Agents should collaborate, review, debate, or refine in one thread.
GroupChatState Turn-taking state for group chat current_round, participants, conversation A deterministic selection_func needs to choose the next speaker.
GroupChatOrchestrator Function-driven group coordinator selection_func, max_rounds, termination_condition You need explicit speaker-selection policy beyond the builder defaults.
AgentBasedGroupChatOrchestrator Agent-driven group coordinator agent, retry_attempts, session, max_rounds A coordinator agent should decide the next speaker or whether to stop.
BaseGroupChatOrchestrator Custom group-chat orchestration base participant_registry, max_rounds, termination_condition You need specialized orchestration behavior not covered by the builder.

When to use / pitfalls

Choosing between orchestration shapes
Pattern Use when Avoid when
Handoff The right specialist emerges during the conversation, and only one agent should own the task at a time. Routing is deterministic at the start, or the task needs parallel work or shared debate.
Group chat Agents should react to each other in a shared thread for brainstorming, maker-checker review, or consensus. A simple transfer or fixed pipeline would finish faster and be easier to test.
Fixed workflow The process order is known: validate, enrich, draft, review, approve, or similar staged work. The next specialist genuinely depends on context discovered during execution.

Common pitfalls

  • Unbounded rounds. Always set max_rounds or a clear termination_condition for group chat and handoff. Multi-agent systems can loop when agents continue refining, re-routing, or disagreeing.
  • Missing handoff persistence. In Python 1.9.0, handoff requires real Agent instances with require_per_service_call_history_persistence=True. Without that flag, the workflow cannot safely preserve per-service-call history across the injected handoff tools.
  • Inventing a round-robin manager. There is no Python RoundRobinGroupChatManager in the verified 1.9.0 API. Implement predictable turn order with selection_func and GroupChatState.
  • Using collaboration for control flow. If the process is known, Chapter 6's sequential or concurrent workflow patterns are easier to reason about, test, and observe.

Worked example

The example below is the verified file at backend/examples/ch07/handoff_group_chat.py. It uses fake chat clients so it runs without service credentials. In a live application, the agent-backed steps would use a real model client and require credentials such as OPENAI_API_KEY.

Runnable handoff and group-chat orchestration example for Python agent-framework==1.9.0.
"""Chapter 7: handoff and group chat orchestration without live LLM credentials."""

import asyncio
from collections.abc import Sequence
from typing import Any

from agent_framework import Agent, ChatResponse, Message
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState, HandoffBuilder


class FakeChatClient:
    def __init__(self, prefix: str) -> None:
        self.prefix = prefix

    async def get_response(self, messages: Sequence[Message], **_: Any) -> ChatResponse[str]:
        last = messages[-1].text if messages else ""
        return ChatResponse(messages=Message("assistant", [f"{self.prefix}: {last}"]))


def fake_agent(name: str, description: str) -> Agent:
    return Agent(
        FakeChatClient(name),
        name=name,
        description=description,
        instructions=f"You are {name}.",
        require_per_service_call_history_persistence=True,
    )


async def handoff_demo() -> None:
    triage = fake_agent("triage", "Routes requests to specialists")
    billing = fake_agent("billing", "Handles billing requests")
    workflow = (
        HandoffBuilder(
            participants=[triage, billing],
            termination_condition=lambda messages: sum(1 for m in messages if m.role == "assistant") >= 1,
        )
        .add_handoff(triage, [billing], description="Billing issue")
        .add_handoff(billing, [triage], description="Return to triage")
        .with_start_agent(triage)
        .build()
    )
    result = await workflow.run("I need help with an invoice")
    print("handoff_outputs:", [output.text for output in result.get_outputs()])


async def group_chat_demo() -> None:
    writer = fake_agent("writer", "Drafts copy")
    reviewer = fake_agent("reviewer", "Reviews copy")

    def select_next(state: GroupChatState) -> str:
        names = list(state.participants.keys())
        return names[state.current_round % len(names)]

    workflow = GroupChatBuilder(participants=[writer, reviewer], selection_func=select_next, max_rounds=2).build()
    result = await workflow.run("Create a short product slogan")
    print("group_chat_outputs:", [getattr(output, "text", output) for output in result.get_outputs()])


async def main() -> None:
    await handoff_demo()
    await group_chat_demo()


if __name__ == "__main__":
    asyncio.run(main())

The handoff portion creates two specialists, triage and billing, registers both as participants, and declares allowed transfer paths in both directions. The start agent is triage, and the simple termination condition stops the no-credential demo after one assistant response.

The group-chat portion creates writer and reviewer participants. Its select_next function reads GroupChatState.participants and GroupChatState.current_round to alternate speakers for two rounds. That is the Python 1.9.0 way to express round-robin turn-taking.

Recap & next

  • Use HandoffBuilder when control should transfer from one specialist agent to another.
  • Use GroupChatBuilder when several agents should collaborate in one shared conversation.
  • For deterministic group-chat turns, pass a selection_func over GroupChatState; do not look for a Python round-robin manager.
  • Cap autonomous collaboration with max_rounds or a termination condition.

Next, Chapter 8: Streaming, checkpointing, and human-in-the-loop shows how to make longer-running orchestration visible, resumable, and controllable by people.