Module 8 · Agents & Multi-Agent Orchestration ⏱ 18 min

Supervisor-Worker Orchestration

By the end of this lesson you will be able to:
  • Decompose a goal into sub-tasks, dispatch each to a worker, and merge the results
  • Explain the five parts of the pattern and where the planning and merging intelligence sits
  • Route sub-tasks to specialised workers and avoid the dispatch-without-merge trap

A single agent hits a ceiling. Give it a big task and it runs out of context window before it finishes, or drowns in a dozen tools it has to choose between on every turn, or simply cannot hold the whole problem in one chain of thought. The fix is not a bigger agent; it is more agents, each doing one focused thing.

The supervisor-worker pattern splits the work along that seam. A supervisor agent looks at the goal, breaks it into sub-tasks, and hands each one to a worker agent that handles exactly that piece. When the workers report back, the supervisor merges their results into a single answer. You scale past one agent's limits by orchestrating several, not by growing one.

flowchart LR
  G["goal"] --> SUP["supervisor"]
  SUP -->|"plan"| SUB["sub-task 1<br/>sub-task 2<br/>sub-task 3"]
  SUB --> W1["worker 1"]
  SUB --> W2["worker 2"]
  SUB --> W3["worker 3"]
  W1 --> MG["merge"]
  W2 --> MG
  W3 --> MG
  MG --> ANS["final answer"]
  style SUP fill:#8b5cf6,color:#fff
  style ANS fill:#166534,color:#fff
The supervisor decomposes the goal into sub-tasks, each worker handles one, and the supervisor merges the results.

The five parts

The pattern has five named parts, and keeping them straight is what makes the code readable. The goal is the whole task. The supervisor is the agent that plans and merges but never executes a sub-task itself. The plan is the decomposition — a list of sub-tasks small enough for one worker each. The workers are the agents (or functions) that execute one sub-task and return a result. The merge is the function that combines worker results into the final answer.

Notice where the intelligence sits. Planning and merging are the supervisor's two real skills; everything in between is delegation. That separation is the whole point: a worker does not need to understand the goal, only its own sub-task, and the supervisor does not need to know how each worker does its job, only what it returns.

The full pipeline: plan turns the goal into sub-tasks, dispatch runs them, merge sums the results. Press Run.
function plan(goal) {
  return goal.pairs.map(([a, b]) => ({ a, b }));
}
function dispatch(workers, subtasks) {
  return subtasks.map((st, i) => workers[i % workers.length](st));
}
function merge(results) {
  return results.reduce((acc, n) => acc + n, 0);
}
function supervise(goal, workers) {
  return merge(dispatch(workers, plan(goal)));
}

const goal = { pairs: [[2, 3], [4, 5], [6, 7]] };
const workers = [({ a, b }) => a + b];
console.log(supervise(goal, workers));

Plan, dispatch, merge

The supervisor runs in three phases. Plan turns the goal into a list of sub-tasks — here, a goal holding pairs of numbers becomes a list of { a, b } objects, one per pair. Dispatch runs each sub-task through a worker and collects the results into an array, preserving order. Merge folds that array down to one value — here, summing the worker outputs.

Each phase is a one-liner over the previous phase's output: merge(dispatch(workers, plan(goal))). That composition is the supervisor in a single expression. Swap the plan for a smarter decomposition, or the merge for a different combination, and you have changed what the system does without touching the workers at all.

flowchart LR
  G["goal"] --> P["plan<br/>decompose"]
  P --> D["dispatch<br/>run each worker"]
  D --> M["merge<br/>combine results"]
  M --> A["answer"]
  style P fill:#8b5cf6,color:#fff
  style D fill:#b45309,color:#fff
  style M fill:#8b5cf6,color:#fff
Three phases: plan decomposes, dispatch runs each worker, merge combines. Plan and merge are the supervisor's jobs.
Dispatch fans the sub-tasks out to workers and returns their results in order. Press Run.
function dispatch(workers, subtasks) {
  return subtasks.map((st, i) => workers[i % workers.length](st));
}

const workers = [({ a, b }) => a + b];
const subtasks = [{ a: 2, b: 3 }, { a: 4, b: 5 }];
console.log(dispatch(workers, subtasks));

Workers can specialise

Nothing says every worker has to be the same. In fact the payoff of the pattern is specialisation: a different worker for each kind of sub-task, each with its own tools and its own prompt. An adder worker knows only addition; a multiplier worker knows only multiplication. Dispatch routes each sub-task to the worker built for it.

The simplest routing is round-robin — sub-task i goes to workers[i % workers.length] — which hands sub-tasks out in turn. Real systems route by type, where a classify step picks the worker, but round-robin is enough to see the mechanism: the supervisor does not care what each worker does internally, only that each sub-task lands on exactly one. Each worker here is itself an agent — a full loop like the one you just built — we are merely stubbing it with a function so the orchestration is the only thing under test.

flowchart LR
  D["dispatch"] -->|"pair 0"| A["adder worker<br/>a + b"]
  D -->|"pair 1"| MUL["multiplier worker<br/>a * b"]
  D -->|"pair 2"| A
  style A fill:#166534,color:#fff
  style MUL fill:#1d4ed8,color:#fff
Specialisation: dispatch routes each sub-task to the worker built for that kind of work.

The supervisor's only tool is delegate

A useful way to see the boundary: from the supervisor's point of view, delegate to a worker is a tool, exactly like call add was in the dispatch loop. The supervisor's loop perceives the goal, decides which sub-task to run next, acts by handing it to a worker, and observes the result — the same state machine as any other agent. The workers are its tools.

That reframing keeps the pattern honest. If you catch your supervisor reaching into a worker's internals, or doing a sub-task itself instead of delegating, it has stopped being a supervisor and become a single bloated agent again — the very thing the pattern exists to avoid. Treat delegate as just another tool in the supervisor's belt, and the whole pattern reduces to a loop you already know how to build.

Exercise

What does this print? Each sub-task is handed to the adder worker, then the results are summed.

const workers = [(st) => st.a + st.b];
const subtasks = [{ a: 2, b: 3 }, { a: 4, b: 5 }];
const results = subtasks.map((st) => workers[0](st));
console.log(results);
console.log(results.reduce((acc, n) => acc + n, 0));
Exercise

Write dispatch(workers, subtasks) that runs each sub-task through a worker and returns an array of results, one per sub-task, in order. Route round-robin: sub-task i goes to workers[i % workers.length].

function dispatch(workers, subtasks) {
  // map each subtask to workers[i % workers.length](subtask)
}
Exercise

Write supervise(goal, workers) that runs the full pipeline: decompose the goal with plan, run the workers with dispatch, and combine the results with merge. All three helpers are already defined for you.

function plan(goal) {
  return goal.pairs.map(([a, b]) => ({ a, b }));
}
function merge(results) {
  return results.reduce((acc, n) => acc + n, 0);
}
function dispatch(workers, subtasks) {
  return subtasks.map((st, i) => workers[i % workers.length](st));
}

function supervise(goal, workers) {
  // decompose with plan, run with dispatch, combine with merge
}
Exercise

This supervisor runs every sub-task through a worker but keeps only the last result, dropping the rest — so for two sub-tasks it returns 9 instead of the combined 14. Use the helpers to dispatch all sub-tasks and then merge them, so nothing is lost.

function plan(goal) { return goal.pairs.map(([a, b]) => ({ a, b })); }
function dispatch(workers, subtasks) { return subtasks.map((st, i) => workers[i % workers.length](st)); }
function merge(results) { return results.reduce((acc, n) => acc + n, 0); }

function supervise(goal, workers) {
  const subtasks = plan(goal);
  let last;
  for (const st of subtasks) {
    last = workers[0](st);   // BUG: keeps only the last result, drops the rest
  }
  return last;
}
Exercise

In a supervisor-worker system, what does the supervisor do that the workers never do?

Recap

  • The supervisor-worker pattern scales past one agent: a supervisor decomposes a goal into sub-tasks, workers execute them, and the supervisor merges the results.
  • The five parts: goal, supervisor (plans and merges), plan (the sub-task list), workers (execute one sub-task each), merge (combine results).
  • The supervisor is merge(dispatch(workers, plan(goal))) — three phases, each a function over the last.
  • Workers can specialise: dispatch routes each sub-task to the worker built for it, often round-robin.
  • Dispatch without merge loses results — the merge is half the supervisor's job, not optional polish.

Next you will make these handoffs safe: passing context between agents without leaking it, so each worker gets exactly what it needs and nothing more.

Checkpoint quiz

A supervisor dispatches three sub-tasks to workers but returns only the last worker's output. What was forgotten?

Why route sub-tasks across several different workers instead of using one?

Go deeper — technical resources