跳转到内容

Fork Mechanism

fork mechanism 是 Claude Code 创建 sub-agent 的方式,其特点是 继承父级完整对话 context。与从空白状态启动的普通 agent 派生不同,被 fork 的 agent 携带父级的完整消息历史,能够以充分的情境感知继续工作。

graph LR
    subgraph Regular Spawn
        A1[Parent Agent] -->|"Agent(subagent_type='Explore')"| B1[Child Agent]
        B1 ---|"Fresh context<br/>+ agent system prompt"| C1[Independent execution]
    end
    subgraph Fork Spawn
        A2[Parent Agent] -->|"Agent(no subagent_type)"| B2[Forked Child]
        B2 ---|"Full parent context<br/>+ parent system prompt<br/>+ fork directive"| C2[Context-aware execution]
    end

关键区别:当 Agent tool 调用中省略 subagent_type 参数时(且 fork 实验处于激活状态),Claude Code 会触发隐式 fork,而非普通的 agent 派生。

fork mechanism 受 FORK_SUBAGENT feature flag 控制,并有严格的互斥规则:

src/tools/AgentTool/forkSubagent.ts
export function isForkSubagentEnabled(): boolean {
if (feature('FORK_SUBAGENT')) {
if (isCoordinatorMode()) return false // coordinator 拥有自己的编排模型
if (getIsNonInteractiveSession()) return false // 仅限 SDK
return true
}
return false
}

fork 模式与 coordinator 模式互斥——coordinator 已有其自己的、基于 worker agent 的委托模型。

fork 路径使用一个从未注册到内置 agent 列表中的合成 agent 定义:

src/tools/AgentTool/forkSubagent.ts
export const FORK_AGENT = {
agentType: FORK_SUBAGENT_TYPE, // 'fork'
tools: ['*'], // 与父级相同的完整 tool 池,以保证 cache 前缀一致
maxTurns: 200,
model: 'inherit', // 继承父级模型,保证 context 长度一致
permissionMode: 'bubble', // permission 提示上浮至父级终端
source: 'built-in',
getSystemPrompt: () => '', // 未使用——父级已渲染的 prompt 字节直接透传
} satisfies BuiltInAgentDefinition

关键设计挑战是 prompt cache 共享:从同一父级 turn 派生的所有 fork 子进程必须产生字节完全相同的 API 请求前缀,只有每个子进程的专属指令才允许不同。

// src/tools/AgentTool/forkSubagent.ts — buildForkedMessages
export function buildForkedMessages(
directive: string,
assistantMessage: AssistantMessage,
): MessageType[] {
// 1. 克隆 assistant 消息(所有 tool_use、thinking、text block)
const fullAssistantMessage: AssistantMessage = {
...assistantMessage,
uuid: randomUUID(),
message: { ...assistantMessage.message, content: [...assistantMessage.message.content] },
}
// 2. 收集所有 tool_use block
const toolUseBlocks = assistantMessage.message.content.filter(
(block): block is BetaToolUseBlock => block.type === 'tool_use',
)
// 3. 为每个 tool_use 构建相同的占位符结果
const toolResultBlocks = toolUseBlocks.map(block => ({
type: 'tool_result' as const,
tool_use_id: block.id,
content: [{ type: 'text' as const, text: FORK_PLACEHOLDER_RESULT }],
}))
// 4. 将每个子进程的专属指令作为唯一变化的文本追加
const toolResultMessage = createUserMessage({
content: [
...toolResultBlocks, // 所有 fork 完全相同
{ type: 'text' as const, text: buildChildMessage(directive) }, // 每个子进程各不相同
],
})
return [fullAssistantMessage, toolResultMessage]
}

产生的消息结构:

[...history, assistant(all_tool_uses), user(placeholder_results..., directive)]
↑ 所有 fork 相同 ↑ 每个子进程各异

由于 fork 子进程之间只有最后一个文本 block 不同,这一设计最大化了 cache 命中率。

每个 fork 子进程通过 buildChildMessage() 接收严格的指令:

src/tools/AgentTool/forkSubagent.ts
export function buildChildMessage(directive: string): string {
return `<fork-boilerplate>
STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.
RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — that's for the parent.
You ARE the fork. Do NOT spawn sub-agents; execute directly.
2. Do NOT converse, ask questions, or suggest next steps
3. USE your tools directly: Bash, Read, Write, etc.
4. If you modify files, commit your changes before reporting.
5. Keep your report under 500 words.
6. Your response MUST begin with "Scope:".
</fork-boilerplate>
fork-directive: ${directive}`
}

输出格式为结构化纯文本:

Scope: <回显分配的范围>
Result: <关键发现>
Key files: <相关文件路径>
Files changed: <列表及 commit hash>
Issues: <如有问题则列出>

fork 子进程在其 tool 池中保留了 Agent tool(以保证 tool 定义的 cache 一致性),因此必须在调用时阻断递归 fork:

src/tools/AgentTool/forkSubagent.ts
export function isInForkChild(messages: MessageType[]): boolean {
return messages.some(m => {
if (m.type !== 'user') return false
const content = m.message.content
if (!Array.isArray(content)) return false
return content.some(
block => block.type === 'text' && block.text.includes('<fork-boilerplate>'),
)
})
}

该保护机制扫描对话历史中的 <fork-boilerplate> 标签——如果存在,说明当前 agent 已经是一个 fork 子进程,不能再次 fork。

当 fork 在隔离的 git worktree 中运行时,会向子进程的 context 注入一条特殊说明,告知其路径转换规则:

export function buildWorktreeNotice(parentCwd: string, worktreeCwd: string): string {
return `You've inherited the conversation context above from a parent agent
working in ${parentCwd}. You are operating in an isolated git worktree at
${worktreeCwd} — same repository, same relative file structure, separate
working copy. Paths in the inherited context refer to the parent's working
directory; translate them to your worktree root. Re-read files before editing
if the parent may have modified them since they appear in the context.`
}
sequenceDiagram
    participant P as Parent Agent
    participant F as Fork System
    participant C1 as Fork Child 1
    participant C2 as Fork Child 2

    P->>F: Agent(prompt="directive1") [no subagent_type]
    P->>F: Agent(prompt="directive2") [no subagent_type]
    F->>F: buildForkedMessages(directive1, parentAssistantMsg)
    F->>F: buildForkedMessages(directive2, parentAssistantMsg)
    F->>C1: [history + identical placeholders + directive1]
    F->>C2: [history + identical placeholders + directive2]
    Note over C1,C2: Cache hit on shared prefix
    C1-->>P: Scope: ... Result: ...
    C2-->>P: Scope: ... Result: ...

两个子进程共享相同的 API cache 前缀——唯一的区别是最后的指令文本 block。当父级同时派生多个 fork 时(并行研究任务的常见场景),这一特性尤为有价值。

以下所有条件均满足时,fork 路径才会激活:

  1. FORK_SUBAGENT feature flag 已启用
  2. 非 coordinator 模式
  3. 非非交互式(SDK)session
  4. Agent tool 调用中省略了 subagent_type 参数
  5. 当前 agent 不是已有的 fork 子进程

激活后,所有 agent 派生(包括指定 subagent_type 的派生)均在后台运行,采用统一的 <task-notification> 交互模型——fork 实验将后台执行作为其设计的组成部分。