夜深了,老译馆还亮着灯。
一份天大的卷宗压在馆主案头——几百页的盟约,明早就要交。他已经伏案译到大半,案上堆满了批注:哪个词定了哪个译法、哪一处的语气该如何拿捏、前后该怎样呼应,全记在他脑子里、写在纸上的眉批里。
一个人,译不完。可若临时叫几个生手进来,他们什么都不知道——上半本定下的那些规矩,得从头讲一遍,讲完天都亮了。
馆主想了个别的法子。
他没有叫生手。他把自己案上这一刻的全部——译到一半的底稿、每一条眉批、每一个已定的译法——原样誊成几份,一字不差。每个帮手拿到的不是一张白纸,而是一份和他此刻案头完全相同、连墨迹都对得上的卷宗。他们一坐下,就已经「知道」他所知道的一切。
接着,他给每个帮手单开一间静室,室里摆着各自那一份底稿,任他们涂改;但那几大箱沉甸甸的字典辞书,不必各搬一套——每间静室的墙上开一道小门,通向中央那间藏书室,要查就过去查。省了搬运,也省了地方。
他还立了一条铁规,钉在每间静室门上:你是帮手,不许再叫帮手;手上的活,自己干完。
分派停当,他没有守着等。他回到自己案头继续做别的,由着几间静室里的灯各自亮着、各自忙着。哪间先完,就往他案头的收件匣里搁一张字条:这一段译好了,在哪间室、底稿搁在哪。完了的静室,若桌上还留着改动,就原样留着别动;若什么也没添,才把室子收拾干净,腾出来。
后来馆里有人按响了「全体停工」的铃——那是冲着馆主自己这一摊喊的。可那几间静室的灯,没灭。它们本就不接那根铃绳;要叫停它们,得另下一道专门的令。
天亮前,几张字条陆续落进收件匣。馆主把它们拼回那份大卷宗,严丝合缝。
他做的这件事,有个名字:分身(fork 子代理)。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Sub-agent task spawning — implicit fork vs named spawn
Multi-Agent · 多智能体
中文速览 · Quick read
Sub-agent task spawning is how the agent splits one job into several that run on their own. The harness exposes a single Agent tool; calling it with a subagent_type selects a named specialist, but calling it without one — while the fork experiment is active — takes a different path entirely (claude-code/src/tools/AgentTool/AgentTool.tsx:323). That path is an implicit fork: instead of briefing a fresh specialist that has its own system prompt, the harness spins up a worker that inherits the parent's full conversation context and system prompt. The FORK_AGENT definition declares tools: ['*'] and model: 'inherit', so the child's tool pool and model match the parent's exactly (claude-code/src/tools/AgentTool/forkSubagent.ts:60-71). Each spawned worker then runs in isolation — a private git worktree on the filesystem, or a delegated remote environment — and reports back asynchronously rather than blocking the parent. This page reads that spawn-and-isolate machinery: the scribe who copies his whole desk, gives each helper a private room with a doorway to the shared library, and keeps working while their lamps burn.
When the Agent tool is invoked, the harness computes the effective agent type as subagent_type ?? (fork enabled ? undefined : general-purpose); if that resolves to undefined, isForkPath becomes true (claude-code/src/tools/AgentTool/AgentTool.tsx:323). The fork path first guards against recursion. A fork child keeps the Agent tool in its pool (so its tool definitions stay cache-identical to the parent's), which means a fork attempt from inside a fork must be rejected at call time — detected either via querySource or by scanning the message history with isInForkChild, throwing "Fork is not available inside a forked worker" (claude-code/src/tools/AgentTool/AgentTool.tsx:332). The fork experiment itself is gated behind isForkSubagentEnabled(), which is also off in coordinator and non-interactive sessions (claude-code/src/tools/AgentTool/forkSubagent.ts:32-38).
The fork is assembled by buildForkedMessages, which clones the parent's assistant message with every content block intact — thinking, text, and each tool_use — then constructs a single user message carrying one tool_result per tool_use, all filled with the identical placeholder text Fork started — processing in background (claude-code/src/tools/AgentTool/forkSubagent.ts:107-169; the constant at claude-code/src/tools/AgentTool/forkSubagent.ts:93). A per-child directive and a boilerplate "you are a forked worker process" message enforce the rules: ignore the parent's "default to forking" instruction, do not spawn sub-agents, do not converse — execute directly (claude-code/src/tools/AgentTool/forkSubagent.ts:171-198).
Isolation comes next. A worktree run creates a separate git checkout and symlinks heavy shared directories (like node_modules) back to the main repository so each worktree does not duplicate them on disk (claude-code/src/utils/worktree.ts:102-138). When isolation is remote instead, the harness checks eligibility and delegates the entire run to a remote (CCR) environment, returning early (claude-code/src/tools/AgentTool/AgentTool.tsx:435-480).
Finally, spawns run async. With the fork experiment on, all spawns are forced async for one unified <task-notification> interaction model — not only forks (claude-code/src/tools/AgentTool/AgentTool.tsx:555-557). registerAsyncAgent builds the task state; if a parent abort controller is supplied it wraps it via createChildAbortController so cancellation cascades, otherwise it makes a fresh one (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:466-515, with the child wiring at claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486). The work then runs through runAsyncAgentLifecycle, an async generator that streams the child's messages, updating progress and appending to retained tasks as it goes (claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686). On completion it enqueues a notification: XML carrying the task id, status, and summary, plus — if a worktree was used — its path and branch (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256).
Three design choices in this machinery are worth dwelling on.
First, the identical placeholders. Every fork child receives byte-identical tool_result text (claude-code/src/tools/AgentTool/forkSubagent.ts:93) inside a cloned-but-identical parent message (claude-code/src/tools/AgentTool/forkSubagent.ts:107-169). Because the shared prefix is bit-for-bit the same across children, the model's prompt cache can be reused across all of them — the scribe's "even the ink marks line up" copies are not just tidy, they make the filing office's cached paperwork reusable in every room.
Second, isolation that does not cost disk. A worktree is a real, separate working copy, so children can edit files without trampling the parent or each other; symlinking node_modules and similar directories keeps that cheap (claude-code/src/utils/worktree.ts:102-138). And cleanup is conservative: the worktree is removed only when no git changes are detected; if the child left changes behind, the worktree persists so the work is not lost (claude-code/src/tools/AgentTool/AgentTool.tsx:667-678) — the desk with edits on it is left untouched, only the empty desk gets cleared.
Third, the abort wiring is deliberately asymmetric. In-process teammates spawned under a supervisor inherit the parent's abort controller via createChildAbortController, so a parent cancel cascades down to them (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486). But background agents spawned from the main loop are intentionally not linked to the parent's controller — so when the user presses ESC to cancel the main thread, the background work survives; it is killed only by the explicit chat:killAgents (claude-code/src/tools/AgentTool/AgentTool.tsx:694-696). That is the bell that stops the main desk but never reaches the side rooms. Combined with forcing all spawns async (claude-code/src/tools/AgentTool/AgentTool.tsx:555-557) and the task-notification handoff (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256), the result is a fan-out where the parent never blocks, children cannot accidentally recurse (claude-code/src/tools/AgentTool/AgentTool.tsx:332), and finished work returns as a tidy, locatable note.
- The fork decision and recursion guard: claude-code/src/tools/AgentTool/AgentTool.tsx:323, claude-code/src/tools/AgentTool/AgentTool.tsx:332; the experiment gate at claude-code/src/tools/AgentTool/forkSubagent.ts:32-38.
- The fork agent definition and message construction: claude-code/src/tools/AgentTool/forkSubagent.ts:60-71, claude-code/src/tools/AgentTool/forkSubagent.ts:93, claude-code/src/tools/AgentTool/forkSubagent.ts:107-169, claude-code/src/tools/AgentTool/forkSubagent.ts:171-198.
- Isolation — worktree symlinks, remote delegation, conditional cleanup: claude-code/src/utils/worktree.ts:102-138, claude-code/src/tools/AgentTool/AgentTool.tsx:435-480, claude-code/src/tools/AgentTool/AgentTool.tsx:667-678.
- Async lifecycle, abort cascade, and notification: claude-code/src/tools/AgentTool/AgentTool.tsx:555-557, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:466-515, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486, claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256, claude-code/src/tools/AgentTool/AgentTool.tsx:694-696.
classDiagram
class AgentTool {
+effectiveType resolvedType
+isForkPath bool
+recursionGuard()
}
class forkSubagent {
+buildForkedMessages()
+placeholder Fork started
}
class Isolation {
<<worktree or remote>>
+worktreeSymlinks()
+remoteDelegateCCR()
}
class LocalAgentTask {
+registerAsyncAgent()
+createChildAbortController()
}
class TaskNotification {
+id status summary
+worktreePathBranch
}
AgentTool ..> forkSubagent : fork path
AgentTool ..> Isolation : worktree or remote
AgentTool ..> LocalAgentTask : spawn async
LocalAgentTask ..> TaskNotification : on complete 读法:AgentTool 算出有效类型,undefined 即 fork 路径并禁止 fork 内再 fork
(AgentTool.tsx:323,332)。buildForkedMessages 克隆父消息、给每个 tool_use 填同一占位
「Fork started」(forkSubagent.ts:107-169)。隔离:worktree 软链 node_modules(worktree.ts:102)
或远程 CCR 委派(AgentTool.tsx:435)。spawn 全异步,createChildAbortController 让取消级联
(LocalAgentTask.tsx:466-515),完成发 <task-notification>(:197-256)。
一次 Agent 调用,只有一个字段决定走哪条路:subagent_type。
留它空白(隐式分叉 fork),还是点名一个具名子代理「explore」(全新生手)?
旁边那道 fork experiment 闸:空白只有在闸开时才分叉,否则回落到 general-purpose。
再选一种隔离方式,按 Spawn,看那道分隔膜两侧——分叉是把整个案头原样誊过去,
具名只递一张白纸加一句话。
设好两个旋钮,按 Spawn,看子代理怎么诞生。
省略 subagent_type 且 fork 闸开着,才走 isForkPath:子代理继承父代理的
完整上下文与 renderedSystemPrompt,assistant 消息被原样克隆,每个 tool_use 都配上
字节一致的占位 Fork started — processing in background——一致到能复用提示缓存。
具名子代理则另起炉灶:自己的系统提示、空白记录、只有你递的那一句话。worktree 用完有改动才留、
干净才删;后台 fork 不接父代理的中止信号,ESC 杀不掉它,只有 chat:killAgents 能。
What it is
Sub-agent task spawning is how the agent splits one job into several that run on their own. The harness exposes a single Agent tool; calling it with a subagent_type selects a named specialist, but calling it without one — while the fork experiment is active — takes a different path entirely (claude-code/src/tools/AgentTool/AgentTool.tsx:323). That path is an implicit fork: instead of briefing a fresh specialist that has its own system prompt, the harness spins up a worker that inherits the parent's full conversation context and system prompt. The FORK_AGENT definition declares tools: ['*'] and model: 'inherit', so the child's tool pool and model match the parent's exactly (claude-code/src/tools/AgentTool/forkSubagent.ts:60-71). Each spawned worker then runs in isolation — a private git worktree on the filesystem, or a delegated remote environment — and reports back asynchronously rather than blocking the parent. This page reads that spawn-and-isolate machinery: the scribe who copies his whole desk, gives each helper a private room with a doorway to the shared library, and keeps working while their lamps burn.
How it works
When the Agent tool is invoked, the harness computes the effective agent type as subagent_type ?? (fork enabled ? undefined : general-purpose); if that resolves to undefined, isForkPath becomes true (claude-code/src/tools/AgentTool/AgentTool.tsx:323). The fork path first guards against recursion. A fork child keeps the Agent tool in its pool (so its tool definitions stay cache-identical to the parent's), which means a fork attempt from inside a fork must be rejected at call time — detected either via querySource or by scanning the message history with isInForkChild, throwing "Fork is not available inside a forked worker" (claude-code/src/tools/AgentTool/AgentTool.tsx:332). The fork experiment itself is gated behind isForkSubagentEnabled(), which is also off in coordinator and non-interactive sessions (claude-code/src/tools/AgentTool/forkSubagent.ts:32-38).
The fork is assembled by buildForkedMessages, which clones the parent's assistant message with every content block intact — thinking, text, and each tool_use — then constructs a single user message carrying one tool_result per tool_use, all filled with the identical placeholder text Fork started — processing in background (claude-code/src/tools/AgentTool/forkSubagent.ts:107-169; the constant at claude-code/src/tools/AgentTool/forkSubagent.ts:93). A per-child directive and a boilerplate "you are a forked worker process" message enforce the rules: ignore the parent's "default to forking" instruction, do not spawn sub-agents, do not converse — execute directly (claude-code/src/tools/AgentTool/forkSubagent.ts:171-198).
Isolation comes next. A worktree run creates a separate git checkout and symlinks heavy shared directories (like node_modules) back to the main repository so each worktree does not duplicate them on disk (claude-code/src/utils/worktree.ts:102-138). When isolation is remote instead, the harness checks eligibility and delegates the entire run to a remote (CCR) environment, returning early (claude-code/src/tools/AgentTool/AgentTool.tsx:435-480).
Finally, spawns run async. With the fork experiment on, all spawns are forced async for one unified <task-notification> interaction model — not only forks (claude-code/src/tools/AgentTool/AgentTool.tsx:555-557). registerAsyncAgent builds the task state; if a parent abort controller is supplied it wraps it via createChildAbortController so cancellation cascades, otherwise it makes a fresh one (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:466-515, with the child wiring at claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486). The work then runs through runAsyncAgentLifecycle, an async generator that streams the child's messages, updating progress and appending to retained tasks as it goes (claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686). On completion it enqueues a notification: XML carrying the task id, status, and summary, plus — if a worktree was used — its path and branch (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256).
Why it matters
Three design choices in this machinery are worth dwelling on.
First, the identical placeholders. Every fork child receives byte-identical tool_result text (claude-code/src/tools/AgentTool/forkSubagent.ts:93) inside a cloned-but-identical parent message (claude-code/src/tools/AgentTool/forkSubagent.ts:107-169). Because the shared prefix is bit-for-bit the same across children, the model's prompt cache can be reused across all of them — the scribe's "even the ink marks line up" copies are not just tidy, they make the filing office's cached paperwork reusable in every room.
Second, isolation that does not cost disk. A worktree is a real, separate working copy, so children can edit files without trampling the parent or each other; symlinking node_modules and similar directories keeps that cheap (claude-code/src/utils/worktree.ts:102-138). And cleanup is conservative: the worktree is removed only when no git changes are detected; if the child left changes behind, the worktree persists so the work is not lost (claude-code/src/tools/AgentTool/AgentTool.tsx:667-678) — the desk with edits on it is left untouched, only the empty desk gets cleared.
Third, the abort wiring is deliberately asymmetric. In-process teammates spawned under a supervisor inherit the parent's abort controller via createChildAbortController, so a parent cancel cascades down to them (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486). But background agents spawned from the main loop are intentionally not linked to the parent's controller — so when the user presses ESC to cancel the main thread, the background work survives; it is killed only by the explicit chat:killAgents (claude-code/src/tools/AgentTool/AgentTool.tsx:694-696). That is the bell that stops the main desk but never reaches the side rooms. Combined with forcing all spawns async (claude-code/src/tools/AgentTool/AgentTool.tsx:555-557) and the task-notification handoff (claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256), the result is a fan-out where the parent never blocks, children cannot accidentally recurse (claude-code/src/tools/AgentTool/AgentTool.tsx:332), and finished work returns as a tidy, locatable note.
Read the source
- The fork decision and recursion guard: claude-code/src/tools/AgentTool/AgentTool.tsx:323, claude-code/src/tools/AgentTool/AgentTool.tsx:332; the experiment gate at claude-code/src/tools/AgentTool/forkSubagent.ts:32-38.
- The fork agent definition and message construction: claude-code/src/tools/AgentTool/forkSubagent.ts:60-71, claude-code/src/tools/AgentTool/forkSubagent.ts:93, claude-code/src/tools/AgentTool/forkSubagent.ts:107-169, claude-code/src/tools/AgentTool/forkSubagent.ts:171-198.
- Isolation — worktree symlinks, remote delegation, conditional cleanup: claude-code/src/utils/worktree.ts:102-138, claude-code/src/tools/AgentTool/AgentTool.tsx:435-480, claude-code/src/tools/AgentTool/AgentTool.tsx:667-678.
- Async lifecycle, abort cascade, and notification: claude-code/src/tools/AgentTool/AgentTool.tsx:555-557, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:466-515, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486, claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686, claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256, claude-code/src/tools/AgentTool/AgentTool.tsx:694-696.
来源 · Source citations
- [1]
claude-code/src/tools/AgentTool/AgentTool.tsx:323 - [2]
claude-code/src/tools/AgentTool/AgentTool.tsx:332 - [3]
claude-code/src/tools/AgentTool/AgentTool.tsx:435-480 - [4]
claude-code/src/tools/AgentTool/AgentTool.tsx:555-557 - [5]
claude-code/src/tools/AgentTool/AgentTool.tsx:667-678 - [6]
claude-code/src/tools/AgentTool/AgentTool.tsx:694-696 - [7]
claude-code/src/tools/AgentTool/forkSubagent.ts:32-38 - [8]
claude-code/src/tools/AgentTool/forkSubagent.ts:60-71 - [9]
claude-code/src/tools/AgentTool/forkSubagent.ts:93 - [10]
claude-code/src/tools/AgentTool/forkSubagent.ts:107-169 - [11]
claude-code/src/tools/AgentTool/forkSubagent.ts:171-198 - [12]
claude-code/src/utils/worktree.ts:102-138 - [13]
claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:197-256 - [14]
claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:466-515 - [15]
claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx:486 - [16]
claude-code/src/tools/AgentTool/agentToolUtils.ts:508-686