Skip to main content

先分清四个概念

Claude Code 里常被一起称为”子 Agent”的东西,其实有四类执行路径: 一句话记忆: AgentTool fork 是给模型使用的工具语义;runForkedAgent() 是给运行时内部能力使用的实现细节;slash command fork 是 skill / command 的执行模式。

AgentTool 主流程

模型看到的 Agent 工具最终会进入 AgentTool.call()。一条普通命名子 Agent 的执行链如下:
关键源码入口:

AgentTool 输入参数

Agent 工具的输入 schema 定义在 AgentTool.tsxbaseInputSchema()fullInputSchema()。有些字段会被 feature gate 从模型可见 schema 中隐藏,但 call() 的实现会按统一的 AgentToolInput 类型处理这些可选字段。

基础参数

多 Agent / Teammate 参数

name + team_name 是一条独立分支:它不会进入普通 runAgent() 本地子 Agent 路径,而是调用 spawnTeammate(),返回 teammate_spawned。如果在 teammate 内继续带 name spawn teammate,会被拒绝,因为 team roster 是扁平结构。

隔离与工作目录参数

isolation 入参优先级高于 agent definition 里的 isolationcwd 的 schema 文案要求不要和 isolation: "worktree" 同时使用;实现上如果两者同时出现,cwd 会优先成为运行目录,但仍可能创建 worktree,因此调用方应视为互斥参数。

参数可见性与实际效果

参数与 agent definition 的优先级

Agent Definition 字段

AgentTool 的调用参数只描述”这一次怎么 spawn”。真正决定 agent 默认能力的是 agent definition。自定义 agent 可以来自用户 / 项目目录、JSON 配置、插件或内置定义,核心字段最终都会归一到 AgentDefinition

常用 frontmatter

示例:

MCP、Hooks、Skills

这些字段属于 agent definition,不是 Agent(...) 调用参数。调用方不能在一次 Agent tool_use 里临时传入 toolshooksskills 来覆盖 agent 定义。

runAgent() 扩展点

runAgent() 不只是把 prompt 丢给模型。它会在进入 query loop 前后挂载一组 agent 级扩展点: 这些扩展点解释了为什么同样是 runAgent(),不同 agent definition 会表现出不同的工具边界、启动行为和长期上下文。

路由规则

AgentTool.call() 首先决定这次调用到底要跑哪一种 agent:
命名 agent 来自内置 agent、用户配置目录、插件 agent 等定义。fork agent 是代码里内置的特殊 agent,定义在 forkSubagent.ts,它不是普通专业角色,而是”继承父上下文的 worker”。

权限模型

子 Agent 权限要分成三层看:能不能启动这个 agent、这个 agent 有哪些工具、工具执行时如何处理权限请求。

启动权限

AgentTool 自身是一个工具调用,因此先经过普通工具权限系统。随后 AgentTool.call() 还会做 agent 级过滤: 被权限规则 deny 的命名 agent 会直接报错,而不是退回到别的 agent。这样可以避免模型绕过用户或配置里的拒绝规则。

工具池权限

普通命名子 Agent 不直接继承父 agent 当前那一轮的工具池限制。它会用自己的权限模式重新组装工具池:
这里有几个重要含义: fork agent 是例外。它为了保持父子请求的 prompt cache 前缀一致,会使用父级 exact tools:
因此 fork 的权限策略不是”重新组装工具池”,而是”继承父工具定义,并用 bubble 权限模式把权限请求上浮到父终端”。

权限模式速览

同步子 Agent

同步子 Agent 是默认路径:没有显式 run_in_background: true,agent 定义也没有 background: true,并且没有被 coordinator / assistant mode / fork gate 等机制强制异步。 同步等待发生在普通工具调用链里。外层 toolExecution.ts 会执行:
如果这个工具是 AgentTool,那么 AgentTool.call() 会在内部跑完整个子 Agent:
返回后,mapToolResultToToolResultBlockParam()completed 结果转成当前 turn 的 tool_result。然后 query.ts 把这个 tool result 放进消息列表,进入下一轮模型调用。 也就是说,同步子 Agent 不通过统一队列回注结果。主模型是在这次 Agent tool call 上等待,直到拿到最终 tool_result 才继续。

同步子 Agent 的可后台化

同步子 Agent 注册为 foreground task,因此它可以中途被后台化。循环里会同时等待下一条子 Agent 消息和后台化信号:
如果后台化信号先到,当前前台 iterator 会被清理,新的后台 runAgent(..., isAsync: true) 接管剩余工作。此时 AgentTool.call() 不再等待最终结果,而是返回 async_launched,后续完成结果走任务通知队列。

异步子 Agent

异步子 Agent 的触发条件包括: 异步路径不会等待子 Agent 完成:
后台生命周期在 runAsyncAgentLifecycle() 中完成:
异步 Agent 使用独立 AbortController。普通 ESC 取消主线程不会自动杀掉后台 Agent;后台 Agent 需要通过任务停止、bulk kill 或 task 管理命令显式结束。

完成通知与统一队列

后台 Agent 完成后,enqueueAgentNotification() 会生成一条 XML 形态的 <task-notification>
这条消息通过 enqueuePendingNotification({ mode: 'task-notification' }) 进入统一 command queue。

队列什么时候消费

task-notification 最终会作为 user-role 消息或 attachment 进入下一轮模型上下文。模型因此能看到后台结果,并决定是否综合、继续行动或回复用户。

还有哪些消息走同一队列

统一队列不只用于后台 Agent。常见来源包括: 队列优先级是 now > next > laterenqueue() 默认 nextenqueuePendingNotification() 默认 later,这样系统通知不会抢在用户输入前面。

继续通信与任务控制

后台子 Agent 返回 async_launched 后,主模型不应该直接假装已经知道最终答案。它有三种后续操作面:发消息、读输出、停止任务。

SendMessage

SendMessage 用来给运行中或曾经启动过的 agent 追加消息。它可以通过两种地址找到本地后台 agent: 发送 plain text message 时必须提供 summary,因为 UI 和权限摘要需要一个短描述。to: "*" 表示广播给 teammate team;结构化消息不能广播。 SendMessage 对本地后台 agent 的行为分三种: 这意味着 SendMessage 不是只能在 agent 正在跑时使用。隔了很久以后,只要调用方还知道 nameagentId,并且对应 transcript 没被清理,就可能恢复并继续这个 agent。反过来,如果 task 状态和 transcript 都没了,SendMessage 无法凭空重建上下文。 几个容易误会的点:

TaskOutput

TaskOutput 是旧式读取后台任务输出的工具,当前 prompt 明确建议优先使用 Read 读取任务返回的 output_file。它仍然可用,主要行为如下: 如果 block: true 等到任务完成,TaskOutput 会把 task 标记为 notified,避免再重复发送完成通知。因为这个工具已经 deprecated,新代码和模型提示都更推荐直接读 output_file

TaskStop

TaskStop 停止运行中的后台任务。它接受 task_id,也兼容旧的 shell_id。校验规则很直接:任务必须存在且状态是 running,否则报错。 停止后会调用统一的 stopTask(),具体 task 类型再映射到各自 kill 逻辑,例如本地 agent 会 abort 自己的 AbortController,shell task 会停止进程,remote task 会走 remote 停止路径。

失败、取消与清理

子 Agent 的异常路径主要分同步和异步看。

同步路径

同步子 Agent 抛出 AbortError 时,AgentTool.call() 会把它继续抛给外层工具框架,主 turn 进入正常的中断处理。非 abort 错误会先记录;如果已经收集到 assistant 消息,会尽量 finalizeAgentTool() 返回部分结果,让主模型看到已有进展。如果完全没有 assistant 消息,则重新抛出错误。 同步 finally 会做这些清理:

异步路径

异步路径由 runAsyncAgentLifecycle() 兜住异常: 代码会先更新 task 状态,再做 handoff classifier 或 worktree cleanup 这类可能较慢的附加工作。这个顺序很重要:TaskOutput(block=true) 等待的是 task 进入 terminal status,不能被后续分类器或 git 清理卡住。 通知也有防重机制。enqueueAgentNotification() 会先原子检查并设置 task.notified;如果已经通知过,就不再重复入队。

AgentTool fork

AgentTool fork 是 Agent 工具的一种特殊路由,不是普通命名 agent。

Gate

fork 默认关闭。需要构建/运行时启用 FORK_SUBAGENT feature,例如开发时显式设置:
即使 feature 打开,以下场景也会强制关闭:

路径

fork 的目标是让多个 worker 共享父请求的 prompt cache 前缀。它会: 这就是为什么 fork path 和普通 agent path 在 tool pool、prompt 构造、模型继承上都不同。

递归保护

fork worker 保留 Agent 工具是为了让工具定义字节和父级一致,但代码会拒绝 fork 内再次 fork: fork worker 应该直接完成任务,而不是继续委派。

Slash command fork

slash command fork 是 skill / command 的执行模式。它由 skill frontmatter 控制:
加载 skill 时,frontmatter.context === 'fork' 会被解析成 command 的 context: 'fork'。执行 slash command 时:
普通交互模式下,executeForkedSlashCommand() 会同步跑完子 Agent,显示 progress UI,然后把结果作为本地命令输出返回给主对话。 assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后,把结果包装成隐藏 prompt 重新放入 command queue。这样多个 scheduled task 不会在启动时串行阻塞用户输入。

runForkedAgent()

runForkedAgent() 是内部服务用的执行器,不暴露给模型,也不产生 Agent tool_result。 它的输入是 cacheSafeParamspromptMessagescanUseTool 等运行时对象,直接跑 query loop:
常见调用方: 它和 AgentTool fork 的共同点是”分叉执行”,但边界完全不同:

Worktree 隔离

Agent 工具支持 isolation: "worktree"。启用后,子 Agent 在临时 git worktree 中运行,适合实现型或实验型任务。 生命周期: 如果 worktree 是 hook-based,代码会保留它,因为无法可靠判断 VCS 变更。

结果格式

AgentTool.mapToolResultToToolResultBlockParam() 根据状态返回不同 tool result: 同步子 Agent 的 completed 结果直接成为当前 Agent tool call 的 tool_result。异步子 Agent 的首次 tool result 是 async_launched,最终输出通过 <task-notification> 回到模型。

输出字段

一次性内置 agent 可以省略 agentId / SendMessage hint 和 usage trailer,避免把不会继续通信的信息塞进上下文。

outputSchema 与 tool_result

AgentTooloutputSchema 描述的是 call() 返回的结构化 data;mapToolResultToToolResultBlockParam() 再把这些 data 映射成模型实际看到的 tool_result 文本块。读代码时可以按这个顺序看:
四类结果的字段重点: 这里的 status 是结果分发的主轴。后面 catch / finally 中的 failed、killed、cleanup 逻辑不会改写已经返回的同步 tool_result;后台路径会通过 task state 和 notification 把终态再交给主模型。

生命周期状态机

把本地子 Agent 当成 task 看,核心状态可以这样理解:
同步和异步的差别不在于是否调用 runAgent(),而在于谁等待 runAgent() 所以“同步子 Agent 怎么等完成”最短答案是:外层工具执行器 await tool.call(),而 AgentTool.call() 内部持续消费 runAgent() 的 async iterator,直到 iterator done 或异常。

等待与回注方式对照

子 Agent 结果回到主模型有三种主要机制: 这里容易混淆的是:后台子 Agent 完成后不会“补写”原来的 tool_result。原来的 Agent tool call 已经返回了 async_launched;最终结果是新的一条队列消息,下一轮模型看到后再决定怎么整合。

Progress、UI 与 Transcript

子 Agent 有三条并行的“可观察输出”:给用户看的 progress、给模型看的最终结果、给系统恢复用的 transcript。 同步 progress 是“边跑边展示,最后一次性返回 tool_result”。异步 progress 是“边跑边写 task state,最后入队 task notification”。sidechain transcript 不等同于用户可见输出;它是系统用来重建 agent 上下文的消息日志。

典型调用示例

同步命名子 Agent

适合短任务或必须立即拿结果才能继续的任务。主模型会等到子 Agent 输出 completed

后台命名子 Agent

适合长任务。主模型先收到 async_launched,其中会包含 agentIdoutputFile。之后可以等待 <task-notification>,也可以用 Read(outputFile) 主动查看已有结果。

可继续通信的后台 Agent

后续可以用:
如果时间隔得很久,优先使用 async_launchedcompleted 里返回的 raw agentId,因为 name registry 是运行时状态,而 sidechain transcript 更可能通过 agentId 被恢复。

Worktree 隔离实现

适合让子 Agent 动手改代码但不污染主工作区。主模型拿到结果后,需要根据 worktree path 决定是否合并、复查或丢弃。

AgentTool fork

只有 fork gate 开启且省略 subagent_type 时才是 fork。fork worker 继承父上下文和 exact tools,目标是并行分析和 prompt cache 复用,不适合写成长期稳定的专业角色。

Slash command fork

结果流:

排障清单

选择哪条路径

常见误区

源码阅读路径

如果要从源码验证一条行为,建议按问题类型走不同入口:

维护提示

更新子 Agent 行为时,优先同时检查这些位置: