« Phase 07 README · Warmup · Hitchhiker's · Deep Dive · Principal Deep Dive · Staff Notes

Phase 07 — Core Contributor Notes: Multi-Agent Orchestration

Our miniature is bus + roles + typed handoff + bounded verification. Every production framework is an elaboration of exactly those four mechanisms with different ergonomics and different opinions about what to make first-class. This doc is the maintainer's-eye view: how the real systems implement orchestration under the hood, the non-obvious design decisions, the API evolutions and why they happened, and what our stdlib version deliberately fakes. Where I am describing a pattern rather than a line of source, I say so — do not quote version-specific behavior from a curriculum.

LangGraph — a Pregel BSP runtime wearing a graph API

LangGraph's surface is "nodes and edges," but the engine underneath is Pregel-style Bulk Synchronous Parallel execution — the same model as Google's graph-processing system. This is the single most important thing to know and the thing most users never learn, and it explains every non-obvious behavior.

Execution proceeds in super-steps. A super-step is: every node that received input in the previous step runs (conceptually in parallel), each node produces writes to channels, and — this is the barrier — no write is visible until the super-step completes. At the barrier, all writes are applied to the channels through their reducers, and then the graph decides which nodes are triggered next (a node runs when a channel it subscribes to has been updated). So state is not a plain dict you mutate; it is a set of channels, each with a reducer that says how concurrent writes merge. The default reducer is last-write-wins (overwrite); the famous add_messages reducer appends. This is the production answer to the shared-state race the Deep Dive flagged in our blackboard: two nodes writing the same key in one super-step do not race — their writes are combined deterministically by the reducer. Our state["results"][name] = result is a hand-rolled, single-threaded stand-in for a channel with an overwrite reducer.

The checkpointer persists the channel state after every super-step. That one decision buys the whole feature set people associate with LangGraph: durable resume after a crash (Phase 08), human-in-the-loop (interrupt at a node, persist, resume later), and time-travel debugging (rewind to a prior checkpoint). recursion_limit bounds the number of super-steps — that is our max_turns. Conditional edges are our supervisor's routing decision; the prebuilt supervisor and hierarchical templates are our topology, pre-wired. The modern Command object lets a node return both a state update and a routing target (goto) in one move — collapsing "write result" and "decide next" into a single node, which is a cleaner supervisor than a separate router node. The sharp edge maintainers warn about: because state only materializes at the barrier, a node cannot read another node's write within the same super-step — a class of "why is my state stale" bugs that only make sense once you know it is BSP, not sequential.

AutoGen / AG2 — conversation, then actors

AutoGen's original model (the 0.2 line) is conversational multi-agent: agents are ConversableAgents (AssistantAgent, UserProxyAgent) that share a message list and take turns. The orchestration primitive is GroupChat plus a GroupChatManager: the manager holds the shared conversation and, each turn, runs speaker selection to pick who talks next. Speaker selection is the heart of it and ships several policies — auto (an LLM call picks the next speaker from the roster, the default), round_robin (fixed rotation), random, and manual (a human picks) — plus allowed_or_disallowed_speaker_transitions, a graph constraining who may follow whom. That transition graph is the tell that unconstrained group chat does not scale: without it, an N-agent chat has the manager making an LLM call every turn just to decide who speaks, and everyone sees the whole growing transcript. Our Supervisor is a GroupChatManager with a deterministic, non-LLM speaker-selection policy and a blackboard instead of a flat message list.

The 0.2 → 0.4 redesign (late 2024 into 2025) is worth understanding because it is a case study in why orchestration frameworks converge. v0.4 threw out the flat conversational core and rebuilt on an event-driven actor model: a layered architecture (autogen-core as an actor runtime with async message passing, topics, and subscriptions; autogen-agentchat as the high-level conversational API on top). The motivation was exactly the failure modes in this phase — the flat turn-taking model was hard to make asynchronous, hard to observe, hard to distribute across processes, and hard to bound. Actors with typed messages and subscriptions are a more honest substrate for "many agents coordinating over a channel," which is what the Deep Dive's ordering/duplicate-delivery hazards demand once you go concurrent. Note also AG2: the community fork that continued the 0.2 lineage after the redesign split opinion. If someone says "AutoGen," ask which — the conversational classic or the actor rewrite; they are different runtimes.

CrewAI — roles and Process, sequential vs hierarchical

CrewAI makes the role first-class and orchestration a Process enum. You define Agents with role, goal, and backstory (prompt scaffolding that maps to our role tag and injected policy), bind them to Tasks, and group them into a Crew that runs Process.sequential (tasks execute in declared order, output threading forward — our pipeline) or Process.hierarchical. Hierarchical is the non-obvious one: CrewAI auto-creates a manager agent (you supply manager_llm, or a custom manager_agent) that delegates tasks to workers and validates their output before accepting it — a supervisor and a critic in one, synthesized for you. That is a strong default and also a sharp edge: the manager is an extra LLM call per delegation you did not write, so hierarchical crews cost meaningfully more than sequential ones, and "my crew is slow/expensive" is usually "you picked hierarchical for a task whose order was actually fixed." CrewAI also added Flows for event-driven, more-deterministic orchestration — the framework's own admission that not everything should be a chatty crew, which is this phase's whole thesis.

OpenAI Swarm → Agents SDK — the handoff is a tool call

Swarm was an intentionally minimal, educational library with two primitives: Agents and handoffs. The elegant idea it crystallized: a handoff is just a function/tool that returns another agent. The client-side runner loop calls the current agent, and if the model invokes a handoff tool, the runner swaps the "current agent" to the returned one and continues — control transfer modeled as ordinary tool-calling, with context_variables threaded along. Swarm was explicitly experimental and stateless.

The Agents SDK is the production successor. Same core idea, hardened: Agents with handoffs=[...], a Runner loop, plus guardrails (input/output validation at the boundary), sessions (state), and first-class tracing. The handoff surfaces to the model as an auto-generated tool named transfer_to_<agent>, and — the invariant our make_handoff mirrors — you can only hand off to a declared agent; the transfer is a validated, first-class event, not a free-text jump. This is why our lab raises ValueError on an unknown target: an unroutable handoff is a bug that must fail loudly, exactly as the SDK refuses a transfer to an undeclared agent. The evolution from Swarm to the SDK is the same lesson as AutoGen's: the toy proves the primitive; production demands validation, tracing, and bounded loops around it.

Google ADK — composition operators

ADK frames orchestration as composition: an LlmAgent is the reasoning unit, and you wrap agents in workflow agentsSequentialAgent, ParallelAgent, LoopAgent — which are our pipeline, fan-out, and bounded-loop topologies as explicit, deterministic operators. LoopAgent(max_iterations=) is our max_rounds by name. Delegation is either sub_agents (LLM-driven transfer_to_agent among a hierarchy) or AgentTool (expose an agent as a callable tool to another). The design point: ADK makes the deterministic control flow (sequence, parallel, loop) non-LLM operators and reserves model-driven routing for where it is actually needed — the cleanest expression of "the pipeline is a workflow, only the branching is agentic."

What our miniature deliberately simplifies

Being explicit about the fakes is the maintainer's honesty:

  • Policies are pure functions, not LLMs. Real routing and critiques are themselves unreliable; our injected policy(context) -> str is deterministic so the coordination is testable. The coordination shape is real; the intelligence is stubbed.
  • Single-threaded, in-process. No real concurrency, so no true message ordering problem, no duplicate delivery, no deadlock on a join — the hazards the Deep Dive names are latent. LangGraph's channels/reducers and AutoGen 0.4's actors exist precisely to handle what we get for free by being single-threaded.
  • The blackboard is a plain dict. No reducers, no channels, no checkpointer. Overwrite-by-key is our only merge policy; durability and resume (Phase 08) are out of scope.
  • Handoffs are in-process records. Real handoffs cross process/network boundaries — a receiver can be a remote MCP server (Phase 03) — which is what makes validation, idempotency, and tracing load-bearing rather than optional.
  • No parallel fan-out. We route one worker at a time; map-reduce over N workers (ADK's ParallelAgent, a LangGraph fan-out super-step) is the extension the lab leaves as an exercise.

The skill is not memorizing node names. It is seeing that LangGraph's channels, AutoGen's shared conversation, CrewAI's Crew, and the Agents SDK's handoffs are four dialects of the same four mechanisms — so you can pick one, and debug it when it will not terminate at 2 a.m.