本文实现分析限定于 2026 年 3 月 31 日的公开镜像快照,它不是 Anthropic 官方源码仓库,也不代表此后产品版本。正文固定到该提交;官方文档用于说明产品概念,功能开关和缺失模块按可见代码分别说明。

上一篇把能力入口拆成 slash command、SkillTool 和 MCP。 这一篇看另一个更高层的能力入口:AgentTool。它不是直接执行一个本地动作, 而是启动一个新的 agent 循环,让这个 agent 带着自己的提示词和工具池去完成子任务。

先说结论:Claude Code 为 subagent 建立独立 query loop 和消息上下文,但这不等于进程或文件系统沙箱。 普通 subagent 会重新选择 agent definition、重建 worker tool pool;fork path 则反过来, 故意复用父级 system prompt、tools、thinking config 和 messages prefix,只在尾部加 fork 指令, 目的就是最大化 prompt cache 命中。

主线可以压成一句话:模型发起 Agent tool_use 后,AgentTool.call() 先决定是 teammate、普通 subagent 还是 fork path;普通 subagent 用 assembleToolPool() 和 resolveAgentTools() 生成 worker tools;runAgent() 再创建 createSubagentContext(),执行独立 query() 循环,并把消息写入 sidechain transcript。

阅读目标:这篇只追本地 in-process subagent 和 fork path: Agent 怎么被选中,工具池怎么被收窄,fork 为什么要复用父级前缀, subagent context 默认隔离了哪些状态,同步和异步 agent 为什么有不同生命周期。 远端 CCR、tmux teammate 和 swarm 只在必要处点到为止。

产品层证据来自 Claude Code 的 sub-agents 文档、 hooks 文档; 源码层来自 公开源码镜像。 本文只描述客户端代码能证明的路径,不从客户端代码推断服务端调度策略。

AgentTool input
  -> select agent definition
  -> assembleToolPool() / resolveAgentTools()
  -> createSubagentContext(parentContext, workerTools)
  -> runAgent() // its own query loop
  -> sidechain transcript + parent-visible result

fork path
  -> reuse parent system/tools/messages prefix
  -> append fork-only suffix
源码形状:普通 subagent 先隔离工具上下文;fork path 反过来保留父级前缀来服务 prompt cache。

一、AgentTool 的输入不是“开新会话”

AgentTool 的 schema 在 AgentTool.tsx。 基础输入有 description、prompt、可选 subagent_type、可选模型和后台运行标记; 相关功能开关启用时,还可能有 name、team_name、mode、 isolation 和 cwd。这些字段决定的是执行形态,不是简单的聊天窗口参数。

1.1 先分 teammate、fork 和普通 agent

AgentTool.call() 先读取 app state 和 permission mode,再处理几条分流。 如果显式参数或当前上下文解析出的 team name 与 name 同时存在,它会走 teammate spawn;否则再看 agent 类型。指定 subagent_type 时按该类型选择;省略类型且 fork 开关关闭时,默认选择 general-purpose;省略类型且 fork 开关打开时,才走 fork path。

普通类型分支选择 agent 前还会应用 allowedAgentTypes 并过滤 deny rule。源码里支持 Agent(name) 这类权限规则: 如果某个 agent 被 deny,调用会直接抛出错误,而不是让模型绕过去。 这和前面的权限篇能接上:启动 subagent 本身也要受明确的权限规则控制。

1.2 required MCP 是启动前置条件

agent definition 可以声明 required MCP servers。 源码会先检查这些 server 是否已经有 tools。 如果所需 server 还在 pending,它每 500 ms 重读状态,最多等 30 秒,发现必需 server 失败时提前结束等待。最终检查的是匹配名称的 server 是否出现在 MCP 工具名中;这个检查不是一次实际调用测试,不能证明每个动作都可用。若检查失败,就提示通过 /mcp 配置或认证。

这说明 agent 不是“启动后再慢慢发现能力”。如果 agent 声明了必需的 MCP,那么这些工具必须在启动前可用,否则系统提示词、工具池和任务预期会互相矛盾。

二、普通 subagent 会重建 worker tool pool

普通 subagent 的关键不是复制父级工具,而是重建一份 worker 工具池。 这段代码 先从父级 app state 拿 toolPermissionContext 和 mcp.tools, 再把 mode 改成 selectedAgent.permissionMode ?? "acceptEdits", 最后调用 assembleToolPool()。

2.1 resolveAgentTools() 逐项缩小可用工具

工具收窄的共享逻辑在 resolveAgentTools()。 它会先调用 filterToolsForAgent(),过滤所有 agent 都不能用的工具、自定义 agent 不能用的工具、 async agent 不能用的工具;然后再处理 agent definition 里的 tools 和 disallowedTools。

MCP tools 在 filterToolsForAgent() 中跳过通用的 agent 类别和 async 过滤;它们仍受后续 tools、disallowedTools 以及执行时权限约束。普通 subagent 的 Agent tool 会被移除,主线程不应用这组 subagent 过滤;启用团队功能的 in-process teammate 则另有允许同步子任务的例外,不能把这一规则推广到全部 agent。

2.2 普通 agent 有自己的系统提示词

非 fork path 会调用 selected agent 的 getSystemPrompt(), 再用 getAgentSystemPrompt() 加上环境细节和 enabled tools。它不是把父级系统提示词原样带过去,而是按 agent definition 重新生成。

这就是普通 subagent 的语义:父 agent 只交代任务,子 agent 用自己的角色、工具和上下文执行。 它像一个小型 runtime,不是父会话里的一段 prompt 模板。

三、fork path 反过来:为了缓存,故意复制父级前缀

fork path 是另一种设计。它不急着给子 agent 新系统提示词,而是尽量复制父级请求前缀。 注释写得很明确: fork child 继承父级 system prompt,并通过 buildForkedMessages() 克隆父级 assistant message、 placeholder tool_results 和 per-child directive。

这条 fork 路径受 isForkSubagentEnabled() 约束:需要 FORK_SUBAGENT 构建开关,且不在 coordinator mode 或非交互式 session。开关生效还会让普通命名 agent 一并走后台启动策略,最终仍受后台任务禁用开关限制。它不等同于 Skill 的 context: fork,也不等同于 CLI 的 --fork-session。

3.1 exact tools 和 thinking config 都是 cache key 的一部分

fork path 的 runAgentParams 会设置 useExactTools: true, 并把 availableTools 设成父级 toolUseContext.options.tools。 源码注释 解释了原因:worker tools 如果用不同 permission mode 重建,工具定义序列化会变,prompt cache 就会在第一个差异处失效。

runAgent() 里也配合了这件事:fork children 继承父级 thinking config;普通 subagent 则默认禁用 thinking 来控制输出成本。 所以 fork path 是一个专门为“共享父级上下文前缀”设计的路径。

parent history
  + assistant(all tool_use blocks)
  + user(tool_result placeholders for every tool_use, child directive)
简化消息形状:每个 fork 保留同一组父消息和占位结果,只在末尾指令处区分子任务;占位结果不证明工具已执行。

3.2 fork 不能递归 fork

fork child 为了工具定义一致,仍然保留 Agent tool。但 调用时会做递归 fork guard: 如果当前 querySource 已经是 fork agent,或者 messages 里能识别 fork child 标记,就直接拒绝再次 fork。 这避免了“为了 cache 保留工具 schema,却允许无限分叉”的矛盾。

四、createSubagentContext 默认隔离

真正把 subagent 变成独立运行单元的,是 createSubagentContext()。 它克隆读取缓存和内容替换决策,创建新的触发集合,并限制父 UI 回调。不过实际返回值仍保留共享入口:任务登记始终能到 root store,文件归属统计回调也继续共享。这里应按字段理解隔离,不能把注释中的默认原则读成“所有副作用都与父级断开”。

子代理克隆读取缓存和替换决策,新建触发集合,保留任务登记和归属统计共享入口;默认传播父取消,后台显式覆盖
读取缓存与触发集合属于子 agent;任务登记和归属统计仍可回到共享 store。上下文隔离不阻止已获许可的文件写入。

4.1 默认不让子 agent 控制父 UI

createSubagentContext() 会克隆 readFileState,新建 nested memory / dynamic skill trigger sets,生成新的 agentId 和 query tracking depth。 默认情况下,setAppState 是 no-op,setToolJSX、addNotification、setStreamMode 则不提供。克隆的读取缓存可由子 agent 自己更新,并不是只读文件系统。

这不是保守过度,而是避免子 agent 在后台或嵌套执行时随便改父级界面。 在 runAgent() 的实际调用中,同步 agent 共享 setAppState,同步与异步 agent 都计入响应指标。helper 默认创建会传播父取消的 child controller;后台任务显式传入不与父 ESC 联动的 controller,才能继续独立运行。两种情况都保留任务登记入口,避免后台 Bash 进程失去清理记录。

4.2 无 UI 的 subagent 会避免权限弹窗

在未覆盖 getAppState 且没有选择共享父级 abort controller 时,helper 才把读取结果包装成 shouldAvoidPermissionPrompts: true。这和权限篇能对上: 无法弹窗的后台路径应该先让 hook 决策,没人能决策就 deny,而不是卡死等待用户点击。

五、runAgent 是子 agent 的小 query loop

AgentTool.call() 最后会把参数交给 runAgent()。 它不是只发一次 API 请求,而是一个 async generator:初始化上下文,执行 hooks,预加载 skills,接上 agent-specific MCP, 然后进入 query() 循环,把子 agent 的消息逐条 yield 回父级。

5.1 SubagentStart、frontmatter hooks 和 skills 都在 agent 内部展开

启动阶段 会执行 SubagentStart hooks,把额外上下文作为 attachment message 放进 initial messages; 如果 agent frontmatter 有 hooks,会注册到该 agent lifecycle,并把 Stop 转成 SubagentStop; 如果有预加载 skills,则把 skill content 作为 meta user message 加进去。

这说明 agent 的上下文不是只由父级 prompt 决定。agent definition 自己可以带 hooks、skills、MCP 需求和系统提示词。 这些内容都会在进入 query loop 前写入子 agent 收到的提示词和消息。

5.2 Sidechain transcript 是子 agent 可恢复的前提

runAgent() 会把 initial messages 写入 sidechain transcript,然后在 query loop 中继续记录 assistant、user、progress 和压缩分界点。这个 sidechain 不是 UI 装饰,而是后面查看、恢复和统计 subagent 的基础。

同步 agent 的进度会以 agent_progress 形式回到父级;异步 agent 则注册 background task, 完成后通过 notification / task result 回到主循环。两者都不是“父级继续偷偷执行一段代码”,而是有自己的 agentId、 transcript 和清理流程。

例如父 agent 提交“检查测试失败原因”后,status: async_launched 只表示后台任务已经启动,并不表示测试已通过。稍后的任务通知或 TaskOutput 才提供完成、失败或停止状态。fork 前缀里的 placeholder tool_results 只用于补齐消息配对,也不是其他工具已成功执行的证据。

六、同步、异步、fork 三条路径怎么记

路径 系统提示词 工具池 状态关系
普通同步 subagent 按 selected agent 生成。 worker permission context + resolveAgentTools。 可共享 setAppState、abort 和响应指标,进度回到父级。
普通异步 subagent 按 selected agent 生成。 async allowed list 继续收窄。 后台 task,abort controller 不跟父级 ESC 绑定,完成后通知。
fork child 继承父级 rendered system prompt。 exact parent tools,避免工具定义变化。 复制父级 cache-safe 前缀,在尾部追加 fork directive。
worktree isolation 按路径覆盖后的上下文生成或追加 notice。 仍走 agent tool 规则。 在独立工作树里执行,结束后按是否有变更决定清理。

到这里,Claude Code 的 subagent 就不神秘了:它不是“另一个聊天窗口”,也不是“模型自己复制自己”。 它是一套明确的执行过程:选择一个 agent,决定普通还是 fork,组装工具池,隔离 mutable state, 跑独立 query loop,最后把结果、进度或通知回填给父级。

下一篇读 hooks 时,会继续看这些执行位置: SessionStart、PreToolUse、PostToolUse、SubagentStart、SubagentStop 这些生命周期插槽, 究竟能在哪些时刻修改输入、阻止工具或追加结果,以及哪些状态仍由运行时控制。

参考源码与文档