Part I: The mental model
a task, in plain English
|
v
AgentChat (high-level) AssistantAgent, UserProxyAgent, teams
| RoundRobinGroupChat / SelectorGroupChat / Swarm
v
a group chat manager records a shared message thread, picks the next speaker
|
v
each turn: one agent runs build model context -> call the LLM -> maybe call tools
| -> produce a Response message
v
broadcast the response every participant's context is updated
|
v
termination check TextMention / MaxMessage / Handoff / token budget
|
v
TaskResult the full message list plus a stop_reason
- - - - - - - - - - - - all of the above rides on: - - - - - - - - - - - -
autogen-core (low-level) agents are actors addressed by AgentId(type, key)
| an AgentRuntime delivers messages
| direct send_message (RPC-style, returns a value)
| publish_message (broadcast to a TopicId)
v
SingleThreadedAgentRuntime in one process
GrpcWorker + gRPC host across processes and languages (Python and .NET)
The one-sentence identity. AutoGen treats a message as the universal interface and an agent as an actor, so a task is solved not by one model call but by a conversation among specialized agents whose messages, tool calls, and human interventions are all the same kind of event flowing through a runtime. A single agent is a function from a prompt to a completion. It cannot run the code it writes, cannot check its own work with fresh eyes, and cannot ask a person for a decision without you wiring that up by hand. AutoGen's answer is to make each of those capabilities a separate agent and let them talk. A coder proposes, an executor runs and reports the traceback, the coder repairs, a reviewer objects, a human approves. The loop grounds the language model in real results instead of leaving it to hallucinate that its code worked.
Two layers make this concrete, and it is worth separating them from the first minute. AgentChat is the high-level, task-driven API most people use, with pre-built agents and teams that look a lot like the classic AutoGen you may have read about. autogen-core is the low-level, event-driven foundation underneath, an actor system where agents are addressed by identity, the runtime routes messages, and communication happens either as direct requests or as broadcasts over named topics. AgentChat is implemented on top of autogen-core. A third package, autogen-ext, holds the concrete integrations, model clients for OpenAI and Azure and others, code executors, tool adapters, and the distributed runtime. Everything in this chapter is checked against the v0.4 line on the main branch as of mid-2026. The project moves quickly and internal names shift, so where an exact class or path is likely to have moved I describe the component by its role and say so.
A note on history, because you will trip over it otherwise.
AutoGen v0.2 was the original
conversation-first library, and its
ConversableAgent abstraction is still the clearest
statement of the whole idea. In late 2024 the original creators
forked the v0.2 codebase into a separate project, AG2, which
inherited the autogen and pyautogen names
on PyPI. Microsoft's official continuation is the v0.4 rewrite in
this repository, published under the distinct package names
autogen-agentchat, autogen-core, and
autogen-ext. Install those exact names and you are
using Microsoft's AutoGen. This chapter teaches v0.4 as the
primary API and uses v0.2 only to explain the conversable-agent
model at its source.
Part II: Using it
AutoGen is a Python package (with a parallel .NET implementation in the same repository). It runs anywhere Python does, including macOS, because the heavy lifting is done by whatever model endpoint you point it at, not on your machine. Install the high-level API together with the OpenAI model client from extensions:
python -m venv .venv && source .venv/bin/activate
pip install -U "autogen-agentchat" "autogen-ext[openai]"
# optional low-code GUI for prototyping teams visually
pip install -U autogenstudio
autogenstudio ui --port 8080
The model client is provider-agnostic in shape. Point it at
OpenAI, at Azure OpenAI, or at any OpenAI-compatible server you
host yourself, for example a local vLLM or
SGLang endpoint, or a small model served
by Ollama, by setting the
base_url and model accordingly. Set your
key in the environment, then write the smallest possible program,
a single agent that answers a task. Everything in v0.4 is async,
so the shape is asyncio.run around an
await agent.run(...):
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main() -> None:
model_client = OpenAIChatCompletionClient(model="gpt-4o")
agent = AssistantAgent("assistant", model_client=model_client)
result = await agent.run(task="Name three prime numbers and stop.")
print(result.messages[-1].content)
await model_client.close()
asyncio.run(main())
result is a TaskResult. Its
messages field is the full ordered transcript of the
run, and the last message is the agent's final answer. That single
agent is already useful, but the point of AutoGen is more than one
of them. The smallest interesting system is a two-agent team that
takes turns until one of them says a stop word. Read this next
block as the canonical AutoGen shape, since almost everything
larger is a variation on it:
import asyncio
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
async def main() -> None:
model_client = OpenAIChatCompletionClient(model="gpt-4o")
writer = AssistantAgent(
"writer",
model_client=model_client,
system_message="You draft short poems. Revise when the critic asks.",
)
critic = AssistantAgent(
"critic",
model_client=model_client,
system_message=(
"You critique the poem. When it is good enough, "
"reply with the single word APPROVE."
),
)
termination = TextMentionTermination("APPROVE")
team = RoundRobinGroupChat([writer, critic], termination_condition=termination)
await Console(team.run_stream(task="Write a two-line poem about the sea."))
await model_client.close()
asyncio.run(main())
run_stream returns an async iterator of messages as
they are produced, and Console pretty-prints that
stream to your terminal. You will watch the writer draft, the
critic object, the writer revise, and the critic finally emit
APPROVE, at which point TextMentionTermination
fires and the run ends. If you would rather have the finished
transcript than a live stream, call await team.run(task=...)
and inspect the returned TaskResult yourself. This is
the whole ergonomic bargain, you describe roles with system
prompts, choose a team shape, and choose when to stop.
Now tools, because an agent that can only talk is limited. In v0.4
you give an AssistantAgent ordinary Python functions
and it exposes them to the model as callable tools, inferring the
JSON schema from the type hints and the docstring:
async def get_weather(city: str) -> str:
"""Return a short weather report for a city."""
return f"The weather in {city} is 21C and clear."
agent = AssistantAgent(
"weather_agent",
model_client=model_client,
tools=[get_weather],
reflect_on_tool_use=True, # after tools run, let the model summarize in prose
)
result = await agent.run(task="What is the weather in Nairobi?")
When the model decides to call get_weather, the agent
executes the function, feeds the result back, and by default can
loop if the model wants to call more tools before answering. With
reflect_on_tool_use=True it makes one more model call
to turn the raw tool output into a natural-language reply. Behind
the scenes the agent emits explicit events for each step, a tool
call request and a tool call execution result, which show up in
the transcript and are what you inspect when a tool misbehaves.
The mistakes beginners make cluster in a few places. First,
forgetting a termination condition. A team with no
stop rule will happily run its agents in circles, burning tokens
and money, until it hits a default message cap or you kill it.
Every team needs a real condition, and conditions compose with
| (or) and & (and), so a common safe
default is a task-specific stop word or a hard ceiling on message
count:
from autogen_agentchat.conditions import TextMentionTermination, MaxMessageTermination
termination = TextMentionTermination("APPROVE") | MaxMessageTermination(20)
Second, forgetting that everything is async.
Calling agent.run(...) without await
gives you a coroutine object, not a result, and nothing happens.
Third, leaking the model client. Close it with
await model_client.close() or use it as an async
context manager, or you leak the underlying HTTP connections.
Fourth, and most conceptual, reaching for a team when one
agent would do. Multi-agent conversation earns its keep
when roles genuinely differ or a feedback loop is needed. For a
single well-scoped tool-using task, one AssistantAgent
is cheaper, faster, and easier to debug.
Part III: When it is the right tool
AutoGen is the right tool when your problem is genuinely multi-agent or event-driven, when you want more than one role collaborating, or when you want a runtime that can host agents across processes and even across languages. It shines for research on agent design patterns, for systems where a human is a first-class participant in the loop, and for teams that want the actor-model foundation to grow into a distributed deployment later. The AgentChat layer gets you a working team in a dozen lines, and the core layer is there when you outgrow it. It is also the home of Magentic-One, Microsoft's generalist multi-agent system, whose orchestrator plus web, file, coding, and terminal agents ships as a team you can instantiate directly.
The honest cases for alternatives. LangGraph
models an agent application as an explicit state graph you author
node by node, with edges you draw, which gives you tighter control
over control flow than AutoGen's conversation-and-broadcast model
and suits workflows you want to pin down exactly.
CrewAI is higher-level and more opinionated around
roles and tasks in a "crew", which is pleasant for straightforward
role pipelines but gives you less of a runtime underneath.
OpenAI's Agents SDK (the successor to its Swarm
experiment) is a lightweight handoff-based library, and AutoGen's
own Swarm team mirrors that same handoff pattern if
that is the shape you want. AG2, the community
fork, continues the v0.2 conversable-agent API, so if you liked
that surface and do not want the rewrite it is a reasonable home.
Semantic Kernel is Microsoft's other agent SDK,
plugin-oriented and enterprise-focused with deep .NET heritage.
The largest strategic caveat is about that last one. Microsoft has converged AutoGen and Semantic Kernel into a single production line called the Microsoft Agent Framework, previewed in late 2025 and reaching a 1.0 in 2026, which is meant to carry AutoGen's dynamic multi-agent orchestration on top of Semantic Kernel's production foundations. AutoGen itself remains valuable as the research frontier and as the clearest teaching codebase for these ideas, and its abstractions flow directly into the unified framework, but if you are starting a brand-new production system in 2026 you should check the current status of that convergence before committing. Treat this as a moving target and verify against the repository and Microsoft's own guidance, since the details here are exactly the kind that shift.
The architecture-shaped warning, the one that bites people in production, is about cost and non-determinism. Every turn in a conversation is at least one LLM call, teams multiply the turns, and tool-calling loops multiply them again. A five-agent group chat that runs twenty rounds is a hundred model calls per task, and if the selector is an LLM too, more. This is the analogue of an accidental fan-out in a distributed system:
runaway: no termination + LLM speaker selection + tool loops
task -> agentA -> agentB -> agentA -> agentB -> ... forever
each arrow = one or more model calls, cost climbs without bound
controlled: hard MaxMessageTermination + a stop word + a tool-iteration cap
task -> ... -> APPROVE (or message #N) -> stop
worst-case cost is bounded before you press run
The design gives you the tools to bound it, termination conditions, token-budget conditions, and per-agent tool-iteration limits, but nothing forces you to use them, and the failure mode is a large bill and a wandering transcript rather than a crash. Bound the conversation the way you would bound a retry loop, from the start.
Part IV: The full life of one team.run(task)
The specimen is the two-agent writer-and-critic team from Part II,
run with RoundRobinGroupChat and a
TextMentionTermination("APPROVE"). I follow the task
from the high-level call all the way down into autogen-core's
message passing and back up to the TaskResult.
Internal type names below are the ones on the v0.4 main branch and
are the most likely things to have been renamed, so hold them
loosely and trust the roles.
Stage 1: constructing the team
RoundRobinGroupChat([writer, critic], termination_condition=...)
builds a BaseGroupChat subclass. Construction is cheap
and does not start anything. It records the list of participant
agents, the termination condition, and the round-robin speaker
policy, and it prepares (but does not yet run) a
SingleThreadedAgentRuntime, the in-process runtime from
autogen-core that will actually deliver messages. Nothing has
talked to a model yet.
Stage 2: run_stream wires up the runtime
The first real work happens inside run or
run_stream. The team registers two kinds of actor with
the runtime. Each participant agent is wrapped in a container agent
(a ChatAgentContainer in the current code) that adapts
the AgentChat agent interface to the core actor interface. A
separate GroupChatManager actor is registered to run
the conversation. All of these subscribe to a shared group topic,
so a message published to that topic reaches every participant, and
each participant also has its own topic so the manager can address
it individually. This subscribe-to-a-shared-topic step is exactly
how "everyone hears everyone" is implemented, and it is pure
autogen-core pub/sub.
Stage 3: the task enters as a message
The team publishes a start message carrying the task text to the group topic and starts the runtime. The manager receives it and appends the task to the shared message thread it maintains, the single source of truth for the conversation. From here the loop is entirely message-driven. There is no central for-loop calling agents, only actors reacting to messages the runtime delivers.
Stage 4: the manager selects a speaker
The manager decides who speaks next. For
RoundRobinGroupChat that is simply the next agent in
the list, the writer on the first turn. For
SelectorGroupChat it would instead prompt an LLM with
the transcript and the agent descriptions to pick the most relevant
next speaker. The manager then publishes a request-to-speak message
addressed to the chosen participant's topic. This is the point
where the round-robin policy and the selector policy differ, and
everything else in the trace is identical between them.
Stage 5: one agent takes its turn
The writer's container receives the request. It hands the relevant
slice of the shared thread to the wrapped
AssistantAgent, whose on_messages logic
runs the real work. The agent adds the incoming messages to its
model context, calls the model client (one network round trip to
the LLM), and if the model returned tool calls it executes them and
may loop, otherwise it produces a text response. That response is
packaged as a message and published back to the group topic. The
LLM call in this stage is where the latency and the token cost of
the turn actually live.
Stage 6: broadcast, then the termination check
Because the response goes to the group topic, both the manager and
the critic's container receive it, so the critic's context is
updated even though it has not spoken yet. This broadcast is why a
group chat behaves like a shared conversation rather than a set of
private channels. The manager appends the response to the thread
and evaluates the termination condition against the updated
message list. TextMentionTermination("APPROVE") scans
the latest message for the token APPROVE. On the
writer's first turn it is absent, so the manager loops back to
Stage 4 and selects the critic. The critic objects, the writer
revises, and eventually the critic replies with
APPROVE.
Stage 7: stop, drain, and the TaskResult
When the condition matches, the manager publishes a stop signal and
the runtime is told to stop once its message queue drains. The team
collects the ordered transcript it accumulated and returns a
TaskResult whose messages is the full
conversation and whose stop_reason explains why it
ended, here a text mention of APPROVE. With
run_stream the same messages were yielded to you (and
to Console) as each was produced, and the
TaskResult is the final item in the stream. Critically,
the termination condition is stateful across the run and is reset
before the next run, and the team retains the thread,
so calling run again with a new task continues the
same conversation rather than starting fresh. That closes the loop,
a task in as English, a bounded multi-agent conversation in the
middle, a structured result out.
Part V: Internals deep dives
Deep dive: the conversable-agent model
The oldest and most load-bearing idea in AutoGen is that every
agent is conversable. In v0.2 this is the
ConversableAgent base class, and its interface is only
three verbs, send a message to another agent,
receive a message, and generate_reply to
produce a response to the messages seen so far. Two subclasses do
almost all the work in practice. AssistantAgent is
backed by an LLM and answers in language and code.
UserProxyAgent stands in for the human, executes code,
and optionally solicits real human input. The canonical v0.2
program is two agents in a loop, an assistant that writes code and
a user proxy that runs it and reports back:
# Classic AutoGen v0.2 shape (the conversable-agent model at its source).
# Note: the PyPI names `autogen` / `pyautogen` now point to the AG2 fork;
# Microsoft's current package is `autogen-agentchat` (v0.4), shown earlier.
from autogen import AssistantAgent, UserProxyAgent
assistant = AssistantAgent("assistant", llm_config={"model": "gpt-4"})
user_proxy = UserProxyAgent(
"user_proxy",
human_input_mode="NEVER",
code_execution_config={"work_dir": "coding", "use_docker": True},
)
user_proxy.initiate_chat(
assistant,
message="Plot the last year of NVDA stock and save it to chart.png.",
)
What makes this more than a chatbot is how generate_reply
works. A conversable agent holds an ordered list of registered
reply functions and tries them in turn, returning the first one
that produces a reply. The default order is meaningful, first check
whether the conversation should terminate or the human should be
asked, then check for a tool or function call to execute, then check
for code to run, and only then call the LLM. Because reply
generation is a pipeline of pluggable handlers rather than a single
model call, the same object can be an executor on one turn, a human
proxy on another, and a language model on a third, which is what
lets a plain conversation carry code execution, tool use, and human
approval without any of those being special cases. The
assistant writes a Python block, the user proxy's code-execution
reply runs it in a working directory or a Docker container and
sends stdout or the traceback back, and the assistant's next
LLM reply fixes the bug. A single agent cannot do this, it has no
execution environment and no second pair of eyes. The multi-agent
loop supplies both, and the grounding it provides, real output fed
back into the next prompt, is the entire reason the pattern beats a
lone model.
v0.4 keeps this model but reshapes the surface. The AgentChat
AssistantAgent is async, exposes the tool loop and the
reflection step as explicit options, and emits typed events for
each internal step instead of hiding them inside
generate_reply. The mental model is unchanged, an agent
is still something that receives messages and generates replies, but
the internals are now events on a runtime rather than callbacks on
an object.
Deep dive: the event-driven core
autogen-core is an actor system, and reading it that way makes it
click. An agent is an actor with an identity, an
AgentId that is a pair of a type (which
behavior) and a key (which instance), for example
AgentId("assistant", "session-42"). Agents do not hold
references to each other. They only know identities and topics, and
the AgentRuntime is the post office that delivers
messages. You do not construct agents directly, you register a
factory for a type and the runtime lazily instantiates an instance
the first time a message is addressed to it, which is what lets the
same code run one agent or ten thousand.
There are exactly two communication styles, and knowing which is
which explains most of the code you will read. Direct messaging,
await runtime.send_message(msg, recipient_id), is
RPC-like, it targets one agent by identity and returns that agent's
response value to the caller. Broadcast, publish_message(msg,
topic_id), is fire-and-forget publish/subscribe, it sends to
a TopicId and every agent subscribed to that topic
receives it, with no return value. A TopicId is itself
a type and a source, and subscriptions
(for instance TypeSubscription) map topics to the agent
types that should hear them. Group chat, as we saw in Part IV, is
built almost entirely from broadcast over a shared topic.
Agents route incoming messages to handlers by message type. The
ergonomic base class is RoutedAgent, and you mark
handler methods with the @message_handler decorator,
typing the message parameter so the runtime knows which handler
gets which message. A minimal custom agent looks like this:
from dataclasses import dataclass
from autogen_core import RoutedAgent, message_handler, MessageContext, AgentId
from autogen_core import SingleThreadedAgentRuntime
@dataclass
class Greeting:
text: str
class Greeter(RoutedAgent):
@message_handler
async def on_greeting(self, message: Greeting, ctx: MessageContext) -> str:
return f"heard: {message.text}"
async def main() -> None:
runtime = SingleThreadedAgentRuntime()
await Greeter.register(runtime, "greeter", lambda: Greeter("a greeter"))
runtime.start()
reply = await runtime.send_message(Greeting("hello"), AgentId("greeter", "default"))
print(reply) # heard: hello
await runtime.stop_when_idle()
The MessageContext carries metadata such as the sender,
the topic if it was a broadcast, and a cancellation token.
Because delivery, identity, and lifecycle are the runtime's
job and not the agent's, the exact same agent code runs unchanged on
an in-process runtime or a distributed one, which is the payoff the
whole v0.4 rewrite was chasing. The motivation for the
rewrite was precisely this, v0.2's conversation-centric design was
hard to observe, hard to scale, and impossible to make
cross-language, and rebuilding on an actor core made agents
composable, the message flow inspectable, and the runtime
swappable. The actor lineage here is the same one behind Erlang and
Akka, and the intuition transfers, isolate state per actor,
communicate only by messages, and let a supervisor route and
restart. If that lineage is unfamiliar, the concurrency and
message-passing ideas are developed in
concurrent systems
programming.
Deep dive: AgentChat teams and group chat
A team is a coordinated set of agents plus a policy for who speaks
and when to stop, and AgentChat ships several. RoundRobinGroupChat
cycles through participants in order, which is predictable and cheap
and perfect when the roles have a natural rotation, like writer then
critic. SelectorGroupChat asks a model to choose the
next speaker from the transcript and the agents' descriptions, which
is more flexible for open-ended tasks but adds a model call per turn
and can loop if the descriptions are vague. Swarm uses
explicit handoffs, an agent names the next agent to act by emitting a
handoff message, which mirrors the OpenAI Agents SDK style and suits
workflows with clear stage transitions. MagenticOneGroupChat
wraps the Magentic-One orchestrator, which maintains a task ledger
and a progress ledger and re-plans when the team gets stuck, aimed at
hard open-ended web-and-code tasks.
Termination is its own small algebra and deserves respect, since it
is the difference between a bounded run and a runaway one. Each
condition is a stateful object evaluated against the growing message
list, and they compose. TextMentionTermination fires on
a keyword, MaxMessageTermination caps the number of
messages, TokenUsageTermination caps cumulative token
spend, HandoffTermination stops when an agent hands off
to a named target (typically the user, for human-in-the-loop), and
ExternalTermination lets your code stop a run from
outside. You combine them with | and &:
from autogen_agentchat.conditions import (
TextMentionTermination,
MaxMessageTermination,
HandoffTermination,
)
# stop on success, OR when a human is needed, OR at a hard ceiling
condition = (
TextMentionTermination("TERMINATE")
| HandoffTermination(target="user")
| MaxMessageTermination(30)
)
Teams are also stateful and resumable. await team.save_state()
returns a serializable snapshot of the conversation and each agent's
context, and load_state restores it, so you can pause a
long-running team, persist it, and resume in another process. This
is the same "address durable state by logical content, not by
in-memory layout" instinct that good systems reach for everywhere.
Deep dive: tool and function calling
A tool is a function the model may ask the agent to call. AutoGen
bridges Python functions to the model's tool-calling API so you
rarely write a schema by hand. When you pass a plain function in
tools=[...], the agent wraps it as a
FunctionTool, reads the parameter type hints and the
docstring, and generates the JSON schema the model needs. At run
time the flow is a small loop. The agent sends the tools along with
the prompt, the model may respond with one or more tool calls
instead of a final answer, the agent executes each call (validating
arguments against the schema), appends the results to the context as
tool-result messages, and calls the model again. That loop can run
several iterations if the model chains tool calls, and it is bounded
so it cannot spin forever. With reflect_on_tool_use=True
the agent makes a final model call to summarize the tool results in
prose, otherwise the raw tool result becomes the response.
Two things are worth knowing beyond the basics. First, the agent emits distinct events, a tool-call request event and a tool-call execution event, so a transcript shows exactly what was called with what arguments and what came back, which is where you look when a tool returns the wrong thing. Second, tools are not limited to your local functions. autogen-ext provides adapters, including adapters for tools served over the Model Context Protocol, so an AutoGen agent can use the same external tool servers other frameworks do. The uniform treatment, everything is a tool with a schema and a result event, is what keeps the agent's core loop simple no matter where the capability comes from.
Deep dive: human-in-the-loop
A human is just another agent, which is the cleanest thing about
AutoGen's design here. In v0.2 the human enters through
UserProxyAgent's human_input_mode, set to
ALWAYS to prompt the person every turn,
TERMINATE to ask only when the conversation would
otherwise end, or NEVER to run fully autonomously. In
v0.4 the AgentChat UserProxyAgent takes an
input_func, which defaults to the console
input() but can be any callable, including an async one
that awaits a message from a web UI or a chat platform. Drop that
agent into a team and, when it is its turn to speak, the team pauses
and calls your input function to get the human's message:
from autogen_agentchat.agents import UserProxyAgent
human = UserProxyAgent("human", input_func=input) # or an async web callback
team = RoundRobinGroupChat([assistant, human], termination_condition=condition)
await Console(team.run_stream(task="Draft a reply to this email, I will approve it."))
The more powerful pattern for real applications is
HandoffTermination together with the Swarm
team. An agent hands off to "user", the handoff
termination stops the run and returns control to your program, you
gather the human's input however you like (which may take minutes or
days), and then you call team.run(task=...) again with
the human's message. Because the team's state persists across those
calls, the conversation resumes exactly where it paused. Making
the human a participant with the same message interface as every
other agent means approval, correction, and clarification are not
bolted-on special cases but ordinary turns in the conversation,
which is why human-in-the-loop in AutoGen feels like the same
program rather than a separate approval subsystem.
Deep dive: the distributed, cross-language runtime
The single-threaded runtime runs every agent in one process, which is all most applications need. The payoff of the actor design shows up when you swap it for the distributed runtime without touching your agent code. In that mode a central gRPC host acts as a message broker, and each worker process runs a gRPC worker runtime that connects to the host as a client and registers its agents. Messages, both direct and broadcast, are serialized and routed through the host to whichever worker owns the target agent or subscribes to the topic. The wire format follows the CloudEvents convention, which is exactly why the runtime can be cross-language, a message published by a Python agent can be delivered to a .NET agent subscribed to the same topic, since both speak the same event protocol over gRPC. The repository carries a full .NET implementation alongside the Python one for this reason.
The honest cost is the usual one for distribution. Everything on the wire must be serializable, the host is a component you now operate, and you inherit the failure modes of a networked system, partial failures, retries, and ordering questions, that the in-process runtime hid. The lesson worth carrying away is the same one that makes the design good, because agents communicate only by typed messages to identities and topics, moving from one process to a cluster is a runtime swap and a serialization concern, not a rewrite of the agents, which is the location transparency that the actor model promises and that v0.2 could not offer.
Part VI: Reading the repository
The repository is a monorepo with a Python side and a .NET side. Paths below reflect the v0.4 layout on main in 2026. Exact file names inside packages change, so navigate by package and role.
Stage 0, orientation. Read the top-level
README.md, then note the two language trees,
python/ and dotnet/. The Python packages
live under python/packages/, notably
autogen-core, autogen-agentchat,
autogen-ext, and autogen-studio. Read the
AgentChat quickstart and the migration guide from v0.2 in the docs
to fix the two-layer picture in your head. Question, which package
owns the runtime, and which owns the pre-built agents and teams?
Stage 1, AgentChat surface. In
autogen-agentchat, read the base chat agent, then
AssistantAgent and UserProxyAgent, then the
teams package (the group-chat base and
RoundRobinGroupChat as the simplest concrete team), then
the conditions and messages modules. Questions, what exactly does
on_messages return, in what order does the assistant do
model call, tool loop, and reflection, and how does a termination
condition see the message list?
Stage 2, the core. In autogen-core,
read the agent and agent-runtime interfaces, then
_single_threaded_agent_runtime.py (the reference
runtime, and the best single file for seeing delivery work), then the
routed-agent and message-handler machinery, then topics and
subscriptions. Questions, how does the runtime turn an
AgentId into a delivered message, how does lazy agent
instantiation from a registered factory work, and where is the fork
between direct send and topic broadcast?
Stage 3, extensions. In autogen-ext,
read the OpenAI model client under the models package, a code
executor (the Docker command-line executor is representative), and a
tool adapter. This is where the abstract interfaces from core and
AgentChat meet real providers. Question, what is the surface a model
client must implement, and how does a function become a tool schema?
Stage 4, teams as core programs. Return to the AgentChat teams package and read how a group chat is assembled out of core primitives, the manager actor, the per-participant container actors, and the shared topic they all subscribe to. This is Part IV in source form and is the moment the two layers fuse in your mind.
Stage 5, the frontier. The Magentic-One
implementation (orchestrator plus specialized agents), the
distributed gRPC runtime in autogen-ext, the parallel
dotnet/ tree for the cross-language story, and
autogen-studio for the visual builder. Where not to
start, the distributed runtime and the .NET side are fascinating but
will confuse the core model if you meet them before the
single-threaded runtime is solid, and Magentic-One's ledgers make
most sense once plain teams feel obvious.
Part VII: Hands-on labs
All labs need only an API key and a CPU. Set your model endpoint in the environment first. Log and message shapes drift with the fast pace of the project, so match on roles, not exact strings.
Lab 1: one agent, then a team. Concept, the AgentChat surface and the run lifecycle.
Run the single-agent program from Part II, print
result.messages in full, and notice the transcript
already contains more than one message, the task, the model's
reasoning, and the answer. Then run the writer-and-critic team and
watch the same TaskResult shape grow to a multi-turn
conversation. Match each printed message to a stage of Part IV.
Lab 2: make it loop, then bound it. Concept, termination as cost control.
Remove the termination condition from the writer-and-critic team and
run it (interrupt it quickly). Observe that it does not stop on its
own and relies on a default message cap. Now add
TextMentionTermination("APPROVE") | MaxMessageTermination(6)
and observe which one fires. Lower the cap to 2 and watch it stop
before the critic ever approves, then read the
stop_reason.
Lab 3: a tool and its events. Concept, the tool-calling loop.
async def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
agent = AssistantAgent("calc", model_client=model_client, tools=[add],
reflect_on_tool_use=True)
result = await agent.run(task="What is 128 plus 273? Use the tool.")
for m in result.messages:
print(type(m).__name__, getattr(m, "content", ""))
Observe the tool-call request event and the tool-call execution
event in the transcript, the actual arguments the model chose, and
the final natural-language answer produced by the reflection step.
Toggle reflect_on_tool_use=False and see the raw tool
result become the answer instead.
Lab 4: raw autogen-core. Concept, actors, identities, and delivery.
Run the Greeter program from Part V. Then add a second
agent type that, in its handler, uses ctx and the
runtime to publish_message a message to a topic both
agents subscribe to, and watch a broadcast reach both. Compare the
return-value semantics of send_message with the
no-return semantics of publish_message. This is the
whole core in miniature.
Lab 5: human-in-the-loop. Concept, the human as a participant.
Add a UserProxyAgent("human", input_func=input) to a
team with an assistant and run a task that needs your approval.
Observe the run pause for your console input on the human's turn, and
that your typed message becomes an ordinary message in the same
transcript. Then try the handoff pattern, give the assistant a
handoff to "user", use HandoffTermination,
run, provide input, and run again to resume.
Lab 6: selector versus round robin. Concept, speaker-selection policy.
Build a three-agent team (say a researcher, a coder, and a critic)
once as RoundRobinGroupChat and once as
SelectorGroupChat on the same task, and compare the
transcripts and the number of model calls. The selector version will
skip around based on relevance and cost more per turn. Give the
agents vague descriptions and watch the selector make worse choices,
which teaches why agent descriptions matter.
Part VIII: Questions and model answers
Understanding checks. Answer aloud before reading.
1. What is a conversable agent?
An entity with three verbs, send a message, receive a message, and generate a reply. Because reply generation is a pipeline of handlers, the same agent can execute code, call tools, ask a human, or call an LLM depending on the message, which is what lets a plain conversation carry all of those without special cases.
2. Why does multi-agent conversation solve tasks a single agent cannot?
A single model call cannot run its own code, cannot review its work with fresh context, and cannot get a human decision. Splitting those into separate agents, a coder, an executor, a critic, a human proxy, creates feedback loops that ground the language model in real results, so errors get caught and corrected instead of hallucinated away.
3. Name the three main packages and what each owns.
autogen-core is the event-driven actor runtime and the
message-passing primitives. autogen-agentchat is the
high-level task API with pre-built agents and teams, built on core.
autogen-ext holds concrete integrations, model clients,
code executors, tool adapters, and the distributed runtime.
4. What are the two communication styles in autogen-core?
Direct messaging with send_message, which targets one
agent by AgentId and returns a response, RPC-style. And
broadcast with publish_message, which sends to a
TopicId that any number of subscribed agents receive,
with no return value. Group chat is built mostly from broadcast over
a shared topic.
5. Trace one turn of a RoundRobinGroupChat.
The manager selects the next agent in the list and publishes a
request to that agent's topic, the agent's container runs its
on_messages (model call, maybe a tool loop) and
publishes a response to the group topic, every participant and the
manager receive it so all contexts update, and the manager appends it
to the thread and checks the termination condition before selecting
again.
6. How does a plain Python function become a tool?
Passing it in tools=[...] wraps it as a
FunctionTool, which reads the type hints and docstring
to generate the JSON schema the model needs. At run time the agent
offers the tool, the model may emit a tool call, the agent validates
arguments, executes, feeds the result back, and loops or answers.
7. What does a termination condition do, and why compose them?
It is a stateful predicate evaluated against the growing message
list that decides when a team stops. You compose with |
and & so a run can stop on success (a keyword), on a
human handoff, or at a hard ceiling on messages or tokens, whichever
comes first, which is how you bound cost before pressing run.
8. How is a human integrated, and why does it feel seamless?
As an agent. UserProxyAgent takes an
input_func, so a human turn is just a message obtained
from the console or a web callback. With
HandoffTermination plus a persisted team state, a run
can pause for arbitrary time and resume on the next
run. Because the human shares the message interface,
approval and correction are ordinary turns, not a separate subsystem.
9. Why was v0.4 rebuilt on an actor runtime?
v0.2's conversation-centric design was hard to observe, hard to scale, and could not go cross-language. Making agents actors that communicate only by typed messages to identities and topics made the message flow inspectable, let the runtime be swapped from in-process to distributed without changing agent code, and enabled Python and .NET agents to share one runtime.
10. How can the same agent code run distributed?
Agents never reference each other, only identities and topics, and the runtime owns delivery and lifecycle. Swapping the single-threaded runtime for the gRPC worker runtime routes messages through a host broker in the CloudEvents format instead of in memory, so distribution is a runtime swap plus a serialization requirement, not an agent rewrite.
11. When would you pick LangGraph, CrewAI, or Semantic Kernel over AutoGen?
LangGraph when you want an explicit authored state graph with tight control flow. CrewAI when a higher-level role-and-task crew fits and you do not need a runtime underneath. Semantic Kernel when you want Microsoft's plugin-oriented, enterprise, .NET-first SDK. And note that Microsoft is converging AutoGen and Semantic Kernel into a single Agent Framework, so for new production systems check that status first.
12. A team quietly costs a fortune and never seems to finish. What happened?
No effective termination bound, so the conversation loops, and every
turn is at least one LLM call, multiplied by agents and by
tool-calling loops, and worse if an LLM selector adds a call per
turn. The fix is a hard MaxMessageTermination or token
budget combined with a real stop condition, plus a tool-iteration
cap, set before the run.
13. What is the difference between AssistantAgent and UserProxyAgent?
AssistantAgent is LLM-backed, it reasons, writes, and
calls tools. UserProxyAgent stands in for the human and,
in the classic model, executes code, so the standard two-agent loop
is the assistant proposing and the user proxy executing and reporting
back, which is the feedback loop that grounds the assistant.
14. What is Magentic-One?
A generalist multi-agent system built on autogen-core, an
orchestrator that keeps a task ledger and a progress ledger and
re-plans, coordinating specialized agents for the web, files, coding,
and a terminal. It ships as MagenticOneGroupChat and
targets hard open-ended tasks that need real tool use.
Part IX: Design lessons
Make the message the universal interface. Once code output, tool results, human approvals, and model completions are all just messages, one conversation carries every capability and adding a new one means adding an agent, not a new control path. This is the everything-is-a-file instinct applied to agents, a narrow interface that many behaviors flow through.
Separate mechanism from policy across two layers. autogen-core provides the mechanism, actors, identities, delivery, and AgentChat provides the policy, which agents, which speaker order, when to stop. Beginners live in AgentChat and never touch the core, experts drop to the core when a team shape does not exist yet, and neither layer has to compromise for the other.
Adopt the actor model for isolation and location transparency. Agents that share no references and communicate only by typed messages to identities and topics can be instantiated lazily, run in one process or many, and even in different languages, because delivery is the runtime's problem. The same discipline underlies Erlang, Akka, and every robust message-passing system.
Ground language models with feedback loops. The reason a coder-plus-executor pair beats one clever prompt is that the executor returns real output and the coder reacts to it. Whenever a model's output can be checked against reality, wire the check back in as another turn, and let correction be part of the conversation rather than a hope.
Bound the loop at the door. Multi-agent systems fan out cost the way distributed systems fan out load, so termination conditions, token budgets, and tool-iteration caps are not afterthoughts but the safety envelope, and the design that makes them composable and explicit is what keeps an autonomous system from becoming an unbounded one.
Treat the human as a peer, not a checkpoint. Giving the human the same message interface as any agent turns approval and correction into ordinary turns and makes pause-and-resume fall out of the same state persistence that everything else uses. Interfaces that include the human as a participant age better than ones that bolt on an approval step.
Part X: Memorization framework
The one-sentence summary. AutoGen solves a task with a conversation among specialized agents, coded in the high-level AgentChat API as a team with a speaker policy and a termination condition, and executed on the low-level autogen-core actor runtime where agents are identities that exchange typed messages by direct send or topic broadcast, in one process or across a distributed, cross-language mesh.
task -> AgentChat team (RoundRobin / Selector / Swarm / MagenticOne) -> group chat manager: shared thread, pick speaker, broadcast, check termination -> each turn: model context -> LLM call -> maybe tool loop -> Response -> termination (TextMention | MaxMessage | Handoff | token budget) -> TaskResult (messages + stop_reason) all riding on autogen-core: AgentId(type, key) actors + AgentRuntime send_message (RPC, returns) | publish_message (broadcast to TopicId) SingleThreadedAgentRuntime (in-process) | gRPC host + workers (distributed, Py/.NET)
The chain mapped to packages:
high-level agents/teams autogen-agentchat (AssistantAgent, teams, conditions) runtime + primitives autogen-core (AgentId, RoutedAgent, runtime, topics) providers + distributed autogen-ext (OpenAI client, code executors, gRPC runtime) generalist system Magentic-One (orchestrator + web/file/coder/terminal) low-code GUI autogen-studio
Memorize these blocks:
- Two layers: AgentChat is the task API, autogen-core is the actor runtime under it, autogen-ext holds the integrations.
- Two message styles:
send_messageis direct and returns a value,publish_messageis broadcast to a topic and returns nothing. - Team shapes: RoundRobin (ordered), Selector (LLM picks), Swarm (handoffs), MagenticOne (orchestrated), each is a speaker policy plus a shared thread.
- Termination is cost control: conditions are stateful, compose with
|and&, and every team needs one or it loops. - Human as agent:
UserProxyAgentwith aninput_func, or a Swarm handoff to"user"withHandoffTerminationfor pause-and-resume. - Names caveat: Microsoft's v0.4 is
autogen-agentchat/-core/-ext, whileautogen/pyautogenon PyPI now belong to the AG2 fork.
Part XI: Papers and further reading
The ideas this chapter leans on come from a handful of papers, and each one is a short, direct read. Where this site covers a neighboring idea in depth, the companion link points there.
- Wu et al., AutoGen, Enabling Next-Gen LLM Applications via Multi-Agent Conversation, 2023. The paper behind this repository, stating the conversable-agent model and the case for solving tasks with a structured conversation.
- Hewitt et al., A Universal Modular ACTOR Formalism for Artificial Intelligence, IJCAI 1973. The origin of the actor model the v0.4 runtime is built on, a lineage developed further in the concurrent systems programming class on this site.
- Fourney et al., Magentic-One, A Generalist Multi-Agent System for Solving Complex Tasks, 2024. Microsoft's generalist team built on autogen-core, shipped in this repository as
MagenticOneGroupChat. - Yao et al., ReAct, Synergizing Reasoning and Acting in Language Models, 2022. The interleaved reason-then-act loop that every tool-calling agent turn runs, and the same loop appears as an explicit graph in the LangGraph walkthrough.
- Schick et al., Toolformer, Language Models Can Teach Themselves to Use Tools, 2023. An early demonstration that a model can decide when to call external tools, the behavior the
FunctionToolloop packages. - Shinn et al., Reflexion, Language Agents with Verbal Reinforcement Learning, 2023. Verbal self-feedback as a learning signal, the idea a critic agent operationalizes, surveyed in the self-improving agents class.
- Madaan et al., Self-Refine, Iterative Refinement with Self-Feedback, 2023. The draft, critique, and revise cycle that the writer-and-critic team in Part II runs as a two-agent conversation.
- Du et al., Improving Factuality and Reasoning in Language Models through Multiagent Debate, 2023. Evidence that several model instances challenging one another beat a single instance, a core argument for multi-agent designs.
- Li et al., CAMEL, Communicative Agents for "Mind" Exploration of Large Language Model Society, 2023. Role-playing agents that cooperate through conversation, a close contemporary of the conversable-agent idea.
- Park et al., Generative Agents, Interactive Simulacra of Human Behavior, 2023. Agents with memory and reflection in a simulated town, the study that made societies of conversing agents a research object.
Part XII: Final takeaway
If the single-model pieces underneath these agents are the gap, the
applied generative AI
and natural language
understanding material builds the language-model side, and the
question of how agents ought to plan and improve over time is taken
up in self-improving
agents and decision
making under uncertainty. Then come back and read
_single_threaded_agent_runtime.py once more, and the
teams you write in AgentChat will read as what they are, thin
policies over a small, honest actor system.