AutoGen

AutoGen is Microsoft's framework for building applications out of multiple conversing LLM agents. Its enduring idea is the conversable agent, an entity that sends messages, receives messages, and generates replies, so that a whole computation becomes a structured conversation between a planner, a coder, a tool executor, a critic, and a human. The v0.4 rewrite, released in January 2025, kept that programming model and rebuilt the machinery underneath it as an event-driven actor runtime that scales from one process to a distributed, cross-language mesh of agents. This chapter is three things at once, a practical tutorial for standing up real teams, a systems walkthrough that follows one team.run(task) from the high-level API down through the runtime's message passing and back up to a result, and a staged guide to reading the repository. It ends with runnable labs, understanding checks with model answers, and a compact framework for holding the whole system in your head.

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_message is direct and returns a value, publish_message is 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: UserProxyAgent with an input_func, or a Swarm handoff to "user" with HandoffTermination for pause-and-resume.
  • Names caveat: Microsoft's v0.4 is autogen-agentchat/-core/-ext, while autogen/pyautogen on 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 FunctionTool loop packages.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.

Key takeaway: AutoGen's bet is that hard tasks are solved by a conversation, not a completion. Give each capability its own agent, a coder, an executor, a critic, a human, let them exchange typed messages through a runtime that does not care whether they sit in one process or across a cluster, bound the loop with a termination condition, and a plan-execute-critique-approve cycle that no single model call could perform falls out of a few dozen lines of ordinary, readable Python.