ESC

Type to search the knowledge base.

Subagents and Delegation

A subagent gets a fresh context, a narrow job, and a report back. Use it to keep the parent window small. Isolate blast radius.

advanced5 min read
  • agents
  • subagents
  • delegation

Exploring a 4k-file repo inside the parent thread is how you blow the window and still miss the bug. Subagents exist to stop that. You spawn a child with a fresh context, a narrow brief, a tight tool list, and a timeout. It returns a structured report. The parent never sees the raw file dumps. That is delegation, not a committee.

A handoff takes the microphone. A subagent sends a memo. If the user should keep talking to the same policy, spawn. If a different department should speak, hand off. Most “multi-agent” demos wanted the memo and built the committee.

Key takeaways

  • A subagent starts empty: new messages, narrow goal, constrained tools, hard timeout.
  • The parent consumes a typed report, not a transcript of grep.
  • Isolation is for tokens and blast radius. A search child does not get deploy.
  • Bound the fan-out. Five children on one user click is a fork bomb with an API key.
  • Fail closed: child timeout or schema miss becomes an error the parent can explain.

Subagents get a fresh window

The parent is already carrying the user, the policy, and the plan. If it also carries every read_file of src/**, the question falls into the middle and the next tool call gets worse. Spawn when the work is:

  • Wide search (“find the tax calculator”)
  • Throwaway exploration (you will discard the traces)
  • A different permission set (read-only auditor)

Do not spawn for “format this string.” That is a function.

import { z } from 'zod';

const Report = z.object({
  summary: z.string().max(1_500),
  files: z.array(z.object({ path: z.string(), reason: z.string() })).max(12),
  quotes: z.array(z.object({ path: z.string(), excerpt: z.string().max(500) })).max(8),
  unresolved: z.array(z.string()).max(8),
});

type Report = z.infer<typeof Report>;

type ChildSpec = {
  id: string;
  goal: string;
  tools: Array<'glob' | 'grep' | 'read'>;
  timeoutMs: number;
  maxSteps: number;
};

async function runChild(
  spec: ChildSpec,
  opts: { cwd: string; reportSchema: typeof Report },
): Promise<unknown> {
  const ac = new AbortController();
  const timer = setTimeout(() => ac.abort(), spec.timeoutMs);
  try {
    return await completeJson({
      system: `You may use: ${spec.tools.join(', ')}. Return JSON that matches the report schema. No edits.`,
      user: spec.goal,
      cwd: opts.cwd,
      tools: spec.tools,
      maxSteps: spec.maxSteps,
      schema: opts.reportSchema,
      signal: ac.signal,
    });
  } finally {
    clearTimeout(timer);
  }
}

export async function delegateSearch(parentGoal: string, repoRoot: string): Promise<Report> {
  const spec: ChildSpec = {
    id: crypto.randomUUID(),
    goal: `Find files and quotes that answer: ${parentGoal}. Do not edit. Do not run the shell.`,
    tools: ['glob', 'grep', 'read'],
    timeoutMs: 45_000,
    maxSteps: 8,
  };

  const raw = await runChild(spec, { cwd: repoRoot, reportSchema: Report });
  const parsed = Report.safeParse(raw);
  if (!parsed.success) {
    return {
      summary: 'child returned invalid report',
      files: [],
      quotes: [],
      unresolved: [parsed.error.message],
    };
  }
  return parsed.data;
}

The parent then reasons over files and quotes. It may read one path itself. It does not ingest the child’s 40-step diary. That diary belongs in tracing, not in the next prompt.

Spawn in a fixed order so you can debug it:

  1. Write the child’s goal as a sentence the parent could show a user.
  2. Copy only the ids and quotes the child needs. No parent system prompt dump.
  3. Attach the allowlist and maxSteps in code.
  4. Run with a timeout. On abort, return unresolved.
  5. Parse the report with a schema. Invalid JSON is a failed child, not prose to “interpret.”
  6. Merge into the parent as one user or tool message labeled with spec.id.

If step 2 copies the whole parent transcript, you did not isolate anything. You opened a second bill with the same haystack.

Constrain the child

Tools are the blast radius. A research child with bash is a junior with prod creds. Encode the allowlist in the runtime, not in the child’s personality paragraph.

Child Tools Must not
Search glob, grep, read write, network, deploy
Test run_tests, read patch outside src/ and tests/
Review read, diff merge, push

Claude Code’s CLI lets you define custom agents with their own prompt, tool list, and model. The useful part of that API is the constraint, not the extra name. Copy the constraint even if you roll a 40-line loop.

Timeouts are part of the contract. Use AbortController on the child’s HTTP and shell. A hung grep should not hang the parent turn. See AbortController. If the child times out, the parent gets unresolved: ['timeout'] and can ask the user, not retry forever.

Fan-out without a fork bomb

Parallel children are tempting: one per directory, one per service. Bound them.

  1. Max children per user request (start with 1–3).
  2. Shared step budget: parentSteps + sum(childSteps) <= cap.
  3. Dedup goals. Two children grepping the same string is a wasted window.
  4. Merge reports in code (Map by path). Do not ask a fourth model to “synthesize the team.”

Context engineering on the parent is now easy: pin the user goal, attach two reports, drop the rest. Multi-agent systems that skip this packing step are just several bills.

The child sandbox should be stricter than the parent. If you already built a coding-agent loop, run children inside it with a subset of tools. Do not give the child the parent’s memory store. Isolated context means isolated writes.

Interview answer (out loud)

“A subagent is a spawned loop with a fresh context, a narrow goal, a tight tool allowlist, and a timeout. It returns a structured report. The parent never sees raw file dumps, which keeps the window small and the blast radius small. I bound fan-out and fail closed on schema errors. I hand off only when a different speaker should own the user conversation.”

Further reading (primary sources)

Related guides