Claude Code 源码深度讲解 · 多 Agent 协作

Multi-Agent 系统完整运行原理

Claude Code 不是单一的"Claude 主循环",而是一个可以孵化、隔离、协作 N 个 agent 的分布式 LLM 运行时。 本文从 AgentDefinition 数据结构,到 runAgent 的 ~30 步流水线,再到 swarm 跨进程团队,逐层拆解每一个设计细节。

01概览:为什么需要多 agent

主 agent 一个人干所有活会撞上 4 个根本问题:上下文窗口、工具数量、模型成本、并发隔离。多 agent 系统是这些问题的集中解法。

1.1 多 agent 解决的 4 类问题

问题主 agent 单独干的代价多 agent 的解法
上下文污染 读 50 个文件做调研 → 200k context 全是噪声,后续推理跑偏 派子 agent 调研,只把 200 字 summary 带回来
工具范围扩散 所有工具 schema 都加载 → ~30k token prompt 浪费 子 agent 用 disallowedTools / 自定义 tools 列表收窄
模型成本失衡 主 Opus 跑 grep / find → 杀鸡用牛刀 Explore 用 Haiku,Verification 用 Opus,主 agent 决策
无法并行 任务串行 → 一个 5 分钟测试卡住整个会话 run_in_background 派多个 agent 并发 + 通知回流

1.2 一句话定义每种 agent

  • built-inbuilt-in agent — 源码里硬编码,如 general-purpose、Explore、Plan
  • customcustom agent — 用户在 ~/.claude/agents/<name>.md 写的 markdown,带 frontmatter
  • pluginplugin agent — 插件包提供的 agent,带 plugin 元信息
  • forkfork agent — 实验性:不传 subagent_type 时触发,继承父 agent 全部上下文 + system prompt
  • teammateteammate — swarm 模式下的"长驻协作进程",独立 tmux/iterm pane,文件邮箱通信

1.3 一组关键术语

术语含义
subagent由主 agent 通过 AgentTool 派出的"一次性"子 agent,任务完成即销毁
fork子 agent 的特殊形态:继承父的全部上下文,共享 prompt cache
teammate长期存在的并行 agent,每个跑在自己的进程/pane 里,通过邮箱协作
leaderswarm 模式下创建团队、分派任务的主 agent
worktreegit worktree 创建的独立工作目录,子 agent 改的文件不影响父
async / background子 agent 立即返回,后台跑,完成后通过 task-notification 通知父
sync / foreground子 agent 跑完才返回,期间父 agent 阻塞等待

02系统架构总览

把多 agent 系统拆成 5 层,从工具调用到通知回流,每一层都有明确职责。

多 Agent 系统 5 层架构 第 1 层 · Agent 发现与注册 built-in 硬编码 + 用户 .md 文件 + 插件包扫描 → AgentDefinitionsResult.activeAgents getActiveAgentsFromList:同名优先级 plugin > user > project > flag > managed > built-in loadAgentsDir.ts · builtInAgents.ts 第 2 层 · AgentTool 调用入口 Zod schema 校验 · MCP 等待与过滤 · 权限规则 · 路径选择(teammate / fork / normal) isolation 解析 · cwd 覆盖 · run_in_background 决策 · agentName 注册 输出形态:async_launched | completed | teammate_spawned | remote_launched AgentTool.tsx 第 3 层 · runAgent 子 agent 流水线 MCP 服务器初始化 · skills 预加载 · system prompt 组装 · 权限模式覆盖 SubagentStart hook · frontmatter hook 注册 · contentReplacementState 克隆 tool 过滤(filterToolsForAgent + resolveAgentTools) · transcript 持久化 → 调用 query() 进入 agent 自己的 query loop runAgent.ts 第 4a 层 · 本地子 agent 执行 同步:阻塞父 agent 等结果 异步:registerAsyncAgent + LocalAgentTask runAsyncAgentLifecycle 驱动 tasks/LocalAgentTask 第 4b 层 · Swarm 跨进程协作 tmux / iterm2 / windows-terminal 后端 in-process(同进程独立 context) spawnTeammate · TeamFile 注册 utils/swarm/backends/ 第 5 层 · 通信回流 同步 → tool_result · 异步 → enqueueAgentNotification <task-notification> · swarm → mailbox
多 agent 系统 5 层数据流:发现 → 入口 → 流水线 → 执行 → 通信

03Agent 类型四大分类

所有 agent 都满足同一个接口 AgentDefinition,但来源不同,加载逻辑、信任级别、可写性都不同。

3.1 四种来源对照

类型位置系统提示信任级别
built-in 源码 built-in/*.ts 动态 getSystemPrompt({ toolUseContext }) admin-trusted
custom ~/.claude/agents/*.md 或项目级 静态闭包 () => promptText 跟 source 走(user/project/managed/flag)
plugin 插件包 plugin/*/agents/*.md 静态闭包,带 plugin 元信息 admin-trusted (受插件签名)
fork (合成) 仅运行时存在(FORK_AGENT) 占位 → 实际用父的 system prompt 跟父 agent 一致

3.2 内置 agent 完整清单(builtInAgents.ts)

agentType用途默认模型典型工具
general-purpose 通用调研 + 多步任务 跟父(getDefaultSubagentModel) ['*'] 全部
Explore 只读代码探索(快) ant: inherit / 外部: haiku 排除 Edit/Write/NotebookEdit/Agent/ExitPlanMode
Plan 实现规划 不能 Edit/Write
verifier 验证主 agent 完成质量(PASS/PARTIAL/FAIL)
statusline-setup 配置 statusline 设置 Read + Edit(局限于 settings.json)
claude-code-guide 回答关于 Claude Code 本身的问题 Bash + Read + WebFetch + WebSearch
合并优先级(loadAgentsDir.ts:193)
当用户和系统 agent 重名时,按这个 Map.set 顺序覆盖,后面的覆盖前面的:
built-in < plugin < user < project < flag < policy
即用户在 ~/.claude/agents/general-purpose.md 写一个同名 agent,会覆盖内置的 general-purpose。 policy(企业策略)的优先级最高,可强制覆盖一切。

04AgentDefinition 数据结构

理解 AgentDefinition 是理解整个 agent 系统的钥匙。所有 frontmatter 字段最终都会映射到这里。

4.1 完整 schema

type BaseAgentDefinition = {
  agentType: string                            // 唯一名称,subagent_type 引用
  whenToUse: string                            // 给主 agent 看的"何时用"
  tools?: string[]                              // 白名单,['*'] = 全部
  disallowedTools?: string[]                    // 黑名单
  skills?: string[]                             // 启动时预加载的 skill
  mcpServers?: AgentMcpServerSpec[]             // agent 私有 MCP 服务器
  hooks?: HooksSettings                         // agent 生命周期内的 hook
  color?: AgentColorName                        // UI 显示颜色
  model?: string                                // 'sonnet' | 'opus' | 'haiku' | 'inherit'
  effort?: EffortValue                          // thinking 配额
  permissionMode?: PermissionMode                // 'plan' | 'acceptEdits' | 'bypass' | 'bubble'
  maxTurns?: number                              // 回合数上限
  requiredMcpServers?: string[]                  // 不满足则该 agent 不可用
  background?: boolean                            // 强制后台运行
  initialPrompt?: string                          // 拼到第一个 user turn 前
  memory?: 'user' | 'project' | 'local'          // 持久化记忆作用域
  isolation?: 'worktree' | 'remote'             // 隔离模式
  omitClaudeMd?: boolean                          // 跳过 CLAUDE.md(Explore/Plan 用)
  criticalSystemReminder_EXPERIMENTAL?: string    // 每个用户回合都注入的提醒
  filename?: string                              // 自定义 agent 的源文件
  baseDir?: string
}

4.2 子类型分化

// 内置:动态生成 system prompt(可读 toolUseContext)
type BuiltInAgentDefinition = BaseAgentDefinition & {
  source: 'built-in'
  baseDir: 'built-in'
  getSystemPrompt: ({ toolUseContext }) => string
  callback?: () => void
}

// 自定义:从 markdown frontmatter 解析,prompt 已固化在闭包里
type CustomAgentDefinition = BaseAgentDefinition & {
  getSystemPrompt: () => string
  source: SettingSource  // userSettings | projectSettings | flagSettings | policySettings
  filename?: string
}

// 插件:同 custom,加 plugin 元信息
type PluginAgentDefinition = BaseAgentDefinition & {
  getSystemPrompt: () => string
  source: 'plugin'
  plugin: string
}

4.3 frontmatter 示例 → AgentDefinition 映射

# ~/.claude/agents/security-reviewer.md
---
description: "Senior security engineer for OWASP review"
tools: ["Read", "Bash", "Grep"]
disallowedTools: ["Bash(rm:*)"]
model: "opus"
permissionMode: "plan"
maxTurns: 50
mcpServers: ["github", { vault: { command: "vault-mcp" } }]
skills: ["security-checklist"]
color: "red"
---

You are a senior security engineer specializing in code review for OWASP top 10...

解析后的 AgentDefinition:

{
  agentType: 'security-reviewer',           // 文件名(去 .md)
  whenToUse: 'Senior security engineer...',  // description
  tools: ['Read', 'Bash', 'Grep'],
  disallowedTools: ['Bash(rm:*)'],          // 带规则模式
  model: 'opus',
  permissionMode: 'plan',
  maxTurns: 50,
  mcpServers: ['github', { vault: {...} }],
  skills: ['security-checklist'],
  color: 'red',
  source: 'userSettings',
  filename: 'security-reviewer',
  getSystemPrompt: () => "You are a senior...",    // 闭包持有 markdown 正文
}

05AgentTool 输入输出

AgentTool 是模型与多 agent 系统的唯一接口。它的 schema 由 5+ 个特性 gate 动态裁剪,模型只看到自己 user-type 能用的部分。

5.1 输入字段

字段类型用途
descriptionstring3-5 词任务描述,显示在 UI
promptstring给子 agent 的实际任务文本
subagent_typestring?选哪个 agent。fork 模式可省略
model'sonnet'|'opus'|'haiku'?覆盖 agent 定义的 model
run_in_backgroundboolean?后台跑 + 通过 task-notification 回流
namestring?swarm 给 spawn 的 teammate 起名,可被 SendMessage 寻址
team_namestring?swarm 指定团队
modePermissionMode?swarm 例如 'plan' 强制 spawn 的 teammate 进 plan 模式
isolation'worktree'|'remote'?worktree 隔离 / 远端执行(ant-only)
cwdstring?显式覆盖工作目录(KAIROS gate)

5.2 输出 4 种形态

// 1. 同步完成:子 agent 跑完了,带回结果
{ status: 'completed', prompt, content: [{ type: 'text', text: ... }], agentId, ... }

// 2. 异步派发:立即返回,后台还在跑
{
  status: 'async_launched',
  agentId: 'agent_xyz',
  description, prompt,
  outputFile: '~/.claude/sessions/.../subagents/xyz.jsonl',
  canReadOutputFile: true     // 主 agent 是否能 Read/Bash 该文件
}

// 3. teammate 派生:swarm 模式,在新 pane 启动
{
  status: 'teammate_spawned',
  teammate_id, agent_id, name, color,
  tmux_session_name, tmux_window_name, tmux_pane_id,
  team_name, plan_mode_required
}

// 4. 远端启动:isolation: 'remote'
{
  status: 'remote_launched',
  taskId, sessionUrl, description, prompt, outputFile
}

5.3 schema 动态裁剪

// AgentTool.tsx:242
export const inputSchema = lazySchema(() => {
  // KAIROS gate 控制 cwd 字段
  const schema = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true })

  // background gate 或 fork 启用 → 隐藏 run_in_background(强制 async)
  return isBackgroundTasksDisabled || isForkSubagentEnabled()
    ? schema.omit({ run_in_background: true })
    : schema
})
为什么用 .omit() 而不是条件 spread?
条件三元 spread 会让 Zod 类型推断折叠成 unknown,丢失编译时类型检查。 .omit() 保留具体类型。call() 用显式 AgentToolInput 类型覆盖回完整字段, 所以即使 schema 不暴露,代码仍能安全访问。

5.4 prompt 是动态生成的

AgentTool 的 prompt 不是静态文本,而是 async prompt({ agents, tools, ... }) 函数:

  1. 从 toolUseContext 拿到所有可用 agents
  2. filterAgentsByMcpRequirements 过滤掉 MCP 不满足的
  3. filterDeniedAgents 过滤掉权限规则禁止的
  4. 把剩下的 agent 列表(type: whenToUse (Tools: ...))拼进 prompt
Cache 优化:agent_listing 注入 vs 内嵌
把 agent 列表写进 tool description 会让每次列表变化(MCP 连接、reload-plugins)都 bust prompt cache。 基于 GrowthBook gate tengu_agent_list_attach,可以改成把列表作为独立 attachment 注入, 让 tool description 保持稳定,大幅提升缓存命中率(节省了 ~10.2% fleet cache_creation tokens)。

06三大执行路径

AgentTool.call() 进来后会根据参数分流到三条完全不同的执行路径。理解这三条路径是理解整个系统的关键。

AgentTool.call() teamName && name? (swarm 派 teammate) spawnTeammate() tmux/iterm2/in-process subagent_type 省略? (fork gate 启用) FORK_AGENT 继承父上下文 + system prompt 普通 subagent 路径 查 activeAgents 找 subagent_type 检查 MCP 依赖 · 检查权限 deny 独立 system prompt + 独立 tools → 可叠加 isolation: 'worktree' / 'remote'
三条互斥路径:swarm teammate(独立进程)/ fork(共享上下文)/ 普通 subagent(独立上下文)

6.1 teammate 路径(spawnTeammate)

触发条件:team_name && name 同时存在,且 swarm 启用。这条路径不调用 runAgent —— 而是启动一个独立 Claude 进程

// AgentTool.tsx:441
if (teamName && name) {
  // 防御:teammates 不能再 spawn teammates(团队结构是平的)
  if (isTeammate() && teamName && name) throw
  // 防御:in-process teammate 不能 spawn 后台 agent
  if (isInProcessTeammate() && teamName && run_in_background === true) throw

  const result = await spawnTeammate({
    name, prompt, description, team_name: teamName,
    use_splitpane: true,
    plan_mode_required: spawnMode === 'plan',
    model, agent_type: subagent_type,
  }, toolUseContext)

  return { status: 'teammate_spawned', ... }
}

6.2 fork 路径

触发条件:省略 subagent_type + FORK_SUBAGENT gate 启用 + 非 coordinator + 非 SDK。

// AgentTool.tsx:482
const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE)
const isForkPath = effectiveType === undefined

if (isForkPath) {
  // 递归 fork 防御:fork 子 agent 不能再 fork
  if (toolUseContext.options.querySource === 'agent:builtin:fork' ||
      isInForkChild(toolUseContext.messages)) throw
  selectedAgent = FORK_AGENT
}

6.3 普通 subagent 路径

大多数情况走这条路径。要做 4 重检查:

  1. 找 agent

    agents.find(a => a.agentType === effectiveType)

  2. 权限过滤

    filterDeniedAgents 检查 Agent(name) 形式的 deny 规则; allowedAgentTypes 来自 Agent(x,y) 工具白名单。

  3. MCP 等待 + 检查

    如果 agent 定义了 requiredMcpServers,需要等 pending MCP 连接完成(最多 30s,500ms 轮询), 然后用实际有 tools 的 server 列表(server 必须是 connected + authenticated 才算)做匹配。

  4. isolation 处理

    worktree 创建临时 git worktree;remote(ant-only)调 CCR 远端启动。

07runAgent 流水线

runAgent.ts 是子 agent 真正"启动"的地方,也是整个系统最复杂的函数。它有 ~30 个步骤,每一步都解决一个具体问题。

7.1 流水线全景

runAgent 30 步流水线 Stage 1 · Identity 设置 ① resolvedAgentModel ← getAgentModel(agentDef.model, parent.mainLoopModel, override.model, permissionMode) ② agentId ← override.agentId ?? createAgentId() ③ Perfetto trace 注册(可视化父子关系) ④ contextMessages ← forkContextMessages 过滤掉孤儿 tool_use ⑤ readFileState ← fork 时 clone 父,否则建空缓存 Stage 2 · Context 裁剪 ⑥ baseUserContext = override ?? getUserContext() ⑦ omitClaudeMd 时去掉 claudeMd(Explore/Plan 用,节省 ~5-15 Gtok/周) ⑧ Explore/Plan 还要去掉 systemContext.gitStatus(stale 数据,可省 1-3 Gtok/周) ⑨ permissionMode 覆盖逻辑(详见 8.2 节) Stage 3 · Tools 解析 ⑩ resolvedTools ← useExactTools ? availableTools : resolveAgentTools(agentDef, ...) ⑪ MCP 服务器初始化(连接 + fetchTools) ⑫ allTools ← uniqBy([resolvedTools, agentMcpTools], 'name') ⑬ admin-trusted 检查(plugin/built-in/policy 才允许 frontmatter MCP) Stage 4 · Prompt 组装 ⑭ agentSystemPrompt = override.systemPrompt ?? getAgentSystemPrompt(agentDef, ...) ⑮ enhanceSystemPromptWithEnvDetails 注入 cwd / OS / 日期 / 工作目录 ⑯ skills 预加载(getPromptForCommand 拿到内容,插入为 isMeta 用户消息) ⑰ initialMessages = [...contextMessages, ...promptMessages, hookContext, skillsPrompts] Stage 5 · Hooks 注册 ⑱ executeSubagentStartHooks(agentId, agentType) → 收集 additionalContexts ⑲ registerFrontmatterHooks(...) — agent 生命周期内有效,Stop 转 SubagentStop ⑳ admin-trusted gate 决定 frontmatter hooks 是否允许注册 Stage 6 · 执行 ㉑ createSubagentContext(parent, ...) — clone readFileState / contentReplacementState ㉒ recordSidechainTranscript(initialMessages, agentId) — 持久化到 subagents/ ㉓ writeAgentMetadata(agentId, { agentType, worktreePath, description }) ㉔ for await (const message of query({ ... })) yield message — 进入 query loop Stage 7 · 清理 ㉕ mcpCleanup() — 清理 inline 定义的 MCP server · ㉖ unregisterPerfettoAgent · ㉗ clearSessionHooks · ㉘ clearInvokedSkillsForAgent
runAgent 30 步流水线:从 identity → context → tools → prompt → hooks → 执行 → 清理

7.2 关键决策片段:abortController 选择

// runAgent.ts:530
const agentAbortController = override?.abortController
  ? override.abortController                // 显式覆盖优先
  : isAsync
    ? new AbortController()              // 异步:全新独立 controller
    : toolUseContext.abortController         // 同步:共享父的

这是同步/异步行为差异的根源:

  • 同步子 agent:用户按 Ctrl+C 中断主线程,会同时中断子 agent
  • 异步子 agent:主线程被中断,子 agent 继续跑,需要显式 kill

7.3 MCP 初始化的 admin-trusted 门控

// runAgent.ts:117
const agentIsAdminTrusted = isSourceAdminTrusted(agentDefinition.source)
if (isRestrictedToPluginOnly('mcp') && !agentIsAdminTrusted) {
  // 用户级 agent + MCP 锁定为 plugin-only:跳过 frontmatter MCP
  return { clients: parentClients, tools: [], cleanup: async()=>{} }
}
为什么这个 gate 重要
企业策略可以锁定"只允许 plugin 提供的 MCP"。如果一刀切,plugin agent 自己定义的 MCP 也会被屏蔽,反而破坏功能。 所以 gate 在这里检查 agent 来源:built-in / plugin / policy 算 admin-trusted,放行;只有 user/project 自定义 agent 被屏蔽。

08上下文隔离与共享

子 agent 既要从父继承够用的环境信息(避免重新发现成本),又要保证错误不污染父。createSubagentContext 是这个权衡的关键。

8.1 隔离 vs 共享对照

状态同步 subagent异步 subagentfork
readFileState 克隆 克隆 克隆
contentReplacementState 克隆 克隆 克隆(关键:cache 共享必须克隆)
setAppState 共享 shareSetAppState=true 独立 共享
abortController 共享父的 全新独立 取决于 isAsync
messages 独立 仅 promptMessages 独立 完整继承 forkContextMessages
system prompt 独立 用 agent 自己的 独立 继承父的
tools 独立 agent 定义 独立 继承父的 useExactTools
thinkingConfig disabled disabled 继承父的
MCP clients 合并 parent + agent 私有 合并 合并

8.2 permissionMode 覆盖规则

// runAgent.ts:421
// agent 定义了 permissionMode 时覆盖,但有 3 种模式不可被覆盖
if (
  agentPermissionMode &&
  state.toolPermissionContext.mode !== 'bypassPermissions' &&
  state.toolPermissionContext.mode !== 'acceptEdits' &&
  !(feature('TRANSCRIPT_CLASSIFIER') && state.toolPermissionContext.mode === 'auto')
) {
  toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
}
为什么这三种模式特殊
  • bypassPermissions:用户已经接受了"全开"风险,子 agent 不应该退回更严格
  • acceptEdits:用户已经允许编辑,不该回退
  • auto(transcript classifier):自动分类器在管控,agent 自己的 mode 不该越权
原则:子 agent 只能提升限制,不能降低限制

8.3 shouldAvoidPermissionPrompts 自动设置

// runAgent.ts:446
const shouldAvoidPrompts = canShowPermissionPrompts !== undefined
  ? !canShowPermissionPrompts
  : agentPermissionMode === 'bubble'
    ? false     // bubble 模式总是允许显示提示(冒泡到父终端)
    : isAsync   // 默认:同步可显示,异步自动拒绝

异步 agent 在后台跑,UI 上没有交互入口,所以不能显示权限对话框 → 默认 deny; bubble 模式特殊,允许把权限提示冒泡到父 agent 的终端处理。

8.4 awaitAutomatedChecksBeforeDialog

if (isAsync && !shouldAvoidPrompts) {
  toolPermissionContext = {
    ...toolPermissionContext,
    awaitAutomatedChecksBeforeDialog: true,
  }
}

异步 + 允许提示的特殊场景(bubble):权限对话框前先等所有自动检查(分类器、permission hooks)跑完。 因为后台 agent 不该频繁打扰用户,用户只在自动化无法决策时才被打扰。

09工具过滤三层机制

每个 agent 看到的 tools 列表都不同,这是通过三层过滤合成的。

9.1 第 1 层:assembleToolPool 重组

// AgentTool.tsx:840 — worker 总是用自己的 permissionMode 重组
const workerPermissionContext = {
  ...appState.toolPermissionContext,
  mode: selectedAgent.permissionMode ?? 'acceptEdits',
}
const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools)

核心点:worker 的工具池跟 parent 的限制无关。即使父被限制成"只能 Read",子 agent 仍按自己的 permissionMode 重新组装 —— acceptEdits 默认开放 Edit/Write。

9.2 第 2 层:filterToolsForAgent

// agentToolUtils.ts:70
function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }) {
  return tools.filter(tool => {
    if (tool.name.startsWith('mcp__')) return true     // MCP 全放

    // plan mode 例外:ExitPlanMode 必须可用
    if (toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) && permissionMode === 'plan') return true

    // 所有 agent 都禁止的:TaskOutput, EnterPlanMode, AskUserQuestion, TaskStop, (非 ant 还禁 Agent)
    if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false

    // custom agent 额外禁止
    if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false

    // async 只能用白名单(in-process teammate 例外)
    if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
      if (isInProcessTeammate()) {
        if (toolMatchesName(tool, AGENT_TOOL_NAME)) return true
        if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) return true
      }
      return false
    }
    return true
  })
}

9.3 三套黑/白名单

名单包含用途
ALL_AGENT_DISALLOWED_TOOLS TaskOutput, ExitPlanMode, EnterPlanMode, AskUserQuestion, TaskStop, Agent(非ant), Workflow(非ant) 所有 agent 都不可用 — 这些是"主线程协调"工具
ASYNC_AGENT_ALLOWED_TOOLS FileRead, WebSearch, TodoWrite, Grep, WebFetch, Glob, Bash/PowerShell, Edit, Write, NotebookEdit, Skill, ToolSearch, EnterWorktree, ExitWorktree 异步 agent 的白名单 — 没有 UI 交互的工具才能用
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS TaskCreate/Get/List/Update, SendMessage, CronCreate/Delete/List 同进程 teammate 的额外白名单 — 协作所需

9.4 第 3 层:resolveAgentTools

// agentToolUtils.ts:122 — 根据 agentDef 的 tools / disallowedTools 收窄
function resolveAgentTools(agentDef, availableTools, isAsync) {
  const filtered = filterToolsForAgent({ tools: availableTools, ... })
  const denyOnly = filtered.filter(t => !disallowedTools.has(t.name))

  // tools 字段处理
  if (agentTools === undefined || agentTools[0] === '*') {
    return { hasWildcard: true, resolvedTools: denyOnly }
  }

  // 有 allowlist:只放白名单里的
  for (const spec of agentTools) {
    const { toolName, ruleContent } = permissionRuleValueFromString(spec)
    // 特殊:Agent(name1, name2) 携带 allowedAgentTypes 元信息
    if (toolName === AGENT_TOOL_NAME && ruleContent) {
      allowedAgentTypes = ruleContent.split(',').map(s => s.trim())
    }
    const tool = availableToolMap.get(toolName)
    if (tool) resolved.push(tool)
  }
  return { resolvedTools: resolved, allowedAgentTypes, ... }
}

9.5 Agent(x,y) 元信息特殊处理

当 frontmatter 写 tools: ['Agent(researcher,reviewer)'] 时,这是说"允许该 agent 调用 Agent 工具,但只能派 researcher 和 reviewer 两种"。 resolveAgentTools 提取这个元信息,设置到 allowedAgentTypes,后续 AgentTool.call() 会用它过滤可派 agent 列表。

10系统提示组装

每个 agent 的 system prompt 都是动态拼接的,包含 agent 自己的 prompt + 环境细节 + 可选的 hook 上下文。

10.1 普通路径(非 fork)

// AgentTool.tsx:765
const agentPrompt = selectedAgent.getSystemPrompt({ toolUseContext })

// 注入环境细节
const enhancedSystemPrompt = await enhanceSystemPromptWithEnvDetails(
  [agentPrompt],
  resolvedAgentModel,
  additionalWorkingDirectories,
)
// enhancedSystemPrompt 现在含:
//   - agent 自己的 prompt
//   - cwd / 平台 / 日期 / shell / Git 信息
//   - additional working directories
//   - 绝对路径要求 / 表情使用约束

10.2 fork 路径(继承父的)

// AgentTool.tsx:729
if (isForkPath) {
  if (toolUseContext.renderedSystemPrompt) {
    // 已经渲染过的字节,完全继承(用于 cache 命中)
    forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
  } else {
    // fallback:重新构建(可能因 GrowthBook 翻转而和父不一致)
    forkParentSystemPrompt = buildEffectiveSystemPrompt({
      mainThreadAgentDefinition, toolUseContext, ...
    })
  }
}
renderedSystemPrompt 的妙用
父 agent 在第一次调用 query() 时已经把 system prompt 渲染成最终字节,缓存在 toolUseContext.renderedSystemPrompt 里。fork 子 agent 直接拿这个字节复用, 保证 API 请求前缀字节级一致 → prompt cache 100% 命中。 重新调 getSystemPrompt 可能因 GrowthBook 翻转产生差异,bust cache。

10.3 SubagentStart hook 注入

// runAgent.ts:551
if (additionalContexts.length > 0) {
  const contextMessage = createAttachmentMessage({
    type: 'hook_additional_context',
    content: additionalContexts,
    hookName: 'SubagentStart',
    hookEvent: 'SubagentStart',
  })
  initialMessages.push(contextMessage)    // 作为第一条用户消息
}

用户配置的 SubagentStart hook 可以注入自定义上下文,比如"当前在 git branch X 上"、"昨天发生了 deploy"等。 这些不是改 system prompt(避免 cache miss),而是作为附件用户消息插在 promptMessages 之前。

10.4 critical system reminder(实验)

criticalSystemReminder_EXPERIMENTAL?: string

agent 定义里如果写了这个字段,内容会在每个用户回合都重新注入(通过 attachment 管线)。 用于"必须每轮都强调"的关键约束,比如"严禁运行 rm -rf"、"必须先 plan 再 implement"。

11同步 vs 异步执行

两种执行模式有完全不同的生命周期、通信方式、UI 行为。理解差异是用对工具的关键。

11.1 决策树:shouldRunAsync

// AgentTool.tsx:827
const shouldRunAsync = (
  run_in_background === true ||                  // 用户显式
  selectedAgent.background === true ||              // agent 定义强制
  isCoordinator ||                                  // coordinator 模式
  forceAsync ||                                     // fork gate(所有 spawn 都 async)
  assistantForceAsync ||                            // KAIROS 助理模式
  (proactiveModule?.isProactiveActive() ?? false)     // proactive agent
) && !isBackgroundTasksDisabled

11.2 同步路径详解

  1. registerAgentForeground

    注册 task 状态(可选自动后台化超时);返回 backgroundSignal Promise。

  2. runAgent 流式消费

    for await (const message of runAgent(...));同时 race backgroundPromise。

  3. ~2s 超时显示 BackgroundHint

    PROGRESS_THRESHOLD_MS = 2000,UI 提示"按 Ctrl+B 后台化"。

  4. 用户主动后台化(可选)

    backgroundPromise resolve → 切换到异步路径,主线程立即返回。

  5. 正常完成

    finalizeAgentTool 提取最后 assistant 消息文本作为结果,作为 tool_result 返回。

11.3 异步路径详解

  1. registerAsyncAgent

    注册 LocalAgentTaskState(status: 'running');立即返回 taskId 给主 agent。

  2. void runWithAgentContext(...)

    fire-and-forget 启动一个独立 lifecycle。AsyncLocalStorage 自动捕获父的 workload。

  3. tool_result 返回 async_launched

    主 agent 拿到 { status: 'async_launched', agentId, outputFile },可以继续做别的。

  4. 子 agent 在后台跑

    每条 message 都通过 onCacheSafeParams 触发 startAgentSummarization;同时实时写入 outputFile

  5. 子 agent 完成

    completeAsyncAgent 标记状态;classifyHandoffIfNeeded 跑一次 transcript classifier。

  6. enqueueAgentNotification

    通过 messageQueueManager 把 <task-notification> XML 入队,priority='later'。

  7. queryLoop 下一回合排空

    主 agent 的 query loop 在合适时机消费,作为附件喂给主 agent,主 agent 知道"agent X 完成了"。

11.4 异步通知 XML 结构

// LocalAgentTask.tsx:342
<task-notification>
  <task-id>agent_xyz</task-id>
  <tool-use-id>toolu_abc</tool-use-id>
  <output-file>~/.claude/sessions/.../subagents/xyz.jsonl</output-file>
  <status>completed</status>
  <summary>Agent "Audit security review" completed</summary>
  <result>Found 3 OWASP issues: ...</result>
  <usage>
    <total-tokens>12345</total-tokens>
    <tool-uses>42</tool-uses>
    <duration-ms>180000</duration-ms>
  </usage>
  <worktree>
    <worktree-path>/tmp/agent-xyz-worktree</worktree-path>
    <worktree-branch>agent-xyz</worktree-branch>
  </worktree>
</task-notification>
priority='later' 的设计意图
通知不能抢占用户输入。如果用户正在打字,新通知不应该插队; 而是等当前 user turn 处理完后,在下一回合开始时被排空。 这样保证用户视角的连续性:"我打完字按回车 → 模型先看到我的话,再看到通知"。

12Fork Subagent 缓存共享

Fork 是个非常聪明的优化:不重新发起一个独立 agent,而是复制父的当前状态派生一个分身。 关键技巧是"字节级前缀一致" → prompt cache 100% 命中。

12.1 启用条件

// forkSubagent.ts:32
function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false      // 与 coordinator 互斥
    if (getIsNonInteractiveSession()) return false  // SDK 不用
    return true
  }
  return false
}

12.2 buildForkedMessages 如何构造继承上下文

父 agent 当前状态 [user] 用户问题 [assistant] 思考... [user] 工具结果 [assistant] thinking + 几个 tool_use - tool_use(Read, id:t1) - tool_use(Agent, id:t2) ← fork 的 tool_use - tool_use(Bash, id:t3) buildForkedMessages 子 fork agent 初始消息 [user] 用户问题 [assistant] 思考... [user] 工具结果 [assistant] thinking + 全部 tool_use ←完整克隆 - tool_use(Read, id:t1) - tool_use(Agent, id:t2) - tool_use(Bash, id:t3) [user] - tool_result(t1, "Fork started — processing") - tool_result(t2, "Fork started — processing") - tool_result(t3, "Fork started — processing") - text: ←per-child directive <fork-boilerplate>...规则...</fork-boilerplate> [DIRECTIVE]: 实际任务文本 ⚠ 唯一不同的就是最后这段 text — 前缀 100% cache 命中
fork 构造原则:历史完全克隆,所有 tool_use 都补占位 tool_result,只有最后的 directive 文本不同

12.3 占位 tool_result 的关键

// forkSubagent.ts:93
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'

// 给 assistant message 里的每个 tool_use 都补一个占位 tool_result
const toolResultBlocks = toolUseBlocks.map(block => ({
  type: 'tool_result',
  tool_use_id: block.id,
  content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }],   // 字节相同
}))

所有 fork 子 agent 看到的占位 tool_result 文本完全相同 → prompt cache 共享率最大化。

12.4 fork-boilerplate directive 的 10 条铁律

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.
2. Do NOT converse, ask questions, or suggest next steps
3. Do NOT editorialize or add meta-commentary
4. USE your tools directly: Bash, Read, Write, etc.
5. If you modify files, commit your changes before reporting. Include the commit hash.
6. Do NOT emit text between tool calls. Use tools silently, then report once at the end.
7. Stay strictly within your directive's scope.
8. Keep your report under 500 words unless the directive specifies otherwise.
9. Your response MUST begin with "Scope:". No preamble.
10. REPORT structured facts, then stop

12.5 递归 fork 防御

// AgentTool.tsx:495
if (
  toolUseContext.options.querySource === 'agent:builtin:fork' ||  // 主防线
  isInForkChild(toolUseContext.messages)                          // 备防线:扫描历史
) {
  throw new Error('Fork is not available inside a forked worker.')
}

主防线:querySource 在 spawn 时设置,autocompact 不会改 options 只会改 messages,所以这个值抗压缩备防线:扫描消息历史找 fork-boilerplate 标签 —— 但 autocompact 可能压掉这条标签消息,所以是 fallback。

12.6 useExactTools 的字节一致性

// AgentTool.tsx:906
availableTools: isForkPath ? toolUseContext.options.tools : workerTools,
...(isForkPath && { useExactTools: true }),

fork 子 agent 直接用父的 tools 数组引用,不重新组装。原因:即使工具集相同, 重新 assemble 出来的工具顺序、序列化字节都可能微小差异 → 破坏 prompt cache。

13Worktree 隔离

isolation: 'worktree' 让子 agent 在独立 git worktree 工作 —— 同一个 repo,但不同的工作副本。这是"探索性修改"的最佳工具。

13.1 工作流程

// AgentTool.tsx:861
if (effectiveIsolation === 'worktree') {
  const slug = `agent-${earlyAgentId.slice(0, 8)}`
  worktreeInfo = await createAgentWorktree(slug)
  // worktreeInfo = { worktreePath, worktreeBranch, headCommit, gitRoot }
}

13.2 cwd 自动切换

const cwdOverridePath = cwd ?? worktreeInfo?.worktreePath
const wrapWithCwd = (fn) => cwdOverridePath
  ? runWithCwdOverride(cwdOverridePath, fn)
  : fn()

runWithCwdOverride 用 AsyncLocalStorage 给 agent 整个生命周期注入新的 cwd。 所有 getCwd() 调用(file 工具、shell)都会拿到 worktree 路径。

13.3 自动清理逻辑

// AgentTool.tsx:921
const cleanupWorktreeIfNeeded = async () => {
  if (!worktreeInfo) return {}
  const { worktreePath, worktreeBranch, headCommit, gitRoot, hookBased } = worktreeInfo

  if (hookBased) return { worktreePath }    // hook 创建的不自动删

  if (headCommit) {
    const changed = await hasWorktreeChanges(worktreePath, headCommit)
    if (!changed) {
      await removeAgentWorktree(worktreePath, worktreeBranch, gitRoot)
      return {}
    }
  }
  return { worktreePath, worktreeBranch }    // 有改动:保留路径,通知父
}
"无改动自动清理"的好处
探索任务大部分会失败或得到结论但不需要保留代码。自动清理减少了用户手动 git worktree remove 的负担。 但如果有未提交改动,会自动保留 + 把路径返回给父 agent,让父决定怎么处理(merge / cherry-pick / 丢弃)。

13.4 fork + worktree 组合

// AgentTool.tsx:869
if (isForkPath && worktreeInfo) {
  promptMessages.push(createUserMessage({
    content: buildWorktreeNotice(getCwd(), worktreeInfo.worktreePath),
  }))
}

注入一条提示给 fork 子 agent:

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. Your changes stay in this worktree and will not affect the parent's files.

14Swarm 团队架构

Swarm 是 swarm 模式 + 多 Claude 进程 + 文件系统的组合。它把"多 agent"从同进程协作扩展到跨进程、跨 pane、跨机器

14.1 创建团队 (TeamCreate)

// TeamCreateTool.ts:155 — 创建团队的 5 步

// 1. 写团队配置文件
const teamFile: TeamFile = {
  name: finalTeamName,
  createdAt: Date.now(),
  leadAgentId,
  leadSessionId: getSessionId(),
  members: [{
    agentId: leadAgentId,
    name: TEAM_LEAD_NAME,        // 'team-lead'
    agentType: leadAgentType,
    model: leadModel,
    joinedAt: Date.now(),
    tmuxPaneId: '',
    cwd: getCwd(),
    subscriptions: [],
  }],
}
await writeTeamFileAsync(finalTeamName, teamFile)
// → ~/.claude/teams/<name>/config.json

// 2. 注册 session-end 清理
registerTeamForSessionCleanup(finalTeamName)

// 3. 重置任务列表(team = task list)
const taskListId = sanitizeName(finalTeamName)
await resetTaskList(taskListId)
await ensureTasksDir(taskListId)

// 4. 设置 leader 的 teamName(让 getTaskListId 返回它)
setLeaderTeamName(sanitizeName(finalTeamName))

// 5. 更新 AppState.teamContext
setAppState(prev => ({ ...prev, teamContext: { teamName, leadAgentId, teammates: ... } }))

14.2 TeamFile 完整结构

type TeamFile = {
  name: string
  description?: string
  createdAt: number
  leadAgentId: string            // "team-lead@<teamName>"
  leadSessionId: string          // 实际 session ID,供 team discovery
  members: [{
    agentId: string             // "name@teamName" 格式
    name: string                // 显示名,SendMessage 寻址用
    agentType?: string          // 角色,如 'researcher'
    model?: string
    joinedAt: number
    tmuxPaneId: string
    cwd: string
    subscriptions: string[]     // 订阅哪些 channel
  }]
}

14.3 派 teammate (spawnTeammate)

// AgentTool.tsx:441 — 当 teamName && name 时
const result = await spawnTeammate({
  name,                          // 比如 'researcher'
  prompt,
  description,
  team_name: teamName,
  use_splitpane: true,
  plan_mode_required: spawnMode === 'plan',
  model: model ?? agentDef?.model,
  agent_type: subagent_type,
}, toolUseContext)

14.4 平结构(no nested teammates)

// AgentTool.tsx:425 — 防御
if (isTeammate() && teamName && name) {
  throw new Error(
    'Teammates cannot spawn other teammates — the team roster is flat.'
  )
}
为什么团队结构必须是平的
TeamFile.members 是一个扁平数组,只有一个 leadAgentId。如果允许 teammate 再 spawn teammate,会出现:
  • 新 teammate 不知道自己的"父 teammate" — 没有这个字段
  • leader 不知道这个新成员是谁派的
  • shutdown / re-assign 等操作的责任链断裂
简单的"flat 限制"避免了一整套层级管理逻辑。

15Backend 抽象层

同样的 spawnTeammate API 在 4 种环境下有完全不同的实现:tmux / iTerm2 / Windows Terminal / 同进程。 TeammateExecutor 接口统一了它们。

15.1 4 种 backend 对照

Backend载体启动新 teammate限制
tmux tmux pane(独立进程) spawn 新 pane,在里面跑 claude 命令 需要 tmux 已安装
iterm2 iTerm2 split pane(独立进程) 通过 it2 CLI 创建 split + 运行 claude 需要 iTerm2 + it2 工具
windows-terminal Windows Terminal pane/tab(独立进程) wt.exe 命令行参数创建 需要 Windows Terminal
in-process 同 Node.js 进程,独立 context fork worker(共享内存) 不能 spawn background;不能嵌套

15.2 PaneBackend 接口(tmux/iterm2/windows-terminal)

type PaneBackend = {
  type: BackendType
  displayName: string
  supportsHideShow: boolean

  isAvailable(): Promise<boolean>
  isRunningInside(): Promise<boolean>

  createTeammatePaneInSwarmView(name, color): Promise<CreatePaneResult>
  sendCommandToPane(paneId, command): Promise<void>
  setPaneBorderColor(paneId, color): Promise<void>
  setPaneTitle(paneId, name, color): Promise<void>
  enablePaneBorderStatus(): Promise<void>
  rebalancePanes(windowTarget, hasLeader): Promise<void>
  killPane(paneId): Promise<boolean>
  hidePane(paneId): Promise<boolean>
  showPane(paneId, target): Promise<boolean>
}

15.3 TeammateExecutor 接口(高层)

type TeammateExecutor = {
  type: BackendType
  setContext?(context): void
  isAvailable(): Promise<boolean>

  spawn(config: TeammateSpawnConfig): Promise<TeammateSpawnResult>
  sendMessage(agentId, message): Promise<void>
  terminate(agentId, reason?): Promise<boolean>    // 优雅
  kill(agentId): Promise<boolean>                 // 强制
  isActive(agentId): Promise<boolean>
}

15.4 detection 流程(backends/detection.ts)

  1. CLAUDE_CODE_TEAMMATE_MODE env(强制覆盖)
  2. 检查 TMUX env 变量 → tmux backend
  3. 检查 TERM_PROGRAM === 'iTerm.app' + it2 CLI 是否安装 → iterm2
  4. 检查 WT_SESSION env → windows-terminal
  5. fallback → in-process

15.5 in-process 的特殊性

// in-process teammate 在同 Node.js 进程跑 worker
// 但有自己的:
- AgentContext (AsyncLocalStorage)
- agentId / agentName / teamName
- abortController
- AppState 视图(通过 ToolUseContext.getAppState 包装)

// 共享的:
- 文件系统 / MCP 连接 / 配置
- AppState 后端(通过 setAppStateForTasks 写)
in-process 的额外限制
  • 不能 spawn background subagent(他们的生命周期跟 leader 进程绑定 — leader 死则 worker 死)
  • 不能再 spawn teammate(平结构限制)
  • 可以用 TaskCreate / SendMessage / CronCreate(IN_PROCESS_TEAMMATE_ALLOWED_TOOLS)

16邮箱通信系统

Swarm 模式下 teammates 之间通过文件邮箱异步通信。这是为什么 swarm 可以跨进程、跨 tmux 窗口工作的核心机制。

16.1 邮箱目录结构

~/.claude/teams/<teamName>/
├── config.json                            // TeamFile
└── inboxes/
    ├── team-lead.json                     // leader 的收件箱
    ├── researcher.json                    // teammate "researcher" 的收件箱
    ├── researcher.json.lock               // 锁文件
    └── reviewer.json

16.2 邮箱消息结构

type TeammateMessage = {
  from: string            // 发件人 name
  text: string            // 消息正文
  timestamp: string       // ISO 8601
  read: boolean           // 收件人是否已读
  color?: string          // 发件人颜色(UI 显示用)
  summary?: string        // 5-10 词摘要(UI 预览)
}

16.3 写信(writeToMailbox)

// teammateMailbox.ts:134
async function writeToMailbox(recipientName, message, teamName) {
  await ensureInboxDir(teamName)
  const inboxPath = getInboxPath(recipientName, teamName)

  // 必须先创建文件(proper-lockfile 要求文件存在)
  // 'wx' = 不存在才写,存在抛 EEXIST(被忽略)
  await writeFile(inboxPath, '[]', { flag: 'wx' })

  try {
    release = await lockfile.lock(inboxPath, LOCK_OPTIONS)

    // 拿锁后重读,避免覆盖期间收到的消息
    const messages = await readMailbox(recipientName, teamName)
    messages.push({ ...message, read: false })
    await writeFile(inboxPath, jsonStringify(messages, null, 2), 'utf-8')
  } finally {
    await release?.()
  }
}

16.4 SendMessage 工具

// SendMessageTool.ts
{
  to: string,                       // teammate 名 / "*" 广播 / "uds:<path>"
  summary?: string,                 // UI 预览
  message: string | StructuredMessage // 文本或结构化消息
}

// StructuredMessage(用于协议级通信)
{ type: 'shutdown_request', reason? }
{ type: 'shutdown_response', request_id, approve, reason? }
{ type: 'plan_approval_response', request_id, approve, feedback? }

16.5 收信(getTeammateMailboxAttachments)

这是邮箱系统跟 query loop 的连接点。每个 user turn 开始,attachment 管线会:

  1. 读未读消息

    readUnreadMessages(agentName, teamName) 返回 read: false 的消息

  2. 过滤掉协议消息

    shutdown_request 等结构化消息必须留在邮箱中等专门的 InboxPoller 处理, 否则 attachment 管线先消费掉,这些信号永远不会触发对应 UI handler。

  3. 合并 AppState.inbox 中的待处理消息

    这些是 InboxPoller 已经从邮箱拉到内存但还没作为 turn 处理的消息。需要去重(file + AppState 都可能有同条)。

  4. 构造为 attachment

    包成 <teammate-message> XML,作为 user message 注入下一个 turn。

  5. 标记为已读

    从文件邮箱写回 read: true,从 AppState 标记 status: 'consumed'。

16.6 协议消息分流示意

teammate 邮箱 researcher.json 普通文本消息 attachment 管线消费 → 注入下个 user turn writeToMailbox + getTeammateMailboxAttachments 结构化协议消息 InboxPoller 路由到 workerPermissions / shutdown queue isStructuredProtocolMessage 判定 关键设计:两条管线必须严格分流 如果 attachment 管线先读到协议消息并标为已读,InboxPoller 就拿不到了 → shutdown 信号丢失,死锁
邮箱中两类消息走不同的处理通道,通过 isStructuredProtocolMessage 判定来分流

17任务通知回流

这是异步 agent 跟父 agent 的通信机制 —— 子 agent 完成时不直接调用父,而是排队一条 message,让父在自己合适的回合消费。

17.1 通知触发点

// agentToolUtils.ts:625 — runAsyncAgentLifecycle 中
enqueueAgentNotification({
  taskId,
  description,
  status: 'completed',        // | 'failed' | 'killed'
  setAppState: rootSetAppState,
  finalMessage,                // 子 agent 最后输出的文本
  usage: { totalTokens, toolUses, durationMs },
  toolUseId,
  worktreePath,
  worktreeBranch,
})

17.2 enqueueAgentNotification 内部

// LocalAgentTask.tsx:272 — 防重复
let shouldEnqueue = false
updateTaskState(taskId, setAppState, task => {
  if (task.notified) return task           // 已通知过,跳过
  shouldEnqueue = true
  return { ...task, notified: true }
})
if (!shouldEnqueue) return

// 中止 prompt suggestion 推测,因为后台 task 状态变了
abortSpeculation(setAppState)

// 拼 XML 消息
const message = `<task-notification>...</task-notification>`

// 入队 priority='later'
enqueuePendingNotification({ value: message, mode: 'task-notification' })

17.3 priority 系统

// messageQueueManager.ts:151
const PRIORITY_ORDER = {
  now: 0,      // 立即处理(用户输入)
  next: 1,     // 下一个 turn 必处理(任务摘要)
  later: 2,    // 在自然 idle 时处理(task notifications)
}

task-notification 用 'later' 优先级,不抢用户输入。dequeue 时同优先级 FIFO,不同优先级按上面顺序。

17.4 完整通知生命周期

主 agent 调用 AgentTool t=0 async_launched t=0.1s 主 agent 继续 主 agent 做别的事 t=0~30s 子 agent 完成 t=30s enqueueAgentNotification 主 agent 完成本 turn t=35s queryLoop 排空队列 t=35.1s 作为 attachment 注入 下个 turn 子 agent 后台跑(query loop) — 写 outputFile · 更新 progress 主 agent 在 t=35.1s 才"知道"子 agent 完成 — 这种延迟是有意的(priority='later'),保证用户输入不被通知抢占。
异步通知的完整生命周期:从 spawn → 后台跑 → 完成入队 → 主 agent 下个 turn 看到

18SubagentStart / SubagentStop Hooks

用户可以在 settings.json 配 hook,在每个 subagent 的关键生命周期点运行自定义脚本。

18.1 触发点

Hook 事件时机能力
SubagentStart runAgent 进入 query() 之前 注入 additionalContexts(作为 user message),不能阻断启动
SubagentStop 子 agent 完成 / 失败 / 中止时 由 frontmatter Stop hook 转换;能阻断"标记为完成"
PreToolUse 子 agent 内每个 tool 调用前 能阻断、改输入
PostToolUseFailure 子 agent 内 tool 失败后 提示恢复策略

18.2 frontmatter 注册的 Stop hook 自动转换

// runAgent.ts:573 — 关键 isAgent 参数
if (agentDefinition.hooks && hooksAllowedForThisAgent) {
  registerFrontmatterHooks(
    rootSetAppState,
    agentId,
    agentDefinition.hooks,
    `agent '${agentDefinition.agentType}'`,
    true,                    // ← isAgent: 把 Stop 转成 SubagentStop
  )
}
为什么要做这种转换
agent 的 frontmatter 里写 hooks.Stop 时,用户期望"这个 agent 结束时跑"。 但 hook 系统的 'Stop' 事件是"主对话 Stop"。如果不转换,主线程结束才会触发,语义错位。 所以注册时自动改名为 SubagentStop,精确匹配语义。

18.3 admin-trusted gate

const hooksAllowedForThisAgent =
  !isRestrictedToPluginOnly('hooks') ||
  isSourceAdminTrusted(agentDefinition.source)

跟 MCP 一样:企业策略可以"只允许 plugin 提供的 hooks"。 built-in / plugin / policy agent 算 admin-trusted,放行;user/project 自定义 agent 的 frontmatter hooks 被屏蔽。

18.4 SubagentStart hook 的 additionalContexts

// runAgent.ts:537
const additionalContexts = []
for await (const hookResult of executeSubagentStartHooks(
  agentId, agentDefinition.agentType, agentAbortController.signal,
)) {
  if (hookResult.additionalContexts?.length) {
    additionalContexts.push(...hookResult.additionalContexts)
  }
}

if (additionalContexts.length > 0) {
  const contextMessage = createAttachmentMessage({
    type: 'hook_additional_context',
    content: additionalContexts,
    hookName: 'SubagentStart',
    hookEvent: 'SubagentStart',
  })
  initialMessages.push(contextMessage)
}

典型用法:hook 脚本输出 {"additionalContexts": ["当前 PR 是 #1234", "测试套件已锁定"]},这些会作为子 agent 第一个用户消息出现。

19端到端示例

用一个真实场景串起所有机制。

场景:用户让模型"重构 auth 模块,做完跑测试 + 安全 review"

  1. 用户输入
    "把 auth 模块重构成基于 JWT,做完后跑测试,然后让安全专家 review"
  2. 主 agent 决策

    模型识别这是个并行任务:重构和测试可以串行,但 review 可以跟测试并行。它决定:

    • 主 agent 自己做重构
    • security-reviewer(用户自定义 agent)做 review,异步后台跑
    • 主 agent 跑测试
    • 等通知回来再总结
  3. 主 agent 调用 AgentTool(后台 review)
    {
      description: "Security review of auth refactor",
      prompt: "Review the JWT-based auth refactor in src/auth/. Check OWASP top 10...",
      subagent_type: "security-reviewer",
      run_in_background: true,
      isolation: "worktree"            // 在隔离副本里看,不污染主分支
    }
  4. AgentTool.call() 走普通路径
    • findActiveAgent → 找到 security-reviewer (CustomAgentDefinition, source: 'userSettings')
    • 权限过滤通过,MCP 不需要
    • isolation: 'worktree' → createAgentWorktree → /tmp/agent-abc12345
    • shouldRunAsync = true → registerAsyncAgent + runAsyncAgentLifecycle
    • 立即返回 { status: 'async_launched', agentId: 'agent_xyz', outputFile: '...' }
  5. 子 agent 在后台进入 runAgent 流水线
    • 用 opus 模型(agentDef.model = 'opus')
    • permissionMode: 'plan' → toolPermissionContext.mode 切换
    • readFileState 全新空缓存(不是 fork)
    • tools 经 resolveAgentTools 收窄成 [Read, Bash, Grep] minus Bash(rm:*)
    • system prompt = security-reviewer 自己的 + 环境注入 + cwd = worktree path
    • SubagentStart hook 跑了一遍,注入 "在 worktree X 上工作"
    • 进入 query loop
  6. 主 agent 同时跑测试

    Bash("npm test") 阻塞 60s 跑完测试。期间子 agent 在后台读代码、检查 OWASP 项。

  7. 主 agent 完成测试,结束当前 turn

    queryLoop 准备进入下一 turn。检查 message queue,发现还没有 task-notification(子 agent 还没完成)。 主 agent 给用户回了一个"测试通过"的中间答复。

  8. 子 agent 完成

    runAsyncAgentLifecycle 调:

    • completeAsyncAgent(标 task 状态 completed)
    • classifyHandoffIfNeeded(没问题,跳过)
    • cleanupWorktreeIfNeeded → reviewer 没改文件 → 自动清理 worktree
    • enqueueAgentNotification(priority='later')
  9. 用户问"安全 review 怎么样了?"

    queryLoop 进入新 user turn。排空 queue 时拿到 task-notification,作为附件注入。

    <task-notification>
      <task-id>agent_xyz</task-id>
      <status>completed</status>
      <summary>Agent "Security review" completed</summary>
      <result>Found 2 issues: missing CSRF token, JWT secret in env file</result>
      <usage>
        <total-tokens>8500</total-tokens>
        <tool-uses>15</tool-uses>
        <duration-ms>45000</duration-ms>
      </usage>
    </task-notification>

    主 agent 看到这条通知 + 用户问题,综合回答:"重构完成,测试 12/12 通过。安全 review 发现 2 个问题:..."

关键时间线

用户输入 t=0 spawn reviewer t=2s async_launched 主 agent 重构 t=2-30s 主 agent 跑测试 t=30-90s 主 agent 中间答复 t=90s reviewer 完成 t=95s 入队 later 用户问 + 通知排空 t=130s reviewer 在隔离 worktree 后台跑 — 不污染主分支 两个 agent 并行工作 95 秒,主 agent 不阻塞,通知通过 priority='later' 在合适时机喂给主 agent。 Worktree 自动清理,因为 reviewer 没改文件。
多 agent 并发 + 异步通知 + worktree 隔离的完整端到端示例

20核心设计原则

从这套实现中可以提炼出 8 条可迁移到其他 agent 系统的设计原则。

  1. 把 agent 当一等公民

    用结构化 AgentDefinition 表达,而不是把"调用 LLM"当作一个 RPC。 每个 agent 有独立的 system prompt、tool 集合、permission mode、model、MCP server。 这样才能精确控制其能力边界。

  2. 静态发现 + 动态注册

    built-in 在源码里,user/project 在 .md 文件,plugin 在插件包。 按优先级合并(plugin < user < project < flag < policy),让"系统提供安全默认 + 用户覆盖 + 企业兜底"成为标准模式。

  3. 显式声明工具范围

    三层过滤(filterToolsForAgent + agent 自己的 tools/disallowedTools + 权限规则)让每个 agent 只看到必要的工具。 这同时降低 prompt 成本(token 数)和安全风险(误用的可能性)。

  4. 同步 vs 异步是不同的语义,不是性能选项

    同步阻塞父 turn,共享 abortController 和 setAppState;异步独立生命周期,通过 task-notification 异步通信。 两条路径有不同的 tool 白名单(异步不能 AskUserQuestion 等交互工具)。把它们当作两套独立的执行模式,而不是"同步 + 后台 flag"。

  5. Fork 是为缓存而生的

    fork 子 agent 完整继承父的字节级 system prompt、tools、history。 所有 fork 子用同一占位 tool_result,只有 directive 不同 → 前缀 100% cache 命中。 这是把"启动一个新 agent"的成本从~5k token 降到 ~50 token。

  6. 消息队列 + priority 优先级

    异步通知用 priority='later',永远不抢用户输入。这种"排队 + 自然消费"模式比同步回调简单得多, 也避免了"通知洪水"问题。同时支持多个 agent 同时完成时的有序处理。

  7. 文件系统作为 swarm 的真理来源

    TeamFile / 邮箱 / 任务列表都用 JSON 文件 + proper-lockfile。 这让 swarm 模式天然支持 tmux / iTerm2 / Windows Terminal / 远端机器 —— 只要能访问同一文件系统就能协作。 没有任何 IPC、daemon、或网络协议。

  8. 双管线分流邮箱中的不同消息类型

    普通文本走 attachment 管线,变成下个 turn 的 user message;协议消息(shutdown/permission)走 InboxPoller, 路由到专门的 handler。两条管线有不能交叉的硬约束 —— 否则信号会被错误消费导致死锁。

最重要的一条:让多 agent 协作"看起来像一个 agent"
从用户视角,Claude Code 看起来是一个 agent —— 接受任务、回答、完成。 实际上幕后可能是 5 个并行子 agent + 2 个 swarm teammate + 1 个 fork 子 agent。 实现这种抽象的关键是:
  • 统一的工具入口(AgentTool / SendMessage)
  • 统一的通信协议(task-notification 附件 + 文件邮箱)
  • 统一的状态管理(AppState.tasks + teamContext)
  • 独立但统一的隔离机制(worktree + permissionMode)
这种"复杂藏在内部"的设计是 agent 系统能否规模化的根本因素。