老顾问的桌子只有那么大。一个棘手的问题递上来,往往要翻遍半架子的旧档、跑一整趟资料室才答得上;可那些翻检的草稿、读废的卷宗、来回比对的便签,若全堆回他自己案头,桌子转眼就被填满——真正要用的那句结论,反倒被压在纸山最底下找不着了。
于是他学了一手。遇到这种"得读一大堆才能答一句"的活,他不再自己埋头翻,而是掐个诀,唤出一个分身。分身领的不是他这张堆满批注的乱桌,而是另搬来的一张干干净净的空桌,上面只写着一句话:要查的题目。分身抱着这张桌子出门,进资料室,爱翻多少翻多少——那一屋子的草稿、废纸、比对便签,全摊在门外那张桌上,跟老顾问的案头半点不沾。
出门前,门口老管事按规矩交代两句:你只准查、只准读,不准动主人正在改的稿子;你自己也是个分身,不许再掐诀唤你自己的分身,手上的活自己干完。而这两条不是嘴上说说——管事就守在门口,分身真伸手去够那管改稿的笔,当场被按住:"这东西不归你用。"光把笔收进抽屉、指望分身瞧不见,是拦不住的:手一伸还是够得着,真正的界线,得有人守在门口挡下。
忙活半天,分身回来了。它没把那一屋子草稿搬进来,只递上薄薄一页:结论在此。老顾问接过这一页,往自己案头轻轻一搁——他的桌子,只多了一行字,却多了一整趟资料室的收获。
这套"派一个分身、领干净空桌出门、把草稿全留在门外、只带一页结论回来"的办法,有个名字:子代理(sub-agent)——一个 spawn_agent 工具,用一份空的 messages 另起一个全新的内层循环,末了只把一个答案回传给父循环。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Stage 5 — sub-agents: a spawn_agent tool that runs a fresh inner loop and returns one answer
Build From Scratch · 从零构建
中文速览 · Quick read
Stage 4 gave the agent a way to survive a long conversation: when the window fills, fold old turns into a summary. But compaction is lossy — it grinds away detail to buy room. The deeper problem is what fills the window. A single "go read the codebase and tell me what it does" can pull twenty files, each hundreds of lines, into the transcript — and every later turn now re-sends all of it. The findings you actually wanted are buried under the raw material it took to find them.
Stage 5 fixes this at the source. It adds one tool — spawn_agent — that hands a self-contained research task to a sub-agent: a second run of the very same loop, but with its own empty messages array and a restricted tool set. The sub-agent reads whatever it needs on its own clean desk, and returns one string — its final answer — which crosses back into the parent as a single tool_result. The parent's context grows by that one line instead of by every file the child had to read. Exploration stops being something the main agent pays for in permanent context.
The first move is to stop treating the loop as a one-shot main(). In tutorial/05-subagents/loop.ts the while body from stage 4 is pulled out into a reusable function, parameterized by who's asking and how long they get:
export interface RunAgentLoopOptions {
model: string;
messages: Anthropic.MessageParam[]; // mutated in place (turns pushed, compaction spliced) so the caller's reference stays live
tools: Anthropic.Tool[];
execute: (name: string, input: Record<string, unknown>) => Promise<{ content: string; isError: boolean }>;
label: string; // printed ahead of the meter so nested loops are distinguishable
maxTurns?: number; // default 20; hit it and the loop stops itself
}
export async function runAgentLoop(client: Anthropic, opts: RunAgentLoopOptions): Promise<string> {
const { model, messages, tools, execute, label, maxTurns = 20 } = opts;
...
return lastText;
}
Nothing about compaction's mechanics or sequential tool dispatch changes — only how compaction's result is stored: the loop now splices it into the array in place instead of reassigning, so a caller's reference to messages stays live. The same body now runs twice in the same process: once for the top-level agent, once for a sub-agent. The only differences are the three parameters messages, tools, and execute. That is the whole trick: a sub-agent is not a new kind of machine, it is this same loop called again with a clean transcript.
Next, the parent needs a tool the model can call to trigger a spawn. In tutorial/05-subagents/agent.ts:
const spawnAgentSpec: Anthropic.Tool = {
name: 'spawn_agent',
description:
'Delegate a self-contained research task to a fresh sub-agent with its own empty context. It can read files and run read-only commands, and returns only its final answer. Use it to explore without filling your own context.',
input_schema: {
type: 'object',
properties: { task: { type: 'string', description: 'the complete task, with all context the sub-agent needs' } },
required: ['task'],
},
};
When the model calls it, runSubagent builds the child. This is where the clean desk and the door-rule live:
async function runSubagent(task: string): Promise<string> {
// A CLEAN DESK: fresh messages array, restricted tools (no write_file, no
// spawn_agent — a sub-agent cannot spawn its own children).
const subTools = [toolRegistry.read_file!.spec, toolRegistry.bash!.spec];
// The model only SEES read_file+bash, but enforce it at dispatch too —
// a boundary that exists only in the tool list is a suggestion, not a boundary.
const SUB_ALLOWED = new Set(['read_file', 'bash']);
const label = ' [sub]'; // prefixes this child's 📏 meter and ⚙ verdict lines
return runAgentLoop(client, {
model: MODEL,
messages: [{ role: 'user', content: task }], // empty except the task
tools: subTools, // narrowed pool
execute: async (name, input) =>
SUB_ALLOWED.has(name)
? gatedExecute(name, input, label)
: { content: `invalid input: tool "${name}" is not available to sub-agents`, isError: true },
label,
maxTurns: 10,
});
}
Read that execute closely — it is the load-bearing line of the whole stage. There are two ways the child's tools are restricted, and only one of them is a real boundary. Handing subTools (just read_file + bash) to the model omits write_file and spawn_agent from what it sees. But that alone is only a suggestion: a model that hallucinated a write_file call, or a compromised transcript that smuggled one in, would sail straight through if the list were the only guard. So the execute dispatcher re-checks the name against SUB_ALLOWED and rejects anything outside the set with an is_error result — the child literally cannot invoke write_file even if it names it. A boundary that exists only in the tool list is a suggestion; a boundary in the dispatcher is enforcement. That is also why spawn_agent is absent from SUB_ALLOWED: a sub-agent cannot spawn its own children, so the fan-out is exactly one level deep and can never recurse.
Finally the parent wires it in. parentExecute intercepts the spawn_agent name, validates the input, runs the child, and — the payoff — measures its own context before and after. Note where the branch sits: before gatedExecute, so spawn_agent itself never passes through decide() — spawning bypasses the gate by design (it is dispatch, not a side effect), while every tool call the child then makes is still gated one by one:
async function parentExecute(name, input) {
if (name === 'spawn_agent') {
if (typeof input.task !== 'string' || input.task.trim() === '') {
return { content: 'invalid input: task must be a non-empty string', isError: true };
}
// Dispatch, not a side effect: spawn_agent never passes decide() —
// every tool call the child makes still does.
console.log(` ↳ spawn_agent(${JSON.stringify(input)}) — dispatching sub-agent`);
console.log(` 👁 parent context before spawn: ~${estimateTokens(messages)} tokens`);
const result = await runSubagent(input.task);
console.log(` 👁 parent context after spawn: ~${estimateTokens(messages)} tokens (child's exploration never entered it)`);
return { content: result, isError: false }; // only the answer crosses back
}
return gatedExecute(name, input);
}
The child's whole transcript — every read_file, every bash, every intermediate turn — lives and dies inside runSubagent's local messages. What returns to the parent is a single string, appended as one tool_result. The parent's token count barely moves.
ANTHROPIC_API_KEY=… bun run tutorial/05-subagents/agent.ts "What does this project do? Read around, then tell me."
An abridged transcript — watch the two meters. The parent's stays flat across the spawn; the [sub] meter churns and climbs on its own. Note the first spawn_agent line: it is a plain dispatch log (↳), not a ⚙ … — allow verdict — parentExecute routes spawn_agent before decide(), so spawning bypasses the gate by design; the child's side effects remain gated. And only 📏/⚙ lines carry the [sub] label — the model's text always prints bare (console.log(b.text)), so the child's final answer is the unprefixed line just before the after-spawn 👁:
📏 ~40 tokens of 8000
I'll delegate the exploration to keep my own context clean.
↳ spawn_agent({"task":"Read the tutorial files and summarize what this project builds"}) — dispatching sub-agent
👁 parent context before spawn: ~120 tokens
[sub] 📏 ~60 tokens of 8000
[sub] ⚙ bash({"command":"ls tutorial"}) — allow
[sub] 📏 ~340 tokens of 8000
[sub] ⚙ read_file({"path":"tutorial/05-subagents/agent.ts"}) — allow
[sub] 📏 ~1120 tokens of 8000
[sub] ⚙ read_file({"path":"tutorial/05-subagents/loop.ts"}) — allow
[sub] 📏 ~1980 tokens of 8000
It's a step-by-step tutorial that builds a mini coding agent…
👁 parent context after spawn: ~120 tokens (child's exploration never entered it)
📏 ~180 tokens of 8000
This project is a staged tutorial that assembles a coding agent from a bare loop up…
The child burned ~2000 tokens reading three files. The parent's two 👁 readings are identical — ~120 tokens on both sides of the spawn, because nothing is appended to its transcript while the child runs — and its next 📏 tick reads ~180: it absorbed the child's answer (one tool_result), not the child's reading. Try it against stage 04 (tutorial/04-context/agent.ts) with the same prompt and watch the single context meter balloon instead.
The tutorial's ~80 lines are a scale model of Claude Code's AgentTool. Map the pieces (deep dives: /stories/multiagent/1/, /stories/multiagent/2/):
- "Route this call to a sub-agent."
parentExecute'sif (name === 'spawn_agent')branch is the toy of the harness's spawn-routing decision — the harness computes an effective agent type and, when it resolves to the fork path, setsisForkPathand selects a sub-agent to run (claude-code/src/tools/AgentTool/AgentTool.tsx:323). - "A sub-agent cannot spawn its own children" enforced at dispatch, not by omission. This is the exact idea behind
SUB_ALLOWEDand the missingspawn_agent. In the real harness a fork child deliberately keeps the Agent tool in its pool (so its tool definitions stay cache-identical to the parent's), which means the recursion has to be rejected at call time — whenisInForkChild()returns true, the guard throws "Fork is not available inside a forked worker. Complete your task directly using your tools." (claude-code/src/tools/AgentTool/AgentTool.tsx:332-334). The tool is visible; the boundary lives in the dispatcher — precisely the SUB_ALLOWED lesson. - The child's tool pool and turn budget are a definition, not an accident.
subTools+maxTurns: 10are the toy of theFORK_AGENTdefinition, which pinstools,maxTurns, andmodelfor the spawned worker (claude-code/src/tools/AgentTool/forkSubagent.ts:60-71). - "Only read, don't converse, return one report." The
spawn_agentdescription ("returns only its final answer") compresses the real child directive, which orders the worker: do not spawn sub-agents, do not converse or editorialize, use tools silently, then report structured facts once and stop (claude-code/src/tools/AgentTool/forkSubagent.ts:171-198). - The child runs its own full loop.
runSubagentcallingrunAgentLoopagain mirrorsrunAsyncAgentLifecycle, which drives a spawned agent from start to terminal notification over its own message stream (claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686). - Only the answer crosses back. Returning
{ content: result }as onetool_resultis the toy ofenqueueAgentNotification, which hands the parent a terminal notification carrying the child'sstatusandfinalMessage(claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256).
The tutorial omits what makes the real thing industrial: the real spawn inherits the parent's full context (an implicit fork) rather than starting empty, runs in an isolated git worktree, executes asynchronously so the parent never blocks, and shares the prompt cache via byte-identical placeholders. The tutorial keeps only the load-bearing skeleton — fresh loop, restricted-and-enforced tools, one answer back.
Stage 04 has no way to delegate. Ask it "read around this repo and tell me what it does," and it explores in its own context: every read_file result, every bash dump, every dead-end file it opened stays in the one transcript, re-sent on every subsequent turn. Two failure modes follow. First, the window fills fast, so compaction fires early and often — and compaction is lossy, so it starts folding away the very findings the exploration produced, sometimes before the model has used them. Second, even short of the limit, signal-to-noise collapses: the answer is a needle in a haystack of raw file contents the model now has to reason around every turn. Stage 5's sub-agent is the fix — it moves the haystack to a desk outside the room and lets only the needle back in.
classDiagram
class runAgentLoop {
<<one loop, run twice>>
+messages
+tools
+execute()
+maxTurns 20
+returns finalText
}
class RunAgentLoopOptions {
+model
+messages
+tools
+execute()
+label
+maxTurns
}
class spawnAgentSpec {
<<Anthropic.Tool>>
+name spawn_agent
+input task
}
class parentExecute {
+route spawn_agent
+tokens before after
+gatedExecute fallback
}
class runSubagent {
<<clean desk>>
+messages fresh task
+subTools read_file bash
+maxTurns 10
+returns one answer
}
class SUB_ALLOWED {
<<dispatch guard>>
+Set read_file bash
+reject others is_error
}
runAgentLoop ..> RunAgentLoopOptions : configured by
parentExecute ..> runAgentLoop : parent loop
parentExecute ..> spawnAgentSpec : exposes
parentExecute ..> runSubagent : on spawn_agent
runSubagent ..> runAgentLoop : fresh inner loop
runSubagent ..> SUB_ALLOWED : enforce at dispatch 读法:Stage 4 的 while 体被抽成一个 runAgentLoop,
同一个循环在同一进程里跑两次——父代理一次、子代理一次(tutorial/05-subagents/loop.ts)。
父循环的 parentExecute 拦下 spawn_agent,交给 runSubagent:
一份空的 messages、只含 read_file+bash 的受限工具集,另起一个全新内层循环。
工具限制在 派发层用 SUB_ALLOWED 强制,而不只是从工具列表里省略——
只存在于列表里的界线只是建议,存在于派发器里才是界线(对照 AgentTool.tsx:332 在调用时抛错拒绝递归 fork)。
子代理只把一个答案作为单个 tool_result 回传,父代理的上下文只多一行
(对照 forkSubagent.ts:60-71,171-198、agentToolUtils.ts:508-686、LocalAgentTask.tsx:197-256)。
父代理的桌子已经堆满批注。要答一个"读一大堆才能答一句"的问题,它不自己翻—— 它派一个分身:一张干净的空桌、一份受限工具(只准 read_file+bash)、另起一个全新的内层循环。 看右边分身的桌子越堆越满、左边父代理的表纹丝不动;忙完,只有一页结论越过边界回到父代理, 分身那一屋子草稿就地蒸发。
先选一个任务,再「派一个分身」。
这正是 Stage 5 的子代理:spawn_agent 用一份空 messages 另起一个全新的
runAgentLoop,受限工具集在派发层用 SUB_ALLOWED 强制——
只存在于工具列表里的界线只是建议,存在于派发器里才是界线。子代理只把一个答案
作为单个 tool_result 回传,父代理的上下文只多一行;它读过的那一屋子文件从不进入父上下文。
The goal
Stage 4 gave the agent a way to survive a long conversation: when the window fills, fold old turns into a summary. But compaction is lossy — it grinds away detail to buy room. The deeper problem is what fills the window. A single "go read the codebase and tell me what it does" can pull twenty files, each hundreds of lines, into the transcript — and every later turn now re-sends all of it. The findings you actually wanted are buried under the raw material it took to find them.
Stage 5 fixes this at the source. It adds one tool — spawn_agent — that hands a self-contained research task to a sub-agent: a second run of the very same loop, but with its own empty messages array and a restricted tool set. The sub-agent reads whatever it needs on its own clean desk, and returns one string — its final answer — which crosses back into the parent as a single tool_result. The parent's context grows by that one line instead of by every file the child had to read. Exploration stops being something the main agent pays for in permanent context.
Build it
The first move is to stop treating the loop as a one-shot main(). In tutorial/05-subagents/loop.ts the while body from stage 4 is pulled out into a reusable function, parameterized by who's asking and how long they get:
export interface RunAgentLoopOptions {
model: string;
messages: Anthropic.MessageParam[]; // mutated in place (turns pushed, compaction spliced) so the caller's reference stays live
tools: Anthropic.Tool[];
execute: (name: string, input: Record<string, unknown>) => Promise<{ content: string; isError: boolean }>;
label: string; // printed ahead of the meter so nested loops are distinguishable
maxTurns?: number; // default 20; hit it and the loop stops itself
}
export async function runAgentLoop(client: Anthropic, opts: RunAgentLoopOptions): Promise<string> {
const { model, messages, tools, execute, label, maxTurns = 20 } = opts;
...
return lastText;
}
Nothing about compaction's mechanics or sequential tool dispatch changes — only how compaction's result is stored: the loop now splices it into the array in place instead of reassigning, so a caller's reference to messages stays live. The same body now runs twice in the same process: once for the top-level agent, once for a sub-agent. The only differences are the three parameters messages, tools, and execute. That is the whole trick: a sub-agent is not a new kind of machine, it is this same loop called again with a clean transcript.
Next, the parent needs a tool the model can call to trigger a spawn. In tutorial/05-subagents/agent.ts:
const spawnAgentSpec: Anthropic.Tool = {
name: 'spawn_agent',
description:
'Delegate a self-contained research task to a fresh sub-agent with its own empty context. It can read files and run read-only commands, and returns only its final answer. Use it to explore without filling your own context.',
input_schema: {
type: 'object',
properties: { task: { type: 'string', description: 'the complete task, with all context the sub-agent needs' } },
required: ['task'],
},
};
When the model calls it, runSubagent builds the child. This is where the clean desk and the door-rule live:
async function runSubagent(task: string): Promise<string> {
// A CLEAN DESK: fresh messages array, restricted tools (no write_file, no
// spawn_agent — a sub-agent cannot spawn its own children).
const subTools = [toolRegistry.read_file!.spec, toolRegistry.bash!.spec];
// The model only SEES read_file+bash, but enforce it at dispatch too —
// a boundary that exists only in the tool list is a suggestion, not a boundary.
const SUB_ALLOWED = new Set(['read_file', 'bash']);
const label = ' [sub]'; // prefixes this child's 📏 meter and ⚙ verdict lines
return runAgentLoop(client, {
model: MODEL,
messages: [{ role: 'user', content: task }], // empty except the task
tools: subTools, // narrowed pool
execute: async (name, input) =>
SUB_ALLOWED.has(name)
? gatedExecute(name, input, label)
: { content: `invalid input: tool "${name}" is not available to sub-agents`, isError: true },
label,
maxTurns: 10,
});
}
Read that execute closely — it is the load-bearing line of the whole stage. There are two ways the child's tools are restricted, and only one of them is a real boundary. Handing subTools (just read_file + bash) to the model omits write_file and spawn_agent from what it sees. But that alone is only a suggestion: a model that hallucinated a write_file call, or a compromised transcript that smuggled one in, would sail straight through if the list were the only guard. So the execute dispatcher re-checks the name against SUB_ALLOWED and rejects anything outside the set with an is_error result — the child literally cannot invoke write_file even if it names it. A boundary that exists only in the tool list is a suggestion; a boundary in the dispatcher is enforcement. That is also why spawn_agent is absent from SUB_ALLOWED: a sub-agent cannot spawn its own children, so the fan-out is exactly one level deep and can never recurse.
Finally the parent wires it in. parentExecute intercepts the spawn_agent name, validates the input, runs the child, and — the payoff — measures its own context before and after. Note where the branch sits: before gatedExecute, so spawn_agent itself never passes through decide() — spawning bypasses the gate by design (it is dispatch, not a side effect), while every tool call the child then makes is still gated one by one:
async function parentExecute(name, input) {
if (name === 'spawn_agent') {
if (typeof input.task !== 'string' || input.task.trim() === '') {
return { content: 'invalid input: task must be a non-empty string', isError: true };
}
// Dispatch, not a side effect: spawn_agent never passes decide() —
// every tool call the child makes still does.
console.log(` ↳ spawn_agent(${JSON.stringify(input)}) — dispatching sub-agent`);
console.log(` 👁 parent context before spawn: ~${estimateTokens(messages)} tokens`);
const result = await runSubagent(input.task);
console.log(` 👁 parent context after spawn: ~${estimateTokens(messages)} tokens (child's exploration never entered it)`);
return { content: result, isError: false }; // only the answer crosses back
}
return gatedExecute(name, input);
}
The child's whole transcript — every read_file, every bash, every intermediate turn — lives and dies inside runSubagent's local messages. What returns to the parent is a single string, appended as one tool_result. The parent's token count barely moves.
Run it
ANTHROPIC_API_KEY=… bun run tutorial/05-subagents/agent.ts "What does this project do? Read around, then tell me."
An abridged transcript — watch the two meters. The parent's stays flat across the spawn; the [sub] meter churns and climbs on its own. Note the first spawn_agent line: it is a plain dispatch log (↳), not a ⚙ … — allow verdict — parentExecute routes spawn_agent before decide(), so spawning bypasses the gate by design; the child's side effects remain gated. And only 📏/⚙ lines carry the [sub] label — the model's text always prints bare (console.log(b.text)), so the child's final answer is the unprefixed line just before the after-spawn 👁:
📏 ~40 tokens of 8000
I'll delegate the exploration to keep my own context clean.
↳ spawn_agent({"task":"Read the tutorial files and summarize what this project builds"}) — dispatching sub-agent
👁 parent context before spawn: ~120 tokens
[sub] 📏 ~60 tokens of 8000
[sub] ⚙ bash({"command":"ls tutorial"}) — allow
[sub] 📏 ~340 tokens of 8000
[sub] ⚙ read_file({"path":"tutorial/05-subagents/agent.ts"}) — allow
[sub] 📏 ~1120 tokens of 8000
[sub] ⚙ read_file({"path":"tutorial/05-subagents/loop.ts"}) — allow
[sub] 📏 ~1980 tokens of 8000
It's a step-by-step tutorial that builds a mini coding agent…
👁 parent context after spawn: ~120 tokens (child's exploration never entered it)
📏 ~180 tokens of 8000
This project is a staged tutorial that assembles a coding agent from a bare loop up…
The child burned ~2000 tokens reading three files. The parent's two 👁 readings are identical — ~120 tokens on both sides of the spawn, because nothing is appended to its transcript while the child runs — and its next 📏 tick reads ~180: it absorbed the child's answer (one tool_result), not the child's reading. Try it against stage 04 (tutorial/04-context/agent.ts) with the same prompt and watch the single context meter balloon instead.
对照 the real one
The tutorial's ~80 lines are a scale model of Claude Code's AgentTool. Map the pieces (deep dives: /stories/multiagent/1/, /stories/multiagent/2/):
- "Route this call to a sub-agent."
parentExecute'sif (name === 'spawn_agent')branch is the toy of the harness's spawn-routing decision — the harness computes an effective agent type and, when it resolves to the fork path, setsisForkPathand selects a sub-agent to run (claude-code/src/tools/AgentTool/AgentTool.tsx:323). - "A sub-agent cannot spawn its own children" enforced at dispatch, not by omission. This is the exact idea behind
SUB_ALLOWEDand the missingspawn_agent. In the real harness a fork child deliberately keeps the Agent tool in its pool (so its tool definitions stay cache-identical to the parent's), which means the recursion has to be rejected at call time — whenisInForkChild()returns true, the guard throws "Fork is not available inside a forked worker. Complete your task directly using your tools." (claude-code/src/tools/AgentTool/AgentTool.tsx:332-334). The tool is visible; the boundary lives in the dispatcher — precisely the SUB_ALLOWED lesson. - The child's tool pool and turn budget are a definition, not an accident.
subTools+maxTurns: 10are the toy of theFORK_AGENTdefinition, which pinstools,maxTurns, andmodelfor the spawned worker (claude-code/src/tools/AgentTool/forkSubagent.ts:60-71). - "Only read, don't converse, return one report." The
spawn_agentdescription ("returns only its final answer") compresses the real child directive, which orders the worker: do not spawn sub-agents, do not converse or editorialize, use tools silently, then report structured facts once and stop (claude-code/src/tools/AgentTool/forkSubagent.ts:171-198). - The child runs its own full loop.
runSubagentcallingrunAgentLoopagain mirrorsrunAsyncAgentLifecycle, which drives a spawned agent from start to terminal notification over its own message stream (claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686). - Only the answer crosses back. Returning
{ content: result }as onetool_resultis the toy ofenqueueAgentNotification, which hands the parent a terminal notification carrying the child'sstatusandfinalMessage(claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256).
The tutorial omits what makes the real thing industrial: the real spawn inherits the parent's full context (an implicit fork) rather than starting empty, runs in an isolated git worktree, executes asynchronously so the parent never blocks, and shares the prompt cache via byte-identical placeholders. The tutorial keeps only the load-bearing skeleton — fresh loop, restricted-and-enforced tools, one answer back.
What breaks without it
Stage 04 has no way to delegate. Ask it "read around this repo and tell me what it does," and it explores in its own context: every read_file result, every bash dump, every dead-end file it opened stays in the one transcript, re-sent on every subsequent turn. Two failure modes follow. First, the window fills fast, so compaction fires early and often — and compaction is lossy, so it starts folding away the very findings the exploration produced, sometimes before the model has used them. Second, even short of the limit, signal-to-noise collapses: the answer is a needle in a haystack of raw file contents the model now has to reason around every turn. Stage 5's sub-agent is the fix — it moves the haystack to a desk outside the room and lets only the needle back in.
来源 · Source citations
- [1]
claude-code/src/tools/AgentTool/AgentTool.tsx:323 - [2]
claude-code/src/tools/AgentTool/AgentTool.tsx:332-334 - [3]
claude-code/src/tools/AgentTool/forkSubagent.ts:60-71 - [4]
claude-code/src/tools/AgentTool/forkSubagent.ts:171-198 - [5]
claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686 - [6]
claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256