Lab 01 — Multi-Agent Orchestrator (Supervisor / Worker / Critic)
Phase 07 · Lab 01 · Phase README · Warmup
The problem
Phase 01 built one agent loop. Real systems sometimes need several agents with different jobs, talking to each other. This lab builds the mechanism underneath every multi-agent framework — a message bus / blackboard, roles, typed handoffs, and a critique-revise loop — with the LLM injected as a deterministic policy so you can assert the exact transcript:
- Supervisor reads a shared blackboard and routes work to the right worker, then stops on
"done"(the orchestrator / supervisor-worker topology). - Worker does a task and posts its result — and sees only the handed-off context, not the whole history (least-context handoff).
- Critic evaluates a worker's draft and returns
approveorrevise: <feedback>. The critique-revise loop is the multi-agent reliability lever: verification buys back the reliability that Phase 00's0.95^ntakes away.
Every loop is bounded (max_turns / max_rounds), because a multi-agent system with no step
budget is an infinite ping-pong waiting to happen.
What you build
| Piece | What it does |
|---|---|
MessageBus.post / history | an ordered, deterministic message log + a shared state blackboard |
Agent / Worker / Critic / Supervisor | one base with an injected policy(context) -> str, three roles |
make_handoff | a typed {from, to, task, context} handoff that validates the target exists |
Supervisor.run | route work to workers by blackboard state, via handoffs, until "done" or max_turns |
parse_verdict | a critic verdict → (approved, feedback); never accidentally approves |
critique_revise | worker drafts → critic approves/revises → worker revises, bounded by max_rounds |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs — dataclasses and renderers are given) |
solution.py | reference + a worked example: a supervisor pipeline and a critique-revise loop |
test_lab.py | 28 tests: the bus, routing-by-state, the max_turns guard, handoff validation, least-context, the round-count assertions, determinism |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # the supervisor + critique-revise worked example
Success criteria
-
Your
MessageBusposts in a deterministic order (a monotonic counter, never a clock) andhistory()returns a copy you can filter bykind/sender/recipient. -
Supervisor.runroutes to the right worker by state, terminates on"done", and stops atmax_turnswhen the supervisor never finishes — withturns == max_turns. -
make_handoffrejects an unknown target, and a worker sees only its handoff context (theCOMPLETED:bookkeeping never leaks into a worker's view). -
critique_reviseapproves a good draft in 1 round, revises a bad draft then approves in 2 rounds, and stops atmax_roundswhen the critic never approves. - Two identical runs produce byte-identical transcripts.
-
All 28 tests pass under both
labandsolution.
How this maps to the real stack
- The message bus / blackboard is AutoGen's group-chat message list and the classic
blackboard architecture: agents coordinate through a shared, inspectable channel rather than
hidden side-effects. Your
statedict is the shared scratchpad every serious multi-agent framework exposes (CrewAI's shared context, LangGraph's graphstate). - The supervisor is LangGraph's supervisor pattern and the OpenAI Agents SDK / Google
ADK orchestrator-worker shape: one agent that routes to specialists. Your
render_state→policy→ worker-name decision is exactly a supervisor node choosing the next edge. - The typed handoff is the OpenAI Agents SDK
handoff: a first-class transfer to a named agent, carrying only the context that agent needs, with the target validated at construction. The least-context choice (pass the task + one prior note, not the whole log) is the production default — context is a budget (Phase 04), and one agent's chatter is another's noise. - The critique-revise loop is reflection / self-refine and the generator-critic pattern (LangGraph's evaluator-optimizer, AutoGen's critic agent). It is Phase 00's verification lever made concrete: a critic that forces one revision turns a 0.9 step into a ~0.99 one.
max_turns/max_roundsare the framework recursion/iteration limits (LangGraph'srecursion_limit, ADK'sLoopAgentmax_iterations) — the guard against a ping-pong loop.
Limits of the miniature. Real agents are LLMs whose routing and critiques are themselves unreliable; real handoffs cross process/network boundaries (an agent can be a remote MCP server, Phase 03); real systems add parallel fan-out (map-reduce over workers), tool use inside each agent (Phase 02), and durability so a crash mid-handoff can resume (Phase 08). The coordination shape — bus, roles, typed handoff, bounded verification loop — is exactly this.
Extensions (your own machine)
- Parallel fan-out. Add
Supervisor.map(task, workers)that hands the same sub-task to N workers concurrently and areducestep that a critic scores — the map-reduce topology. - Debate. Run two workers with opposing prompts and a judge critic that picks a winner (the multi-agent-debate line of work); measure whether debate beats a single critic.
- Cost accounting. Add an
llm_callscounter to the bus and print the cost of the supervisor pipeline vs a single agent doing the whole task — make Phase 00's "more agents multiply cost/latency" tradeoff concrete. - Wire a real model behind the
Policyinterface (one function swap per agent) and run the same supervisor + critique-revise against a live LLM; compare transcripts.
Interview / resume signal
"Built a multi-agent orchestrator from scratch — a deterministic message bus/blackboard, supervisor/worker/critic roles with an injected model, typed least-context handoffs with target validation, and a bounded critique-revise loop — and proved the coordination invariants (routes by state, terminates on
done, never exceeds the turn/round budget) in the trace. Verification via a critic is the reliability lever that buys back Phase 00's0.95^n."