Multi-Agent Architecture Basics
9 examples to get you started with Multi-Agent Architecture - 6 basic and 3 intermediate.
These sketches are conceptual and provider-agnostic. They show control flow for handoffs, not a full production SDK.
Prerequisites
- Comfort with a single agent loop: model call, optional tool call, observe result, stop condition.
- Read Why Split One Agent Into Many Specialized Agents if you are unsure the split is justified.
- No framework required for this page.
Basic Examples
1. Name Two Roles Before Any Code
Write the boundary in one sentence each.
roles = {
"orchestrator": "Own user goal, pick specialist, merge results, enforce global budget",
"researcher": "Search and summarize sources; no write tools; return citations",
}
assert "orchestrator" in roles and "researcher" in roles- If you cannot name different tools or success criteria, stay single-agent.
- The orchestrator owns the global stop policy; the specialist owns only its subgoal.
- Role names without different allowlists are costumes.
Related: Why Split One Agent Into Many Specialized Agents - when to split
2. Minimal Handoff Packet
Pass a structured packet, not the whole chat dump.
def make_packet(goal, constraints, inputs, budget):
return {
"subgoal": goal,
"constraints": constraints, # policies, language, disallowed actions
"inputs": inputs, # ids, paths, prior summaries
"success": ["summary", "citations"],
"budget": budget, # max_turns, timeout_s
}- Packet fields should be machine-checkable where possible.
- Include what "done" means for this specialist only.
- Keep secrets out unless the specialist needs them.
Related: Context Handoff: Passing State Between Agents Cleanly - full packet recipe
3. Specialist Return Contract
Force a status the orchestrator can branch on.
def specialist_return(status, summary, artifacts=None, errors=None):
assert status in {"ok", "failed", "needs_human"}
return {
"status": status,
"summary": summary,
"artifacts": artifacts or {},
"errors": errors or [],
}- Free-form prose returns force re-parsing and lose reliable branching.
needs_humanis a first-class outcome, not an afterthought.- Artifacts should be structured (ids, URLs, JSON), not only narrative.
Related: Architecture Guide: Designing a Multi-Agent System's Communication Contract - formal contract
4. Two-Agent Run Skeleton
Orchestrator builds a packet, specialist runs its own loop, orchestrator merges.
def run_two_agent(user_goal, orchestrator, researcher, tools, max_global_turns=12):
packet = orchestrator.plan_handoff(user_goal)
result = researcher.run(packet, tools=tools["researcher"]) # own max_turns
if result["status"] == "ok":
return orchestrator.finalize(user_goal, result)
if result["status"] == "needs_human":
return orchestrator.escalate(result)
return orchestrator.fail_closed(result)- Give the specialist a subset of tools, not the full belt.
- Enforce max turns inside
researcher.runand a global budget outside. - Finalize only on
ok; do not invent success from partial chatter.
Related: Orchestrator-Worker Patterns for Coordinating Specialist Agents - dispatch patterns
5. Bound Both Loops
Nested agents without nested budgets thrash forever.
def run_specialist(packet, llm, tools, max_turns=6):
state = [packet]
for turn in range(max_turns):
state = llm.step(state, tools)
if llm.is_done(state):
return llm.to_return_contract(state)
return {
"status": "failed",
"summary": "max_turns",
"artifacts": {},
"errors": [{"code": "max_turns", "turns": max_turns}],
}- Host-enforced caps beat "please stop" in the prompt alone.
- Return structured failure when the cap hits so the orchestrator can replan or escalate.
- Global runs need their own cap across all specialists.
Related: Multi-Agent Architecture Best Practices - bounds checklist
6. Log the Handoff Span
Trace parent/child so "the system failed" becomes debuggable.
def handoff_with_trace(run_id, from_agent, to_agent, packet, call):
span = {
"run_id": run_id,
"from": from_agent,
"to": to_agent,
"packet_keys": sorted(packet.keys()),
"subgoal": packet.get("subgoal"),
}
result = call(packet)
span["status"] = result.get("status")
log(span) # your tracer / logger
return result- Log agent ids, status, and packet keys on every transfer.
- Prefer packet hashes or redacted summaries over full secret-bearing payloads in logs.
- Link child specialist turns to the handoff span.
Related: Avoiding Context Loss and Duplication Across Agent Handoffs - loss/dupe checks
Intermediate Examples
7. Router Chooses Among Two Specialists
Deterministic rules first; LLM routing only when needed.
def pick_specialist(goal: str) -> str:
g = goal.lower()
if any(k in g for k in ("bug", "stack trace", "pr", "code")):
return "coder"
if any(k in g for k in ("source", "research", "compare vendors")):
return "researcher"
return "researcher" # safe default with read-only tools- Default to the least privileged specialist that can still work.
- Log the route reason for evals ("keyword", "classifier", "user pin").
- Reject routes that would grant tools the user path does not need.
Related: Orchestrator-Worker Patterns for Coordinating Specialist Agents - routing recipes
8. Sequential Specialists With Shared Task State
Pass artifact refs through a small shared record instead of growing transcripts.
def pipeline(goal, orchestrator, researcher, writer):
task = {"goal": goal, "artifacts": {}, "history": []}
r = researcher.run(make_packet("gather sources", [], task, {"max_turns": 5}))
task["artifacts"]["sources"] = r["artifacts"]
task["history"].append({"agent": "researcher", "status": r["status"]})
if r["status"] != "ok":
return orchestrator.fail_closed(r)
w = writer.run(make_packet("draft brief", [], task, {"max_turns": 4}))
return orchestrator.finalize(goal, w)- Shared state holds artifacts; each packet still states the subgoal.
- Append status history for audit without replaying full tool dumps.
- Stop the pipeline on first hard failure unless you have an explicit retry policy.
Related: Context Handoff: Passing State Between Agents Cleanly - shared store vs message pass
9. Smoke Checklist Before Frameworks
Answer these before picking LangGraph, CrewAI, or a custom host.
checklist = {
"roles": ["orchestrator", "specialist_1"],
"tools_per_role": "least privilege documented",
"packet_schema": ["subgoal", "constraints", "inputs", "success", "budget"],
"return_schema": ["status", "summary", "artifacts", "errors"],
"budgets": ["per_specialist_max_turns", "global_max_turns", "timeout"],
"escalation": "needs_human path defined",
"trace": "run_id + agent_id + handoff status",
}- If any field is blank, the multi-agent design is incomplete.
- Framework choice is wiring; this checklist is architecture.
- Revisit after the first production incident - usually packet or budget gaps.
Related: Architecture Guide: Designing a Multi-Agent System's Communication Contract - ADR-style contract | Multi-Agent Architecture Best Practices - ten practices
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.