How AutoGen Agents Exchange Messages: 4 Flow Patterns
Swarnava Dutta8 min read
Autogen Multi Agent ExampleAutogen Agent MessagingAutogen Group Chat
Contents

I had two AutoGen agents that worked perfectly in isolation - an analyst and a coder - but the moment I wired them into a group chat, the coder kept answering its own questions while the analyst sat idle. The issue wasn't the LLM. It was speaker selection defaulting to the last speaker, with my handoff logic missing entirely. I spent most of a Saturday tracing message objects before the routing clicked.
Quick answer
AutoGen agents exchange messages through four patterns: direct request/response between two agents, typed message handlers with publish/subscribe topics, group chat with automatic speaker selection, and explicit handoffs naming the next agent. Direct chats use run(), while multi-agent flows rely on group chat with a speaker-selection policy or Handoff objects. Debugging failures requires inspecting message history on each agent.
How AutoGen Agents Exchange Messages: The 4 Core Flows
Every message follows the same lifecycle: an agent creates a typed object, the runtime routes it to a recipient or topic, the handler processes it and generates a response, and the framework appends both to history. The routing step is where the four patterns diverge.
| Criteria | Direct | Group Chat | Handoff | Broadcast |
|---|---|---|---|---|
| Recipient control | Explicit agent ref | Speaker-selection policy | Named in Handoff |
Topic string |
| Agent count | 2 | 3+ | 2+ | Any subscribers |
| Response expected | Yes | From selected speaker | From target | None guaranteed |
| Context ownership | Shared between pair | Team-level history | Transfers with control | Per-subscriber |
| Best use case | Simple Q&A loop | Multi-role collaboration | Pipeline stages | Fanout notifications |
One distinction trips people up constantly: AgentChat API vs. Core API. The current AgentChat layer (autogen_agentchat) gives you AssistantAgent, Handoff, and RoundRobinGroupChat. The Core layer (autogen_core) exposes typed message handlers and publish/subscribe topics. Legacy ConversableAgent from AutoGen 0.2 uses entirely different imports. Mixing them produces silent failures - agents register but never receive messages.
Agent Messages vs. Internal Events
Not everything in a streamed run is a message another agent receives. Chat messages (TextMessage, MultiModalMessage) route to other agents and are model-visible. Tool-call events and execution results stay internal unless explicitly forwarded. Handoff events carry a target field naming the next agent. Termination signals (StopMessage) end the loop without producing a reply.
Each message carries source, a recipient or topic, a concrete type for handler dispatch, and a content payload. The source field matters more than you'd expect - my group chat bug traced to the speaker selector reading source to decide turns, and my custom messages had the wrong value set.
Direct Messaging: A Two-Agent AutoGen Example
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import MaxMessageTermination
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main():
model = OpenAIChatCompletionClient(model="gpt-4o-mini")
analyst = AssistantAgent("analyst", model_client=model,
system_message="You analyze data and ask clarifying questions.")
result = await analyst.run(task="Write a pandas snippet to pivot sales by region")
for msg in result.messages:
print(f"{msg.source}: {msg.content}")
asyncio.run(main())
The run() method creates the initial TextMessage, the runtime delivers it, and the agent's handler produces a reply in result.messages. No group manager involved.
Version note: Legacy AutoGen 0.2 used ConversableAgent with initiate_chat(). The current AssistantAgent.run() replaces both. Swap initiate_chat for run(task=...) and move termination config to explicit TerminationCondition objects.
Request/Response and Multi-Turn Replies
Extending to follow-up turns means letting the runtime loop. Pass MaxMessageTermination(6) and each agent sees the full prior message list as context every turn.
This differs fundamentally from group chat routing. Sending a direct message guarantees the target responds. In a group chat, the manager might route your message to a different agent based on its selection policy. Bound your turns explicitly - open-ended loops burn tokens fast as each turn feeds the entire history back into the model.
Message Handlers, Typed Routing, and Broadcast Topics
The Core API routes messages by Python type, not string matching. You define a handler decorated with @message_handler, specifying the message type it accepts. The runtime dispatches to the matching handler deterministically - a CodeReviewRequest always hits the CodeReviewRequest handler, regardless of content.
from dataclasses import dataclass
from autogen_core import MessageContext, RoutedAgent, message_handler
@dataclass
class CodeReviewRequest:
code: str
language: str
correlation_id: str
@dataclass
class CodeReviewResult:
approved: bool
comments: str
class ReviewerAgent(RoutedAgent):
@message_handler
async def handle_review(self, message: CodeReviewRequest, ctx: MessageContext) -> CodeReviewResult:
return CodeReviewResult(approved=True, comments="Looks good")
For fanout - logging a transaction while also auditing it - publish/subscribe topics replace direct delivery. An agent publishes to a TopicId, and every subscriber receives a copy.
When two handlers accept the same base type, use distinct dataclass types per intent or add a correlation_id field. Relying on natural-language recipient names inside content breaks the moment someone edits a prompt. For handlers with side effects, stamp each message with a unique ID and check before executing - I've seen agents double-post Slack notifications because a handler assumed every delivery was unique.
AutoGen Group Chat: Speaker Selection and Agent Handoffs
When a task hits a SelectorGroupChat, the team manager wraps it in a TextMessage and picks the first speaker. Each reply appends to a shared list every participant sees. The manager then selects the next speaker - round-robin rotates mechanically, while selector-based choice prompts the model using agent names and descriptions.
Deterministic handoffs work better for pipelines. A Handoff(target="reviewer") tells the runtime exactly who goes next - no model decision involved. I had a "code_writer" and "code_reviewer" with near-identical descriptions, and the selector treated them as interchangeable until I made each description state what the agent should never do.

The most common failure: an agent addresses the group chat manager instead of the intended specialist. This happens when the model sees the manager's name in the participant list. Constrain allowed_speaker_transitions to exclude it.
Prevent Wrong Speakers, Endless Turns, and Handoff Loops
- Unique names and non-overlapping descriptions.
analyst_financialbeatsagent_2. State boundaries: "Only answers SQL questions. Never writes Python." - Constrained candidate sets. Pass
allowed_speaker_transitionsto limit which agents can follow which. For handoff loops (A hands to B, B immediately hands back), check the last two speakers before accepting. - Stacked termination conditions. Combine
MaxMessageTermination(20)withTextMentionTermination("TASK_COMPLETE"). Without these, a confused selector burns your token budget - I've watched a three-agent team generate 80+ turns before I killed the process.
AutoGen Message History, Shared Context, and Agent Memory
AutoGen teams maintain a shared message list that every participant reads during a run. It's transient - dies when run() returns. Per-agent model context is separate: each AssistantAgent builds its own prompt from team history plus its system message. Application state lives entirely outside both.
Nothing persists automatically between runs. Extract structured facts and store them yourself - database, cache, or vector store. Inject only the relevant slice back into the next run's task or system message.

Context-window growth drives up costs faster than most teams expect. For long sessions, summarize earlier turns into a compact state object and reset the message list. Isolate sessions by keying state on (user_id, session_id) to prevent agents leaking context across users.
Choose Memory Scope and Ownership
Five scopes cover most designs: session (one run() call), user (preferences persisted across sessions), agent (tool credentials scoped to one instance), team (shared task state in a group chat), and application (global config and rate limits). Assign one authoritative owner for any mutable shared state. I hit this with a planner and executor both writing to the same task-status key - the result matched neither agent's intent. Designate one writer; others read only. Store durable facts as structured records ({"region": "EMEA", "q2_revenue": 4200000}), not raw transcript fragments that drift when summarized.
Trace How AutoGen Agents Exchange Messages at Runtime
Instrument every message with timestamp, source, destination, message class name, turn number, and correlation_id. Without this, debugging a multi-agent system is guessing.
Start by verifying your API version - confirm you're importing from autogen_agentchat or autogen_core, not the legacy autogen package. Mixed imports produce agents that silently never receive messages. Next, log the raw message object before and after handler dispatch; if a message appears in logs but never reaches model context, the handler returned before appending it. Finally, check speaker selection by logging the candidate list and chosen speaker each turn - wrong-speaker bugs hide here.
Silent stalls almost always trace to blocking synchronous work inside an async handler (the event loop freezes) or a swallowed exception where a broad try/except returns None instead of a reply. The runtime interprets the missing reply as "conversation over." For repeatable tests, replace live model clients with deterministic test doubles that return fixed responses, and assert on message order, recipient, and payload type rather than generated text.
How to Deploy AutoGen Agents Without Losing Message State
Moving from asyncio.run(main()) to production means splitting four concerns: agent definitions (version-controlled config), model clients (keys from environment variables), runtime execution (API handler or background worker), and persistent state (database or cache behind its own connections).
After a process restart, restore session state by loading structured records keyed on (session_id, correlation_id) - not by replaying the prior transcript. Replaying re-triggers tool calls, double-posts notifications, and burns tokens reconstructing what you already know.
For production hardening, isolate one runtime instance per session to avoid shared-state overwrites. Wrap model client calls with timeouts and retry on transient 429/503 errors with exponential backoff. Thread a correlation_id from the inbound API request through every message, log line, and database write - without it, correlating a user complaint to a specific agent turn across workers is nearly impossible. Emit structured JSON logs with source, destination, and message type to whatever aggregator your team runs.
The simplest production path: a FastAPI endpoint that accepts a task, spins up agents, calls run(), streams results via SSE, and writes final state to Postgres. When volume grows, move execution behind a task queue so the API returns immediately. The agent definitions stay identical - only the execution wrapper changes.
FAQ
How do AutoGen agents exchange messages?
Through typed Python objects routed by the runtime across four patterns: direct request/response via run(), typed message handlers dispatched by class, group chat with speaker selection, and explicit Handoff objects naming the next agent. Each message carries source, recipient, type, and content that handlers dispatch on.
What is AutoGen?
An open-source multi-agent framework from Microsoft. It provides AgentChat for high-level team orchestration with assistants, group chats, and handoffs, and Core for typed message routing with publish/subscribe topics. Agents coordinate through structured message passing rather than shared memory.
How to handle agent memory?
Store structured facts from conversations in a database or vector store - not raw transcripts. AutoGen's shared message list resets after each run() call. Inject only relevant prior state into the next run's task or system message. Key records on (user_id, session_id) to prevent context leaking across users.
How to deploy AutoGen agents?
Wrap agent execution behind an API endpoint that accepts a task, instantiates agents, calls run(), and writes structured results to persistent storage. Pull API keys from environment variables, isolate one runtime per session, and thread a correlation_id through every message and log for end-to-end tracing.


