Build your own Claude Code
Subagents·25 min

Parallel dispatch

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage parallel-dispatch --watch

Isolated context buys you something the parent can't do for itself: fan-out. If you need to explore three areas of a codebase, you don't have to do it serially in one window — you spawn three subagents, each in its own context, and collect their final messages. Claude Code does exactly this when it dispatches several Task calls at once.

This step builds the dispatcher: take N agent specs, run each as a subagent, and return their results in order. Each agent carries its own definition, prompt, sandbox, and recorded model — they don't share state, which is the whole reason isolation makes parallelism safe.

Because every subagent has a fresh context and its own execute, there's nothing to coordinate between them — no shared message history, no shared file handles. That independence is what lets one subagent fail without aborting the batch: a single agent's error is its own result, bounded to its slot, while the others still return. You collect a result per agent, in input order, and hand the parent the full list.

What to build

Your task

Dispatch N subagents and collect in order

Wire up the parallel subcommand in program.ts to dispatch a list of agents. For each entry in C.agents, build a sandbox from its files, parse its agentMd with parseAgentDef, and run runSubagent(def, prompt, makeCassetteModel(cassette), makeExecute(tools), C.allTools). Push { name, finalText, trajectory } for each — preserving input order — and print { results }. Reuse the runSubagent you built last step; the dispatcher is just iteration over isolated runs.

src/subagents.ts
// In program.ts, the `parallel` case dispatches a batch:
//
//   const results = [];
//   for (const a of C.agents) {
//     const dir = mkSandbox(a.files);
//     const tools = new Tools(dir);
//     const def = parseAgentDef(a.agentMd);
//     const r = await runSubagent(
//       def, a.prompt ?? 'go',
//       makeCassetteModel(a.cassette), makeExecute(tools), C.allTools,
//     );
//     results.push({ name: def.name, finalText: r.finalText, trajectory: r.trajectory });
//   }
//   return print({ results });
//
// Each subagent is fully isolated — its own sandbox, model, and allowlist.
// Collect results IN ORDER; a failing agent stays bounded to its own slot.
Order is part of the answer

Even when subagents run concurrently (Promise.all in the real client), the results must come back in the order the agents were given — the parent referenced them positionally. The grader's fixture dispatches Explore then Plan; the results array must be [Explore, Plan], never reordered by which finished first. Each result is { name, finalText, trajectory } — note there's no blockedTools here, just the three batch fields.

Isolation is what makes a failure bounded

A subagent that errors out resolves to its own result — a bounded failure in its slot — instead of throwing and aborting the whole batch. Because nothing is shared between agents, one failing run can't corrupt another's context or files. That's the payoff of everything you built this chapter: isolated contexts compose into safe parallelism.

How it's graded

process + cassette. The grader runs program.ts parallel with a list of agents, each carrying its own agentMd, prompt, files, and cassette. It builds a fresh sandbox + cassette model per agent, dispatches each through runSubagent, and prints { results }. Your output must match the reference exactly: every agent's finalText and trajectory collected, in input order. Explore greps a.txt then says "done explore"; Plan says "done plan" with an empty trajectory.

How the grader runs it
./your_program.sh parallel
# agents: [ Explore (greps a.txt → "done explore"),
#           Plan   (no tools     → "done plan") ]
# -> { "results": [
#        { "name":"Explore", "finalText":"done explore", "trajectory":[ {Grep…} ] },
#        { "name":"Plan",    "finalText":"done plan",    "trajectory":[] } ] }