8 / 10
Guide Book Β· Chapter 8

Subagents & delegation

When a second agent earns its cost, what it can and cannot pass back, and the orchestration patterns that actually hold up.

A subagent is a separate agent with its own context window, its own tools and its own loop. The parent sends a task and receives a result. Nothing else crosses the boundary.

text
β”Œβ”€β”€ parent agent ─────────────────────────────┐
β”‚  context: the conversation, the plan        β”‚
β”‚                                             β”‚
β”‚   spawn ──► β”Œβ”€β”€ subagent ──────────────┐    β”‚
β”‚             β”‚ fresh context            β”‚    β”‚
β”‚             β”‚ reads 40 files           β”‚    β”‚
β”‚             β”‚ returns 200 tokens ──────┼─────
β”‚             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  context: +200 tokens, not +40 files        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The real reason: context isolation

typescript
// What a search costs in the parent's window vs. in a subagent's.
const filesRead = 40
const tokensPerFile = 1200
const summaryTokens = 250

const inline = filesRead * tokensPerFile
console.log(`inline:   ${inline} tokens of file contents, in the parent's context forever`)
console.log(`subagent: ${summaryTokens} tokens of findings β€” the reading happened elsewhere`)
console.log(`saved:    ${inline - summaryTokens} tokens, on every subsequent turn`)

What crosses the boundary

typescript
type Spawn = { task: string; context: string }
type Report = { summary: string }

// The parent sends text. The subagent returns text. That is all.
function subagent(spawn: Spawn): Report {
  // Fresh window: it knows only what the spawn message said.
  const knows = [spawn.task, spawn.context]
  return { summary: `Found 3 call sites. Knew ${knows.length} things at the start.` }
}

const report = subagent({
  task: "Find every call site of `legacyAuth()` and report the file and line of each.",
  context: "TypeScript monorepo. Source under packages/*/src. Ignore tests and dist.",
})

console.log(report.summary)
console.log("\nThe subagent could NOT see:")
for (const missing of [
  "the conversation so far",
  "why the parent wants this",
  "files the parent already read",
  "decisions made earlier in the session",
]) {
  console.log(`  β€’ ${missing}`)
}

⚠️ A subagent starts cold. Everything it needs must be in the spawn message: the task, the constraints, the conventions, the definition of done. "Look into that thing we discussed" produces a report about nothing β€” the subagent has never seen the discussion.

Writing a spawn message

typescript
const poor = "Check the auth code."

const good = `
Find every place in this repo where a session is created or invalidated.

Repository: TypeScript monorepo, source under packages/*/src. Ignore dist/ and *.test.ts.
Relevant names: createSession, destroySession, SessionStore, req.session.

Report, and nothing else:
- a table of file:line β†’ which operation
- any place that creates a session without a matching invalidation path
- any place that touches the session cookie directly

Do not modify any file. Do not fix anything you find.
`

console.log(`poor: ${poor.length} chars β€” the subagent must guess almost everything`)
console.log(`good: ${good.trim().length} chars β€” task, scope, vocabulary, output shape, limits`)
console.log(good.trim())

Five things a spawn message needs: the task, the scope, the vocabulary to search for, the exact shape of the report, and the prohibitions.

When delegation pays

typescript
type Task = { name: string; reads: number; writes: boolean; independent: boolean }

function shouldDelegate(t: Task): { verdict: string; why: string } {
  if (t.writes) return { verdict: "no", why: "coordinating writes across agents invites conflicts" }
  if (t.reads < 5) return { verdict: "no", why: "a cold start costs more than the reading saves" }
  if (!t.independent) return { verdict: "no", why: "it needs the parent's context to make sense" }
  return { verdict: "yes", why: "broad reading, no writes, self-contained" }
}

const tasks: Task[] = [
  { name: "find all call sites of X", reads: 40, writes: false, independent: true },
  { name: "read one config file", reads: 1, writes: false, independent: true },
  { name: "refactor the auth module", reads: 12, writes: true, independent: false },
  { name: "audit 30 files for a pattern", reads: 30, writes: false, independent: true },
  { name: "continue the fix we were discussing", reads: 8, writes: true, independent: false },
]

for (const t of tasks) {
  const r = shouldDelegate(t)
  console.log(`${r.verdict.padEnd(4)} ${t.name.padEnd(36)} ${r.why}`)
}

Parallel fan-out

typescript
type Finding = { area: string; issues: string[] }

async function audit(area: string): Promise<Finding> {
  await new Promise((r) => setTimeout(r, 10))
  return { area, issues: [`${area}: one thing worth a look`] }
}

const areas = ["auth", "billing", "notifications", "search"]

const started = Date.now()
const findings = await Promise.all(areas.map(audit)) // concurrent, not sequential
const elapsed = Date.now() - started

console.log(`${findings.length} areas audited in ${elapsed < 50 ? "one round" : "several rounds"}`)
for (const f of findings) console.log(`  ${f.area.padEnd(14)} ${f.issues[0]}`)

// The parent's job: synthesise, not re-read.
console.log(`\nparent receives ${findings.length} summaries, not ${areas.length * 30} files`)

Independent, read-only, similarly-shaped tasks are the ideal fan-out. Anything with shared state or ordering between the parts is not.

Cheaper models for cheap work

typescript
const assignments = [
  { work: "orchestrating, deciding, writing code", model: "claude-opus-5", effort: "high / xhigh" },
  { work: "reading many files for a pattern", model: "claude-haiku-4-5", effort: "low" },
  { work: "summarising a long document", model: "claude-sonnet-5", effort: "low" },
  { work: "judging outputs against a rubric", model: "claude-sonnet-5", effort: "medium" },
] as const

for (const a of assignments) {
  console.log(`${a.model.padEnd(20)} ${a.effort.padEnd(12)} ${a.work}`)
}

console.log("\nCaveat: caches are model-scoped, so a mixed fleet forfeits cache reuse between them.")
console.log("Measure the one-model-at-lower-effort option before building a cascade.")

Orchestration patterns

typescript
const patterns = [
  {
    name: "fan-out / gather",
    shape: "parent β†’ N independent subagents β†’ parent synthesises",
    fits: "audits, multi-source research, per-file analysis",
  },
  {
    name: "pipeline",
    shape: "research β†’ plan β†’ implement β†’ review, each with its own context",
    fits: "work where each stage's output is small and the input is large",
  },
  {
    name: "explorer / worker",
    shape: "a cheap subagent finds the relevant 3 files; the parent does the work",
    fits: "large repos where locating is expensive and editing is not",
  },
  {
    name: "critic",
    shape: "a fresh subagent reviews the parent's diff with no memory of writing it",
    fits: "catching what the author cannot see β€” the value IS the lack of context",
  },
]

for (const p of patterns) {
  console.log(`${p.name}\n  ${p.shape}\n  fits: ${p.fits}\n`)
}

The critic pattern is worth singling out: a reviewer with no memory of having written the code has no attachment to it, and catches the things the author's context makes invisible.

When not to delegate

typescript
const antipatterns = [
  ["delegating a two-file edit", "the cold start costs more than the work"],
  ["delegating without a report shape", "you get prose you then have to parse"],
  ["several subagents writing to the same files", "conflicts nobody is watching for"],
  ["a subagent that needs the conversation", "it cannot have it β€” it starts cold"],
  ["delegating because it feels thorough", "a fan-out is not a substitute for thinking"],
] as const

for (const [what, why] of antipatterns) {
  console.log(`βœ— ${what.padEnd(42)} ${why}`)
}

πŸ’‘ Tip: Default to doing the work inline. Delegate when there is a specific, nameable saving β€” this reading would otherwise sit in my context forever, or these four things are genuinely independent. "It seems like a big task" is not a reason; a big task usually needs one agent with the whole picture.

Reference: Subagents in the Claude Code documentation.

main.ts
Loading editor…
No output yet
Press Run or βŒ˜β†΅ to compile and run your code.

Search

Jump to a lesson or a past run