← Corpus / agent-harnesses / profile

autogen

Path
profiles/Profile__AutoGen.md

AutoGen — Profile

A profile of AutoGen as it lives in this study (studies/agent-harnesses/autogen/). Cites pinned paths so you can jump to source rather than trust paraphrase. Read alongside Profile__OpenHands.md (event-stream trace architecture) and any opencode profile in this study (CLI-first single-agent, own MCP client) — AutoGen is this study’s entry for native multi-agent team orchestration with an explicit, swappable turn-taking algorithm per team type, layered under a distributed actor-style runtime rather than built as a single-agent loop with delegation.

README.md:14,18-25 states the repo’s current status plainly: AutoGen is in maintenance mode — “It will not receive new features or enhancements and is community managed going forward,” with Microsoft steering new projects to Microsoft Agent Framework. This checkout is the 0.4+ generation (confirmed via python/packages/{autogen-core,autogen-agentchat,autogen-ext}/pyproject.toml, all version = "0.7.5"), the layered rewrite that replaced the 0.2.x monolithic pyautogen package (still present at python/packages/pyautogen/ for migration reference, not read for this profile).

TL;DR

The README’s own architecture summary (README.md:179-183):

The autogen framework uses a layered and extensible design… [Core API] implements message passing, event-driven agents, and local and distributed runtime… [AgentChat API] implements a simpler but opinionated API for rapid prototyping… [Extensions API] enables first- and third-party extensions.

Mechanically: teams are actor-runtime topics, not a shared Python loop. BaseGroupChat._init() (python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.py:191-245) registers every participant as a ChatAgentContainer core-agent behind its own topic type (f"{participant.name}_{team_id}"), plus a group-chat-manager agent on a dedicated manager topic, plus a shared group topic all participants subscribe to for broadcast. Turn-taking isn’t a for-loop over a list — it’s GroupChatRequestPublish messages published to the runtime (SingleThreadedAgentRuntime by default, _base_group_chat.py:141) and consumed asynchronously.

Speaker selection is delegated to a select_speaker() abstract method (_base_group_chat_manager.py:305-318) implemented differently per team type: RoundRobinGroupChatManager.select_speaker() (_round_robin_group_chat.py:72-82) is a plain modular index increment; SelectorGroupChatManager.select_speaker() (_selector_group_chat.py:152-217) calls an LLM with a role-and-history prompt (_select_speaker(), _selector_group_chat.py:232-308), re-prompting on invalid/repeated/multi-name responses up to max_selector_attempts (default 3) before falling back to the previous speaker or first participant; SwarmGroupChatManager.select_speaker() (_swarm_group_chat.py:82-98) reads the target field off the most recent HandoffMessage in the thread — no model call at all, just a scan.

Conversation state persists as a nested, per-agent-name dictionary, not a single transcript. BaseGroupChat.save_state() (_base_group_chat.py:748-796) calls AgentRuntime.agent_save_state() on every participant plus the group-chat-manager and nests results under TeamState.agent_states keyed by agent name (not agent ID — _base_group_chat.py:783-786 explicitly decouples state from runtime identity so state is portable across teams/runtimes). Each AssistantAgent.save_state() (autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py:1630-1633) in turn delegates to its ChatCompletionContext.save_state() (autogen-core/src/autogen_core/model_context/_chat_completion_context.py:66-67), which just dumps the raw LLMMessage list. Group-chat-manager state additionally carries algorithm-specific fields — RoundRobinManagerState.next_speaker_index, SelectorManagerState.previous_speaker, SwarmManagerState.current_speaker (all in autogen-agentchat/src/autogen_agentchat/state/_states.py:43-61) — so resuming a team restores not just messages but exactly where the turn-taking algorithm left off.

Tool-calling is native-first, MCP-second, and the direction of the bridge matters. autogen_core.tools.Tool (autogen-core/src/autogen_core/tools/_base.py:56-80) is a Protocol with run_json/save_state_json/load_state_json; BaseTool (_base.py:96-215) and FunctionTool (autogen-core/src/autogen_core/tools/_function_tool.py:30) are the native implementations, predating this checkout’s MCP support. MCP is bridged into this native shape, not the reverse: McpToolAdapter(BaseTool[BaseModel, Any], ABC, ...) (autogen-ext/src/autogen_ext/tools/mcp/_factory.py:29) wraps a single mcp.Tool so it satisfies the native Tool protocol, and Workbench (autogen-core/src/autogen_core/tools/_workbench.py:78-93) — also native-core, predating MCP — gets its own MCP-backed implementation, McpWorkbench (autogen-ext/src/autogen_ext/tools/mcp/_workbench.py:47). The mcp package is an optional extra (autogen-ext/pyproject.toml:153, mcp = ["mcp>=1.11.0"]), not a core dependency.

If you want one sentence: AutoGen runs multi-agent “teams” as actor-runtime topic graphs where a pluggable select_speaker() method — round-robin index, LLM-prompted role selection, or handoff-message target-reading — decides the next turn, conversation state saves as a name-keyed nested dict of each participant’s serialized ChatCompletionContext plus manager-specific turn-tracking fields, and tool-calling is fundamentally the native Tool/BaseTool/Workbench protocol with MCP wired in as an adapter layer (McpToolAdapter, McpWorkbench) behind an optional [mcp] extra, not the other way around.

Why this exists — three load-bearing design choices

  1. The group chat is a runtime topology, not a loop. BaseGroupChat._init() (_base_group_chat.py:191-245) wires a TypeSubscription per participant (own topic + shared group topic) and one for the manager (own topic + group topic + output topic). This is why Team composes: a RoundRobinGroupChat can nest another RoundRobinGroupChat as one of its own participants (demonstrated in the docstring example, _round_robin_group_chat.py:184-233) — the inner team is just another ChatAgentContainer-wrapped agent from the outer team’s perspective (_chat_agent_container.py:79-81 checks isinstance(self._agent, Team) and calls run_stream on it instead of on_messages_stream).
  2. select_speaker() is the one seam every team type overrides — everything else in BaseGroupChatManager is shared. _transition_to_next_speakers() (_base_group_chat_manager.py:172-193) is identical across Round Robin / Selector / Swarm; it awaits self.select_speaker(self._message_thread) polymorphically and publishes GroupChatRequestPublish to whichever topic(s) come back. Termination checking (_apply_termination_condition, _base_group_chat_manager.py:195-228), message-thread bookkeeping, and turn counting are all base-class, non-overridden logic.
  3. State save/load is decoupled from runtime identity on purpose. The docstring on save_state (_base_group_chat.py:766-771) states this was a breaking change as of v0.4.9 specifically so saved state is “portable across different teams and runtimes” — moving from agent-ID-keyed to agent-name-keyed state. load_state (_base_group_chat.py:798-830) catches pydantic.ValidationError and raises a pointed error telling the caller the format changed and to read the release notes (_base_group_chat.py:826-830) — an explicit acknowledgment that old saved state will not silently load wrong.

Turn-taking algorithms, compared at the source

All three managers subclass BaseGroupChatManager (_base_group_chat_manager.py:25) and only differ in select_speaker() + reset() + save_state()/load_state():

  • RoundRobinGroupChatManager (_round_robin_group_chat.py:16-82): self._next_speaker_index = (current_speaker_index + 1) % len(self._participant_names) (_round_robin_group_chat.py:79-82) — pure modular arithmetic, no model call, always returns exactly one speaker.
  • SelectorGroupChatManager (_selector_group_chat.py:50-341): builds a roles block (name + description per participant, _selector_group_chat.py:203-208), formats the selector_prompt template with {roles}/{participants}/{history}, and calls self._model_client.create(...) (or .create_stream if model_client_streaming). _mentioned_agents() (_selector_group_chat.py:310-341) regex-matches the model’s free-text response against participant names (handling underscore-vs-space and escaped-underscore variants), retries up to max_selector_attempts on zero or multiple matches, and can be overridden entirely by a user-supplied selector_func (sync or async, checked first, _selector_group_chat.py:163-177) or narrowed by a candidate_func that filters the participant pool before the LLM call (_selector_group_chat.py:180-193). allow_repeated_speaker (default False) excludes the previous speaker from candidates unless explicitly permitted.
  • SwarmGroupChatManager (_swarm_group_chat.py:15-113): select_speaker() (_swarm_group_chat.py:82-98) walks thread in reverse looking for the most recent HandoffMessage and returns its .target field; if none exists, the current speaker continues. validate_group_state() (_swarm_group_chat.py:47-73) enforces that any handoff target is a real participant name, with an explicit error message pointing at HandoffTermination resume semantics — this is the mechanism Swarm([agent], ...) uses for human-in-the-loop handback (README’s HandoffTermination example, README.md quickstart references this pattern in agents/_assistant_agent.py’s handoffs= parameter).

A fourth, more elaborate manager exists for MagenticOneGroupChat (state schema at state/_states.py:64-72: task, facts, plan, n_rounds, n_stalls — a planning-and-replanning orchestrator, not read in depth for this profile) under teams/_group_chat/_magentic_one/.

Conversation-state persistence, end to end

Three nested layers, each with its own save_state/load_state pair:

  1. ChatCompletionContext (autogen-core/src/autogen_core/model_context/_chat_completion_context.py:10-75) is the lowest layer: an abstract base holding self._messages: List[LLMMessage], with add_message() (line 55), get_messages() (abstract, line 60, implemented differently by UnboundedChatCompletionContext, BufferedChatCompletionContext, HeadAndTailChatCompletionContext, TokenLimitedChatCompletionContext — all in the same model_context/ directory), and save_state()/load_state() (lines 66-70) that just round-trip ChatCompletionContextState(messages=self._messages).
  2. AssistantAgent.save_state() (autogen-agentchat/src/autogen_agentchat/agents/_assistant_agent.py:1630-1633) wraps that context dump in AssistantAgentState(llm_context=model_context_state) (schema at state/_states.py:13-17); load_state() (_assistant_agent.py:1635-1639) validates and forwards llm_context back into self._model_context.load_state(...).
  3. ChatAgentContainer.save_state() (_chat_agent_container.py:197-202) wraps the delegate agent’s state plus its own pending _message_buffer (messages received but not yet delivered to the agent) in ChatAgentContainerState (state/_states.py:35-40) — this is the layer that exists purely because of the actor-runtime topology: a participant can have buffered-but-unprocessed messages at save time, and those need to round-trip too (_chat_agent_container.py:204-213).
  4. BaseGroupChat.save_state() (_base_group_chat.py:748-796) is the outermost layer: it calls self._runtime.agent_save_state(agent_id) — note, the runtime’s method (SingleThreadedAgentRuntime.agent_save_state, autogen-core/src/autogen_core/_single_threaded_agent_runtime.py:880-881, itself just delegating to the agent instance’s own save_state()) rather than calling the agent object directly, “because we want to support saving state of remote agents” (comment at _base_group_chat.py:790-791) — the distributed-runtime case is designed in from this layer down, even though the default runtime is single-threaded/in-process.

Group-chat-manager state is saved through the identical agent_save_state path, keyed under self._group_chat_manager_name in the same TeamState.agent_states dict (_base_group_chat.py:793-796) — so a RoundRobinGroupChatManager’s next_speaker_index and a SwarmGroupChatManager’s current_speaker live right alongside participant states in one saved blob, not a separate file.

Tool-calling — native Tool/BaseTool/Workbench first, MCP as an adapter

  • The native abstraction predates and is independent of MCP. Tool (autogen-core/src/autogen_core/tools/_base.py:56-80) is a runtime_checkable Protocol requiring name, description, schema (a ToolSchema TypedDict, lines 41-46), args_type()/return_type()/state_type(), run_json(), and save_state_json()/load_state_json(). BaseTool (_base.py:96-215) is the concrete ABC most tools subclass; FunctionTool (autogen-core/src/autogen_core/tools/_function_tool.py:30) wraps a plain typed Python function/coroutine, deriving its ToolSchema from the function’s type annotations via args_base_model_from_signature. Workbench (autogen-core/src/autogen_core/tools/_workbench.py:78-93) is a second, coarser native abstraction — “a set of tools that may share resources and state” — with async-context-manager start/stop lifecycle, used when tools need a live session (browser state, DB connection) rather than being stateless callables.
  • MCP support lives entirely in autogen-ext, gated behind an optional extra. autogen-ext/pyproject.toml:153: mcp = ["mcp>=1.11.0"] — the mcp PyPI package is not a hard dependency of autogen-ext, matching the README’s install instruction pip install -U "autogen-agentchat" "autogen-ext[openai]" (core install has no MCP) and the MCP quickstart’s explicit pip install "autogen-ext[mcp]"-equivalent framing (README.md:65-102).
  • The bridge direction: MCP tools become native Tools, not the reverse. McpToolAdapter(BaseTool[BaseModel, Any], ABC, Generic[TServerParams]) (autogen-ext/src/autogen_ext/tools/mcp/_factory.py:29-56) takes an mcp.Tool (the MCP SDK’s tool descriptor) and an McpServerParams, converts the tool’s JSON schema to a Pydantic model via schema_to_pydantic_model (line 50), and implements run() (_factory.py:57-83) by opening (or reusing) an mcp.ClientSession and calling session.call_tool(...). Three concrete subclasses exist per transport — StdioMcpToolAdapter, SseMcpToolAdapter, StreamableHttpMcpToolAdapter (files _stdio.py, _sse.py, _streamable_http.py) — all produced by the single factory function mcp_server_tools() (autogen-ext/src/autogen_ext/tools/mcp/_factory.py:10-214), which lists the server’s tools and returns one adapter instance per tool, ready to drop straight into an AssistantAgent(tools=[...]) list alongside plain FunctionTools.
  • McpWorkbench mirrors this at the coarser Workbench layer. McpWorkbench(Workbench, Component[McpWorkbenchConfig]) (autogen-ext/src/autogen_ext/tools/mcp/_workbench.py:47) wraps an entire MCP server (not just one tool) and additionally supports MCP Resources, Prompts, Sampling, Roots, and Elicitation (table at _workbench.py:59-78) via an optional McpSessionHost — this is the object used in the README’s own MCP quickstart (README.md:75,87, McpWorkbench(server_params) passed as workbench=mcp to AssistantAgent), i.e., the README itself demonstrates the workbench path, not the raw mcp_server_tools() adapter path, as the recommended MCP entry point.
  • Net finding: both a native tool abstraction and MCP support exist, and they are not peers — MCP is deliberately downstream of and conformant to the native Tool/Workbench protocols, which is why an AssistantAgent’s tools= list can freely mix FunctionTool, McpToolAdapter instances, and AgentTool (agent-as-tool, autogen_agentchat.tools.AgentTool, used in the README’s multi-agent orchestration example, README.md:106-151) without the agent code needing to know which is which.

What’s inside this submodule

PathWhat’s there
python/packages/autogen-core/src/autogen_core/tools/_base.pyTool protocol, BaseTool, BaseStreamTool, BaseToolWithState — the native tool abstraction
python/packages/autogen-core/src/autogen_core/tools/_function_tool.pyFunctionTool — wraps a typed Python function as a native tool
python/packages/autogen-core/src/autogen_core/tools/_workbench.pyWorkbench ABC — coarser, stateful/session-based tool grouping, native, MCP-agnostic
python/packages/autogen-core/src/autogen_core/model_context/ChatCompletionContext + 4 concrete strategies (unbounded/buffered/head-tail/token-limited) — the per-agent LLM message store
python/packages/autogen-core/src/autogen_core/_single_threaded_agent_runtime.pySingleThreadedAgentRuntime — default in-process actor runtime; agent_save_state/agent_load_state
python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat.pyBaseGroupChat — topic wiring, run/run_stream, team-level save_state/load_state, pause/resume
python/packages/autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.pyBaseGroupChatManager — shared turn-taking scaffolding, select_speaker as the abstract seam
.../_round_robin_group_chat.pyRoundRobinGroupChatManager/RoundRobinGroupChat — modular-index speaker selection
.../_selector_group_chat.pySelectorGroupChatManager/SelectorGroupChat — LLM-prompted speaker selection, selector_func/candidate_func overrides
.../_swarm_group_chat.pySwarmGroupChatManager/Swarm — handoff-message-target speaker selection
.../_magentic_one/MagenticOneGroupChat — planning/replanning orchestrator (task/facts/plan/n_stalls state)
.../_chat_agent_container.pyChatAgentContainer — wraps one ChatAgent/Team as a runtime actor; per-participant message buffer + state
python/packages/autogen-agentchat/src/autogen_agentchat/state/_states.pyEvery *State pydantic schema — AssistantAgentState, TeamState, RoundRobinManagerState, SelectorManagerState, SwarmManagerState, MagenticOneOrchestratorState
python/packages/autogen-ext/src/autogen_ext/tools/mcp/_factory.pymcp_server_tools() + McpToolAdapter base — MCP-tool-to-native-Tool bridge
python/packages/autogen-ext/src/autogen_ext/tools/mcp/_workbench.pyMcpWorkbench — MCP-backed implementation of the native Workbench ABC
python/packages/autogen-ext/src/autogen_ext/tools/mcp/{_stdio,_sse,_streamable_http}.pyTransport-specific MCP tool adapters
python/packages/autogen-studio/No-code GUI for prototyping multi-agent workflows (not production-ready per README caution)
python/packages/agbench/AutoGen Bench — benchmarking suite
python/packages/magentic-one-cli/Reference Magentic-One multi-agent team (web browsing, code exec, file handling)
python/packages/pyautogen/Legacy 0.2.x monolithic package, kept for migration reference — not the architecture this profile describes
dotnet/Parallel .NET implementation (Microsoft.AutoGen.* NuGet packages), not read for this profile
LICENSE / LICENSE-CODECC-BY-4.0 (docs) / MIT (code) — confirmed by direct read, not assumed

If you read three files: autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_base_group_chat_manager.py (the shared turn-taking scaffold and the select_speaker seam), autogen-agentchat/src/autogen_agentchat/teams/_group_chat/_selector_group_chat.py (the most elaborate of the three concrete managers — LLM-prompted selection with retry/fallback), and autogen-ext/src/autogen_ext/tools/mcp/_factory.py (the exact shape of the MCP-to-native-tool bridge).

Mental model for using it well

  • Pick the team type by how turn order should be decided, not by feature checklist: Round Robin for deterministic fixed-order collaboration, Selector for LLM-judged dynamic routing among heterogeneous specialist agents, Swarm for explicit agent-declared handoffs (including human-in-the-loop via HandoffTermination).
  • Treat save_state()/load_state() on a Team as the unit of durability, not per-agent state in isolation — the nested TeamState.agent_states dict is what actually round-trips a paused conversation, including exactly whose turn is next.
  • Reach for Workbench/McpWorkbench when tools share session state (browser, DB connection); reach for plain FunctionTool/McpToolAdapter list entries when tools are stateless — both compose in the same tools=/workbench= agent constructor arguments.
  • A nested Team is a valid participant of another TeamChatAgentContainer treats Team and ChatAgent participants uniformly via an isinstance check, so hierarchical team-of-teams composition is a first-class pattern, not a workaround.
  • Given the maintenance-mode status (README.md:18-25), treat new capability gaps as permanent unless the community fork/PR path resolves them — Microsoft’s own guidance is to start new work on Microsoft Agent Framework instead.

When NOT to reach for this

  • You’re starting a new project today. The README’s own caution (README.md:18-25,200-202) is unambiguous: AutoGen is community-managed, bug-fixes-and-security-only, and Microsoft explicitly points new users at Microsoft Agent Framework.
  • You want a single-agent CLI harness with its own MCP client as the primary interaction mode. AutoGen’s unit of work is the multi-agent Team; a single AssistantAgent works but the framework’s actual design weight (topics, ChatAgentContainer, group-chat managers) is multi-agent-first.
  • You need production-hardened authn/security out of the box for a no-code surface. AutoGen Studio is explicitly flagged as prototyping-only, not production-ready (README.md:160-164).
  • You want tool-calling to be MCP with no native abstraction underneath. AutoGen’s tool layer is native-first (Tool/BaseTool/Workbench predate MCP support in this codebase); if the entire point of adoption is “we standardize purely on MCP as the tool contract,” the native layer is an extra abstraction to reason around, not a pure pass-through.
  • You need a distributed, multi-process runtime out of the box. The default BaseGroupChat runtime is SingleThreadedAgentRuntime (in-process); the “remote agent” and distributed runtime path is designed for (agent_save_state’s docstring comment) but a separate, heavier setup than the default.

How this compares to the rest of the study

AxisOpenHandsopencodeAutoGen
ShapeMulti-agent, event-stream trace architectureCLI-first single-agent, own MCP clientPython framework, layered core/agentchat/ext, multi-agent teams as the primary unit
StatusActively developedActively developedMaintenance mode — bug fixes/security only (README.md:18-25)
Multi-agent orchestrationEvent-stream-driven agent delegationNot the primary unit (single-agent)First-class: Team abstraction with 4 concrete turn-taking algorithms (Round Robin, Selector/LLM, Swarm/handoff, Magentic-One/planner)
Turn-taking mechanismNot profiled hereN/APluggable select_speaker() per manager — modular index, LLM-prompted with retry, or handoff-message-target scan
Conversation-state persistenceNot profiled hereOwn session storageNested, name-keyed TeamState.agent_states dict; each agent’s ChatCompletionContext messages + manager-specific turn state (e.g. next_speaker_index)
Tool-calling modelNot profiled hereOwn MCP client, structured tool-call loopNative-first: Tool/BaseTool/Workbench protocol predates MCP; MCP bridged in via McpToolAdapter/McpWorkbench behind an optional [mcp] extra
Runtime substrateEvent streamProcess/session modelActor-style AgentRuntime (topics + subscriptions); default is in-process SingleThreadedAgentRuntime, designed for remote/distributed extension
Best fitTracing/observability-centric agentic codingTerminal-first agent work, swappable MCP toolsetExisting AutoGen deployments, or new work specifically wanting a multi-agent team with an explicit, swappable speaker-selection algorithm — with eyes open about maintenance-mode status

The axis AutoGen owns in this study is turn-taking as an explicit, swappable algorithm with its own serializable state — no other entry in this study exposes “how the group decides who talks next” as a single overridable method (select_speaker) with three (four, counting Magentic-One) shipped implementations and per-implementation state schemas that round-trip through a uniform team-level save_state/load_state contract.

One-line summary

AutoGen (0.7.5, layered autogen-core/autogen-agentchat/autogen-ext, now in Microsoft maintenance mode per README.md:18-25) runs multi-agent teams as actor-runtime topic graphs where BaseGroupChatManager.select_speaker() is the one seam three built-in team types override — RoundRobinGroupChatManager’s modular index, SelectorGroupChatManager’s LLM-prompted role selection with regex-mention parsing and retry/fallback, and SwarmGroupChatManager’s handoff-message-target scan — while conversation state persists as a TeamState.agent_states dict keyed by participant name (not runtime ID, deliberately decoupled since v0.4.9) nesting each agent’s serialized ChatCompletionContext plus manager-specific turn-tracking fields; and tool-calling is native-first (Tool/BaseTool/FunctionTool/Workbench in autogen-core, predating MCP) with Model Context Protocol support bridged in as an adapter layer (McpToolAdapter, McpWorkbench) behind an optional autogen-ext[mcp] extra, not the framework’s foundational tool contract.