The Multi-Agent Primitive Model
Multi-agent frameworks like AutoGen, LangGraph, CrewAI, the OpenAI Agents SDK, and Microsoft Agent Framework look different, but they sit in the same four-dimensional design space: the agent, the handoff, the shared state, and the orchestrator. You build those four from scratch, run a toy system on them, and map each major framework onto the same axes so a new release takes one paragraph to read.
Learning Objectives
- Name the four primitives (agent, handoff, shared state, orchestrator) and say which one is stateful.
- Map OpenAI Swarm, AutoGen, CrewAI, LangGraph, Microsoft Agent Framework, and Google ADK onto the four primitives.
- Compare static, handoff-driven, LLM-selected, and queue-driven orchestrators and pick one for a given routing requirement.
- Run a stdlib toy system and point to where in the code the next-speaker decision lives.
Problem
A new multi-agent framework ships every six months. AutoGen in 2023. CrewAI in 2024. LangGraph and OpenAI Swarm in 2024. Google ADK in April 2025. Microsoft Agent Framework RC in February 2026. Each press release claims to be "the right abstraction."
If you try to learn them one at a time you will burn out. The APIs look different, and the docs disagree about what an "agent" is. One framework calls its shared memory a "blackboard," another a "message pool," a third a "StateGraph." It starts to feel like the field is churning.
It isn't. Under the marketing, the same four primitives keep showing up. Learn them once and a new framework takes one paragraph to read.
Concept
The four primitives
- Agent: a system prompt plus a tool list. It is stateless, so each run starts from its system prompt and the current message history.
- Handoff: a structured transfer of control from one agent to another. Mechanically it is a tool call that returns a new agent, or a graph edge that follows a condition.
- Shared state: any data structure that more than one agent can read (and sometimes write). A message pool, a blackboard, a key-value store, or vector memory.
- Orchestrator: whatever decides who speaks next. The options are an explicit graph (deterministic), an LLM speaker-selector (soft), the last speaker's handoff call (OpenAI Swarm), or a scheduler over a queue (swarm architecture).
That is the design space. Each framework picks defaults for the four axes, and the rest is surface syntax.
How every 2026 framework maps to it
| Framework | Agent | Handoff | Shared state | Orchestrator |
|---|---|---|---|---|
| OpenAI Swarm / Agents SDK | Agent(instructions, tools) | tool returns Agent | caller's problem | the LLM's next handoff call |
| AutoGen v0.4 / AG2 | ConversableAgent | speaker-selector on GroupChat | message pool | selector function (LLM or round-robin) |
| CrewAI | Agent(role, goal, backstory) | Process.Sequential / Hierarchical | Task outputs chained | manager LLM or static order |
| LangGraph | node function | graph edge + condition | StateGraph reducer | the graph, deterministic |
| Microsoft Agent Framework | agent + orchestration patterns | pattern-specific | thread / context | pattern-specific |
| Google ADK | agent + A2A card | A2A task | A2A artifacts | host decides |
The surface differences look big. Underneath, they are the same four knobs.
Why this matters
Once you see the primitives, framework comparison becomes a short checklist:
- Does the orchestrator trust the LLM to route (Swarm) or does it pin routing in code (LangGraph)?
- Is shared state full-history (GroupChat) or projected (StateGraph reducer)?
- Can agents modify each other's prompts (CrewAI manager) or only hand off (Swarm)?
Those three questions answer 80% of which framework fits a given problem. You stop shopping for "the best multi-agent framework" and start designing around the axis you care about.
The stateless insight
Apart from shared state, the primitives are stateless. An agent is a function of (prompt, tools), a handoff is a function call, and an orchestrator is a scheduler. The only stateful part of the system is shared state. That is where the interesting bugs live: memory poisoning (Lesson 15), message ordering, versioning, write contention.
Frameworks that hide shared state (Swarm) push the problem to the caller. Frameworks that centralize it (LangGraph checkpoint, AutoGen pool) make it inspectable, but the coordination cost moves onto the shared-state implementation.
Anatomy of a single primitive
Agent
Agent = (system_prompt, tools, model, optional_name)
No memory and no state of its own. Two agents with the same system prompt and tools are interchangeable. What looks like per-agent state actually lives in shared state or the handoff protocol.
Handoff
Handoff = (from_agent, to_agent, reason, payload)
Three implementations dominate:
- Function return: the tool returns the next agent. This is the OpenAI Swarm pattern, and agents carry routing in their tool schemas.
- Graph edge: the LangGraph way. Edges are declarative. The LLM produces a value and a condition selects the next node.
- Speaker selection: AutoGen GroupChat. A selector function (sometimes itself an LLM call) reads the pool and picks who speaks next.
Shared state
SharedState = { messages: [], artifacts: {}, context: {} }
At minimum it is a list of messages. Often it is more: structured artifacts (CrewAI Task outputs), typed context (LangGraph reducers), external memory (MCP, vector DB).
Two topologies: full pool (every agent sees every message) and projected (agents see a role-scoped view). Full pools are simple and scale badly. Projected pools scale, but you have to design the schema up front.
Orchestrator
Orchestrator = ({state, last_speaker}) -> next_agent
Four flavors:
- Static: the graph is fixed at build time (LangGraph deterministic, CrewAI Sequential).
- LLM-selected: an LLM reads the pool and picks the next speaker (AutoGen, CrewAI Hierarchical).
- Handoff-driven: the current agent decides by calling a handoff tool (Swarm).
- Queue-driven: workers pull from a shared queue, with no explicit next speaker (swarm architectures, Matrix).
What changes between frameworks
With the primitives fixed, the remaining design decisions are:
- Memory strategy: ephemeral or durable checkpointing (LangGraph checkpointer).
- Safety boundary: who can approve a handoff (human-in-the-loop).
- Cost accounting: per-agent token budgets.
- Observability: tracing handoffs and persisting state for replay.
You can implement each of these on top of the primitives. None of them is a new primitive.
Build It
code/main.py implements the primitives in about 170 lines of stdlib Python. There is no real LLM. Each agent is a scripted policy, so the focus stays on the coordination structure.
The file defines:
Agent: a dataclass of name, system prompt, and a policy function that reads the shared state and returns a message. The tool list from the model above is left out because the policies never call tools.SharedState: a message pool behind a lock, withappend,snapshot, andlast_by.- Handoff: not a class here. Each message may carry a
handoffkey naming the next agent, or"done". - Three orchestrators:
StaticOrchestrator,HandoffOrchestrator, andLLMSelectorOrchestrator. The selector in the last one is a scripted round-robin function standing in for an LLM call.
The demo runs the same three-agent pipeline (researcher, writer, reviewer) through each orchestrator and prints the message pool after each run. The agents and the shared state are the same across runs. Only the code that picks the next speaker changes.
Run it:
python3 code/main.py
Expected output: three message pools, one per orchestrator. With these scripted policies the three pools come out the same, because the researcher hands off to the writer on its first turn and the writer hands off to the reviewer. That is the point of the demo. Swap in a policy that hands off differently (say a researcher that returns "done" immediately) and the handoff-driven run stops early while the static run still visits every agent. That gap is the LLM-routing tradeoff in miniature.
Use It
outputs/skill-primitive-mapper.md is a skill that reads a multi-agent codebase or framework doc and returns the four-primitive mapping. Run it on a new framework release to get a one-paragraph understanding before you read the docs in depth.
Ship It
Before adopting a new framework, write its primitive mapping. If you can't, either the docs are incomplete or the framework is inventing a fifth primitive (rare, so check for a shared-state flavor you haven't seen).
Pin the mapping in your architecture doc. When someone new joins the team, send them the mapping before the API docs. When framework versions change, diff the mapping instead of the changelog.
I'd read the classes in main.py before the framework table. SharedState is the only one that holds anything between turns: a list of messages behind a lock. The agents are plain objects with a policy function, and the orchestrators hold only their routing config (an order, a start agent, a selector). The step limit is just an argument to run. That matches the stateless point in the Concept section, and it shows up in the code.
My bet is that the three orchestrators are worth comparing line by line. Each one is a short run method, and the only difference is where the next speaker comes from: a fixed list, the handoff key on the last message, or a selector function. Everything else stays the same, so you can see that routing is a swappable piece.
I'd also keep the toy in proportion. Handoff is just a dictionary key here and the selector is a round-robin function, so the output tells you nothing about how an LLM would route. I'd treat the demo as a map of where the decision lives, then try the exercises with policies that disagree with each other.
Exercises
- Run
code/main.pythree times with different agent policies. Observe how the orchestrator choice changes which agents run. - Implement a fourth orchestrator type: a queue-driven one where agents poll shared state for work. What deadlock can happen, and how do you detect it?
- Take the LangGraph quickstart (https://docs.langchain.com/oss/python/langgraph/workflows-agents) and rewrite it as the four primitives. Which of LangGraph's abstractions map 1:1 and which are convenience wrappers?
- Read the OpenAI Swarm cookbook (https://developers.openai.com/cookbook/examples/orchestrating_agents). Identify which of the four primitives Swarm makes most ergonomic, and which one it pushes to the caller.
- Find one framework in this table that hides shared state entirely. Explain what breaks when agents need to coordinate across handoffs without re-reading history.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Agent | "An LLM with tools" | A (system_prompt, tools, model) triple. Stateless. |
| Handoff | "Transfer of control" | A structured call that names the next agent and optional payload. Three implementations: function return, graph edge, speaker selection. |
| Shared state | "Memory" / "context" | The only stateful part of a multi-agent system. Message pool or blackboard. |
| Orchestrator | "Coordinator" | Whoever decides who runs next. Static graph, LLM selector, handoff-driven, or queue-driven. |
| Primitive | "Abstraction" | One of the four axes every framework parameterizes. Not a framework feature. |
| Message pool | "Shared chat history" | Full-history shared state. Easy to reason about, scales badly. |
| Projected state | "Scoped view" | Role-specific view into shared state. Scales, requires schema design. |
| Speaker selection | "Who talks next" | Orchestrator pattern where a function (often an LLM) picks the next agent from a group. |
Further Reading
- OpenAI cookbook: Orchestrating Agents, Routines and Handoffs: the clearest articulation of handoff-driven orchestration
- AutoGen stable docs: GroupChat and speaker selection are the reference for LLM-selected orchestration
- LangGraph workflows and agents: graph-edge orchestration and reducer-based shared state
- CrewAI introduction: role-goal-backstory agents, Sequential and Hierarchical processes
- AG2 (community AutoGen continuation): the live AutoGen v0.2 line after Microsoft moved v0.4 into maintenance
Code
- main.py (python, 174 lines)