Sprint 3 · Orchestration

Multi-agent orchestration: supervisor–worker patterns, handoffs, and the cost of coordination

Build

Concept

From one agent to several

A single agent running a ReAct loop — reason, act, observe, repeat — covers a large fraction of useful agentic tasks. But some problems resist a single-agent approach: the task is too large for one context window, different subtasks benefit from specialist framing, or independent sub-problems can be solved in parallel rather than sequentially. These are the conditions under which a multi-agent architecture becomes worth its overhead. They are also the conditions under which it is easy to add agents for the wrong reasons — complexity theatre, not genuine necessity.

The core move in multi-agent orchestration is task decomposition: a supervisor agent receives a high-level goal and breaks it into subtasks, each assigned to a worker agent with a focused, narrow context. Workers return results; the supervisor collects, validates, and synthesises them into a final output. The conversation between supervisor and workers can be implemented as direct function calls, message queues, or structured state objects, but the essential structure is always the same: one planner, multiple executors, one aggregator.

Handoffs between agents require a structured message contract. Each handoff should carry at minimum: the subtask description, any relevant context from prior steps, and the format the supervisor expects back. Implicit handoffs — passing raw strings between agents with no envelope — are the most common source of downstream parse failures and silent correctness degradation. State synchronisation across workers is the other common failure point: workers that share state must write to an agreed schema, or the supervisor's aggregation step will encounter incompatible formats.

The ReAct loop and Reflexion

Each worker in a multi-agent system typically runs its own ReAct loop (Yao et al., 2022): it reasons about its subtask, selects a tool or action, observes the result, and iterates until it has a satisfactory answer or exhausts its budget. The supervisor may run a simpler loop — decompose, dispatch, collect — or its own ReAct loop if planning requires iterative refinement.

When a worker fails, the question is whether to retry blindly or to reflect. Reflexion (Shinn et al., 2023) introduces verbal self-reflection as the retry mechanism: after a failed attempt, the agent produces a natural-language diagnosis of what went wrong and why, stores that reflection in its working memory, and retries with the reflection as additional context. The retry is therefore informed — the agent is not repeating the same action, it is acting on an explicit self-critique. This is a meaningful improvement over simple retries and a meaningful improvement over the standard tool-error handler that passes a raw exception back to the model with no interpretation.

Reflexion belongs at the worker level. A worker that fails a subtask reflects and retries. The supervisor's job is to decide, after a worker exhausts its reflection budget, whether to reassign the subtask, accept a partial result, or abort. That policy decision belongs in the supervisor's control flow, not in the worker's self-reflection loop — mixing the two produces agents that can neither reflect cleanly nor abort gracefully. For more on how Anthropic structures multi-agent research systems, see the multi-agent engineering post.

When multi-agent is worth it

Multi-agent orchestration has real costs: additional LLM calls (latency and token spend), more complex state management, harder debugging, and new failure modes at every handoff boundary. These costs are only justified when one or more of the following conditions hold:

If none of these conditions apply, a single agent is the correct choice. The framework of choice for wiring multi-agent graphs in Python is LangGraph, which provides a typed state graph, node-level control flow, and built-in support for supervisor–worker patterns — but the architectural decisions above apply regardless of whether you use LangGraph, a custom message bus, or direct function calls.

The Golden Rule

Never ask an LLM to do something a deterministic function can do reliably. Applied to multi-agent: don't add agents or orchestration where a deterministic pipeline or a single agent suffices. Task routing by keyword, result validation by schema check, and subtask aggregation by string concatenation are all deterministic operations. Run them in code. Reserve LLM calls for the parts that genuinely require language understanding — subtask framing, result interpretation, and synthesis. Every agent in the graph that could be replaced by a function is overhead you are paying per-call, not a design feature.

Why orchestration logic belongs in code, not in a prompt

A common mistake is to put the supervisor's dispatch logic inside a single large prompt: "Given this goal, break it into tasks and assign each to the appropriate worker." This works in demos and fails in production for the same reason that business rules fail in a vector database — authority and priority are not encoded. The model may assign a subtask to the wrong worker, skip a subtask that seems redundant, or invent a worker type that does not exist. Orchestration is control flow. Control flow belongs in code. The supervisor's prompt should describe how to frame a subtask and what a good subtask result looks like — not which workers exist or in what order to call them. Those decisions are deterministic given the goal's structure, and deterministic decisions belong in code.

Build

What to build this sprint

The reference implementation at projects/sprint-3/agent.py is a complete, runnable supervisor–worker system. A supervisor receives a high-level goal, decomposes it into three semantically independent subtasks, dispatches each to a specialist worker, collects results, and prints a structured trace. Read every line before building your own.

  1. Run the reference agent. From the repo root:
    bash
    $ python agentic-engineering/projects/sprint-3/agent.py
    --- supervisor: decomposing goal ---
      goal: "Analyse a codebase for quality, security, and documentation"
      [supervisor] subtask 1 → worker:code-quality
      [supervisor] subtask 2 → worker:security
      [supervisor] subtask 3 → worker:docs
    
    --- worker:code-quality running ---
      [worker] received: check code quality
      [worker] result: 3 functions lack docstrings; cyclomatic complexity ok
      ok: code-quality result is non-empty
    
    --- worker:security running ---
      [worker] received: scan for security issues
      [worker] result: no hardcoded secrets; one use of eval() detected
      ok: security result is non-empty
    
    --- worker:docs running ---
      [worker] received: check documentation coverage
      [worker] result: README present; 2 public functions missing docstrings
      ok: docs result is non-empty
    
    --- supervisor: collecting results ---
      ok: all 3 workers returned results
      ok: trace has 3 entries
      ok: no worker result is empty
    
    --- tradeoff note ---
      This multi-agent design is justified here because the three analysis tasks
      are semantically independent and benefit from specialist framing. A single
      agent with a combined prompt would produce less focused analysis and
      complicate debugging. The orchestration cost is worth the separation of concerns.
      ok: tradeoff note printed
    
    All demos passed.
    Confirm the supervisor printed all three worker results and the trace has exactly three entries before moving on.
  2. Read the decomposition logic. Open projects/sprint-3/agent.py and find the function that splits the goal into subtasks. Note that subtask routing is a deterministic lookup — the supervisor maps goal keywords to worker names in code, not by asking a model. Confirm you can trace exactly which line dispatches each subtask and what happens if a worker returns an empty string.
  3. Add a fourth worker. Add a worker:testing specialist that receives "check test coverage" and returns a plausible stub result. Update the supervisor's decomposition to include this fourth subtask. Run the agent again and confirm the trace now has four entries and all four workers appear in the output.
  4. Implement a Reflexion retry. Modify one worker so that on its first call it returns an empty string (simulating a failure), then on retry it returns a real result. In the supervisor's collection step, detect the empty result, log a reflection note ("worker returned empty — retrying with narrower scope"), and call the worker a second time. Confirm the retry produces a non-empty result and the trace records both the failure and the retry.
  5. Write your tradeoff note. In projects/sprint-3/README.md, answer: for the goal you used in this sprint, is the multi-agent design justified? Apply the four conditions from the concept section. If you extended the agent to a fifth worker, does that worker strengthen or weaken the justification? Write one sentence identifying a simpler single-agent design that would cover two of the four subtasks without loss of quality — and explain why you might or might not prefer it.
When you are done

Your agent.py runs, a supervisor splits a goal into subtasks across at least two specialist workers, and the output includes a printed trace showing the decomposition and which worker handled what. Your README contains one note explaining when this multi-agent design is justified versus a single agent. That is the Sprint 3 ship condition. The checklist below is injected automatically from the course config.