vault backup: 2026-06-09 23:15:17

This commit is contained in:
2026-06-09 23:15:17 +08:00
parent 31fd89aafe
commit e92c0327d9
111 changed files with 7276 additions and 8846 deletions
+188 -164
View File
@@ -1,12 +1,22 @@
---
title: "子 Agent 机制 - 权限、流程、同步/异步与 Fork"
description: "从源码角度解析 Claude Code 子 Agent:AgentTool 的执行链路、权限模式、同步与异步生命周期、任务通知队列、AgentTool fork、slash command fork 与 runForkedAgent 的边界。"
keywords: ["子 Agent", "AgentTool", "权限模式", "同步子 Agent", "异步子 Agent", "forkSubagent", "runForkedAgent"]
tags:
- 子Agent
- AgentTool
- 权限模式
- 异步
- fork
create time: 2026-06-09 22:30
---
{/* 本章目标:把子 Agent 的几条容易混淆的执行链路拆开说明,并给出源码入口。 */}
# 子 Agent 机制 - 权限、流程、同步/异步与 Fork
## 先分清四个概念
## 概述
从源码角度解析 Claude Code 子 Agent:AgentTool 的执行链路、权限模式、同步与异步生命周期、任务通知队列、AgentTool fork、slash command fork 与 runForkedAgent 的边界。
## 正文
### 先分清四个概念
Claude Code 里常被一起称为"子 Agent"的东西,其实有四类执行路径:
@@ -17,26 +27,25 @@ Claude Code 里常被一起称为"子 Agent"的东西,其实有四类执行路
| Slash command fork | 用户执行 `context: fork` 的 slash command / skill | 否,不是模型发出的 `Agent` tool_use | 普通模式同步返回命令输出;assistant 模式后台回注隐藏 prompt | `src/utils/processUserInput/processSlashCommand.tsx` |
| `runForkedAgent()` | 运行时内部服务直接分叉一条执行支线 | 否,内部 API | 调用方内部消费结果 | `src/utils/forkedAgent.ts` |
一句话记忆:
> [!tip] 一句话记忆
> `AgentTool` fork 是给模型使用的工具语义;`runForkedAgent()` 是给运行时内部能力使用的实现细节;slash command fork 是 skill / command 的执行模式。
`AgentTool` fork 是给模型使用的工具语义;`runForkedAgent()` 是给运行时内部能力使用的实现细节;slash command fork 是 skill / command 的执行模式。
## AgentTool 主流程
### AgentTool 主流程
模型看到的 `Agent` 工具最终会进入 `AgentTool.call()`。一条普通命名子 Agent 的执行链如下:
```text
assistant message
-> tool_use: Agent({ prompt, subagent_type?, run_in_background?, ... })
-> query.ts: runTools(...)
-> toolExecution.ts: await tool.call(...)
-> AgentTool.call(...)
-> resolve selectedAgent / fork path / permission mode / tool pool
-> runAgent(...)
-> finalizeAgentTool(...)
-> mapToolResultToToolResultBlockParam(...)
-> user message with tool_result
-> query.ts starts next model turn with that tool_result
```mermaid
graph TD
A["assistant message"] --> B["tool_use: Agent"]
B --> C["query.ts: runTools()"]
C --> D["toolExecution.ts: await tool.call()"]
D --> E["AgentTool.call()"]
E --> F["resolve selectedAgent / fork path / permission mode / tool pool"]
F --> G["runAgent()"]
G --> H["finalizeAgentTool()"]
H --> I["mapToolResultToToolResultBlockParam()"]
I --> J["user message with tool_result"]
J --> K["query.ts starts next model turn"]
```
关键源码入口:
@@ -49,11 +58,11 @@ assistant message
| `src/query.ts` | 主 agentic loop,收集 tool results 并进入下一轮模型调用 |
| `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | 后台本地 Agent task 的注册、状态更新、完成通知 |
## AgentTool 输入参数
### AgentTool 输入参数
`Agent` 工具的输入 schema 定义在 `AgentTool.tsx` 的 `baseInputSchema()` 和 `fullInputSchema()`。有些字段会被 feature gate 从模型可见 schema 中隐藏,但 `call()` 的实现会按统一的 `AgentToolInput` 类型处理这些可选字段。
### 基础参数
#### 基础参数
| 参数 | 类型 | 必填 | 作用 | 影响路径 |
|------|------|------|------|----------|
@@ -63,7 +72,7 @@ assistant message
| `model` | `'sonnet' \| 'opus' \| 'haiku'` | 否 | 这次调用的模型覆盖 | 普通命名 agent 中优先级高于 agent definition 的 `model`;coordinator mode 下忽略;fork path 继承父模型 |
| `run_in_background` | `boolean` | 否 | 请求后台运行 | 为 `true` 时走异步 task;如果后台任务被禁用或 fork gate 开启,这个字段会从 schema 中隐藏 |
### 多 Agent / Teammate 参数
#### 多 Agent / Teammate 参数
| 参数 | 类型 | 必填 | 作用 | 影响路径 |
|------|------|------|------|----------|
@@ -71,18 +80,20 @@ assistant message
| `team_name` | `string` | 否 | 指定要加入或使用的 team | 与 `name` 一起触发 `spawnTeammate()`;省略时可继承当前 `appState.teamContext.teamName` |
| `mode` | permission mode | 否 | teammate spawn 的权限模式提示 | 当前实现只用于 teammate 的 `plan_mode_required: spawnMode === 'plan'`;它不是普通本地子 Agent 的 `permissionMode` 覆盖 |
`name + team_name` 是一条独立分支:它不会进入普通 `runAgent()` 本地子 Agent 路径,而是调用 `spawnTeammate()`,返回 `teammate_spawned`。如果在 teammate 内继续带 `name` spawn teammate,会被拒绝,因为 team roster 是扁平结构。
> [!warning]
> `name + team_name` 是一条独立分支:它不会进入普通 `runAgent()` 本地子 Agent 路径,而是调用 `spawnTeammate()`,返回 `teammate_spawned`。如果在 teammate 内继续带 `name` spawn teammate,会被拒绝,因为 team roster 是扁平结构。
### 隔离与工作目录参数
#### 隔离与工作目录参数
| 参数 | 类型 | 必填 | 作用 | 影响路径 |
|------|------|------|------|----------|
| `isolation` | `'worktree'`,内部构建还支持 `'remote'` | 否 | 覆盖 agent definition 的隔离模式 | `worktree` 创建临时 git worktree;`remote` 委派到 CCR,直接返回 `remote_launched` |
| `cwd` | `string` | 否 | 指定子 Agent 的运行目录 | 仅在 `KAIROS` schema 中暴露;会通过 `runWithCwdOverride()` 改变文件和 shell 操作的 cwd |
`isolation` 入参优先级高于 agent definition 里的 `isolation`。`cwd` 的 schema 文案要求不要和 `isolation: "worktree"` 同时使用;实现上如果两者同时出现,`cwd` 会优先成为运行目录,但仍可能创建 worktree,因此调用方应视为互斥参数。
> [!warning]
> `isolation` 入参优先级高于 agent definition 里的 `isolation`。`cwd` 的 schema 文案要求不要和 `isolation: "worktree"` 同时使用;实现上如果两者同时出现,`cwd` 会优先成为运行目录,但仍可能创建 worktree,因此调用方应视为互斥参数。
### 参数可见性与实际效果
#### 参数可见性与实际效果
| 参数 | 可能不可见的情况 | 说明 |
|------|------------------|------|
@@ -91,7 +102,7 @@ assistant message
| `isolation: "remote"` | 非内部构建 | 外部构建只接受 `worktree` |
| `model` | coordinator mode 或 fork path | coordinator 会清空 model override;fork 需要继承父模型以保持请求前缀和行为一致 |
### 参数与 agent definition 的优先级
#### 参数与 agent definition 的优先级
| 配置项 | 调用参数 | agent definition | 最终规则 |
|--------|----------|------------------|----------|
@@ -102,11 +113,11 @@ assistant message
| 权限模式 | 无本地覆盖参数 | `selectedAgent.permissionMode` | 普通子 Agent 用 definition 的 `permissionMode`,默认 `acceptEdits`;fork 使用 `bubble` |
| 工具集合 | 无调用参数 | `selectedAgent.tools` | 普通子 Agent 在 `runAgent()` 里按 definition 过滤;fork 使用父级 exact tools |
## Agent Definition 字段
### Agent Definition 字段
`AgentTool` 的调用参数只描述"这一次怎么 spawn"。真正决定 agent 默认能力的是 agent definition。自定义 agent 可以来自用户 / 项目目录、JSON 配置、插件或内置定义,核心字段最终都会归一到 `AgentDefinition`。
### 常用 frontmatter
#### 常用 frontmatter
| 字段 | 类型 | 作用 | 运行时影响 |
|------|------|------|------------|
@@ -144,7 +155,7 @@ memory: project
You are a focused code reviewer. Prioritize bugs, regressions, and missing tests.
```
### MCP、Hooks、Skills
#### MCP、Hooks、Skills
| 字段 | 作用 | 说明 |
|------|------|------|
@@ -154,9 +165,10 @@ You are a focused code reviewer. Prioritize bugs, regressions, and missing tests
| `skills` | 预加载 skill 名称 | `runAgent()` 会解析并注入对应 skill;插件 skill 支持命名空间或后缀匹配 |
| `initialPrompt` | 首个 user turn 前置内容 | 可用于启动时固定注入额外说明 |
这些字段属于 agent definition,不是 `Agent(...)` 调用参数。调用方不能在一次 `Agent` tool_use 里临时传入 `tools`、`hooks` 或 `skills` 来覆盖 agent 定义。
> [!info]
> 这些字段属于 agent definition,不是 `Agent(...)` 调用参数。调用方不能在一次 `Agent` tool_use 里临时传入 `tools`、`hooks` 或 `skills` 来覆盖 agent 定义。
### runAgent() 扩展点
#### runAgent() 扩展点
`runAgent()` 不只是把 prompt 丢给模型。它会在进入 query loop 前后挂载一组 agent 级扩展点:
@@ -170,28 +182,26 @@ You are a focused code reviewer. Prioritize bugs, regressions, and missing tests
这些扩展点解释了为什么同样是 `runAgent()`,不同 agent definition 会表现出不同的工具边界、启动行为和长期上下文。
## 路由规则
### 路由规则
`AgentTool.call()` 首先决定这次调用到底要跑哪一种 agent:
```text
subagent_type 有值
-> 使用命名 agent
subagent_type 省略 && isForkSubagentEnabled() 为 true
-> 使用 fork agent
subagent_type 省略 && fork gate 关闭
-> 回退到 general-purpose
```mermaid
graph TD
A["AgentTool.call()"] --> B{"subagent_type 有值?"}
B -->|是| C["使用命名 agent"]
B -->|否| D{"isForkSubagentEnabled()?"}
D -->|是| E["使用 fork agent"]
D -->|否| F["回退到 general-purpose"]
```
命名 agent 来自内置 agent、用户配置目录、插件 agent 等定义。fork agent 是代码里内置的特殊 agent,定义在 `forkSubagent.ts`,它不是普通专业角色,而是"继承父上下文的 worker"。
## 权限模型
### 权限模型
子 Agent 权限要分成三层看:能不能启动这个 agent、这个 agent 有哪些工具、工具执行时如何处理权限请求。
### 启动权限
#### 启动权限
`AgentTool` 自身是一个工具调用,因此先经过普通工具权限系统。随后 `AgentTool.call()` 还会做 agent 级过滤:
@@ -202,9 +212,10 @@ subagent_type 省略 && fork gate 关闭
| teammate 限制 | in-process teammate 不能继续 spawn teammate,也不能 spawn 后台 agent |
| fork 递归保护 | fork worker 里不能再次 fork |
被权限规则 deny 的命名 agent 会直接报错,而不是退回到别的 agent。这样可以避免模型绕过用户或配置里的拒绝规则。
> [!info]
> 被权限规则 deny 的命名 agent 会直接报错,而不是退回到别的 agent。这样可以避免模型绕过用户或配置里的拒绝规则。
### 工具池权限
#### 工具池权限
普通命名子 Agent 不直接继承父 agent 当前那一轮的工具池限制。它会用自己的权限模式重新组装工具池:
@@ -238,7 +249,7 @@ availableTools: toolUseContext.options.tools
因此 fork 的权限策略不是"重新组装工具池",而是"继承父工具定义,并用 `bubble` 权限模式把权限请求上浮到父终端"。
### 权限模式速览
#### 权限模式速览
| 模式 | 子 Agent 中的意义 |
|------|------------------|
@@ -247,7 +258,7 @@ availableTools: toolUseContext.options.tools
| `bypassPermissions` | 显式危险模式,只有用户启用跳过权限时才应出现 |
| `bubble` | fork 专用思路:权限请求冒泡到父级会话处理 |
## 同步子 Agent
### 同步子 Agent
同步子 Agent 是默认路径:没有显式 `run_in_background: true`,agent 定义也没有 `background: true`,并且没有被 coordinator / assistant mode / fork gate 等机制强制异步。
@@ -259,23 +270,25 @@ const result = await tool.call(...)
如果这个工具是 `AgentTool`,那么 `AgentTool.call()` 会在内部跑完整个子 Agent:
```text
AgentTool.call()
-> agentIterator = runAgent(...)[Symbol.asyncIterator]()
-> while true:
await agentIterator.next()
收集 assistant / user 消息
转发 progress 给 UI / SDK
如果 result.done,跳出
-> finalizeAgentTool(agentMessages, ...)
-> return { data: { status: "completed", ...agentResult } }
```mermaid
graph TD
A["AgentTool.call()"] --> B["agentIterator = runAgent()"]
B --> C{"while true"}
C --> D["await agentIterator.next()"]
D --> E["收集 assistant / user 消息"]
E --> F["转发 progress 给 UI / SDK"]
F --> G{"result.done?"}
G -->|否| D
G -->|是| H["finalizeAgentTool()"]
H --> I["return completed"]
```
返回后,`mapToolResultToToolResultBlockParam()` 把 `completed` 结果转成当前 turn 的 `tool_result`。然后 `query.ts` 把这个 tool result 放进消息列表,进入下一轮模型调用。
也就是说,同步子 Agent 不通过统一队列回注结果。主模型是在这次 `Agent` tool call 上等待,直到拿到最终 `tool_result` 才继续。
> [!tip]
> 同步子 Agent 不通过统一队列回注结果。主模型是在这次 `Agent` tool call 上等待,直到拿到最终 `tool_result` 才继续。
### 同步子 Agent 的可后台化
#### 同步子 Agent 的可后台化
同步子 Agent 注册为 foreground task,因此它可以中途被后台化。循环里会同时等待下一条子 Agent 消息和后台化信号:
@@ -288,7 +301,7 @@ const raceResult = await Promise.race([
如果后台化信号先到,当前前台 iterator 会被清理,新的后台 `runAgent(..., isAsync: true)` 接管剩余工作。此时 `AgentTool.call()` 不再等待最终结果,而是返回 `async_launched`,后续完成结果走任务通知队列。
## 异步子 Agent
### 异步子 Agent
异步子 Agent 的触发条件包括:
@@ -303,27 +316,23 @@ const raceResult = await Promise.race([
异步路径不会等待子 Agent 完成:
```text
AgentTool.call()
-> registerAsyncAgent(...)
-> void runAsyncAgentLifecycle(...)
-> return { status: "async_launched", agentId, outputFile }
```mermaid
graph TD
A["AgentTool.call()"] --> B["registerAsyncAgent()"]
B --> C["runAsyncAgentLifecycle()"]
C --> D["return async_launched"]
D --> E["后台生命周期继续"]
E --> F["for await message of runAgent()"]
F --> G["updateAsyncAgentProgress()"]
G --> H["finalizeAgentTool()"]
H --> I["completeAsyncAgent()"]
I --> J["enqueueAgentNotification()"]
```
后台生命周期在 `runAsyncAgentLifecycle()` 中完成:
> [!warning]
> 异步 Agent 使用独立 `AbortController`。普通 ESC 取消主线程不会自动杀掉后台 Agent;后台 Agent 需要通过任务停止、bulk kill 或 task 管理命令显式结束。
```text
runAsyncAgentLifecycle()
-> for await message of runAgent(...)
-> updateAsyncAgentProgress(...)
-> finalizeAgentTool(...)
-> completeAsyncAgent(...)
-> enqueueAgentNotification(...)
```
异步 Agent 使用独立 `AbortController`。普通 ESC 取消主线程不会自动杀掉后台 Agent;后台 Agent 需要通过任务停止、bulk kill 或 task 管理命令显式结束。
## 完成通知与统一队列
### 完成通知与统一队列
后台 Agent 完成后,`enqueueAgentNotification()` 会生成一条 XML 形态的 `<task-notification>`:
@@ -341,7 +350,7 @@ runAsyncAgentLifecycle()
这条消息通过 `enqueuePendingNotification({ mode: 'task-notification' })` 进入统一 command queue。
### 队列什么时候消费
#### 队列什么时候消费
| 场景 | 消费方式 |
|------|----------|
@@ -351,7 +360,7 @@ runAsyncAgentLifecycle()
`task-notification` 最终会作为 user-role 消息或 attachment 进入下一轮模型上下文。模型因此能看到后台结果,并决定是否综合、继续行动或回复用户。
### 还有哪些消息走同一队列
#### 还有哪些消息走同一队列
统一队列不只用于后台 Agent。常见来源包括:
@@ -367,11 +376,11 @@ runAsyncAgentLifecycle()
队列优先级是 `now > next > later`。`enqueue()` 默认 `next`,`enqueuePendingNotification()` 默认 `later`,这样系统通知不会抢在用户输入前面。
## 继续通信与任务控制
### 继续通信与任务控制
后台子 Agent 返回 `async_launched` 后,主模型不应该直接假装已经知道最终答案。它有三种后续操作面:发消息、读输出、停止任务。
### SendMessage
#### SendMessage
`SendMessage` 用来给运行中或曾经启动过的 agent 追加消息。它可以通过两种地址找到本地后台 agent:
@@ -401,7 +410,7 @@ runAsyncAgentLifecycle()
| `name` 只在注册还在时可靠 | name registry 是运行时状态;跨很久恢复时 raw `agentId` 更稳定 |
| cross-session send 有额外限制 | `bridge:` / `uds:` 地址只支持 plain text,且可能需要显式权限或连接状态 |
### TaskOutput
#### TaskOutput
`TaskOutput` 是旧式读取后台任务输出的工具,当前 prompt 明确建议优先使用 `Read` 读取任务返回的 `output_file`。它仍然可用,主要行为如下:
@@ -412,19 +421,20 @@ runAsyncAgentLifecycle()
| `block: true` | 等待任务完成,默认行为 |
| `timeout` | 阻塞等待的最大时长 |
如果 `block: true` 等到任务完成,`TaskOutput` 会把 task 标记为 `notified`,避免再重复发送完成通知。因为这个工具已经 deprecated,新代码和模型提示都更推荐直接读 `output_file`。
> [!tip]
> 如果 `block: true` 等到任务完成,`TaskOutput` 会把 task 标记为 `notified`,避免再重复发送完成通知。因为这个工具已经 deprecated,新代码和模型提示都更推荐直接读 `output_file`。
### TaskStop
#### 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 消息,则重新抛出错误。
@@ -440,7 +450,7 @@ runAsyncAgentLifecycle()
| `clearDumpState()` | 清理 dump/transcript 调试状态 |
| `cleanupWorktreeIfNeeded()` | 未后台化时清理或保留 worktree |
### 异步路径
#### 异步路径
异步路径由 `runAsyncAgentLifecycle()` 兜住异常:
@@ -454,11 +464,11 @@ runAsyncAgentLifecycle()
通知也有防重机制。`enqueueAgentNotification()` 会先原子检查并设置 `task.notified`;如果已经通知过,就不再重复入队。
## AgentTool fork
### AgentTool fork
AgentTool fork 是 `Agent` 工具的一种特殊路由,不是普通命名 agent。
### Gate
#### Gate
fork 默认关闭。需要构建/运行时启用 `FORK_SUBAGENT` feature,例如开发时显式设置:
@@ -473,17 +483,17 @@ $env:FEATURE_FORK_SUBAGENT='1'; bun run dev
| coordinator mode | coordinator 已有自己的委派模型 |
| non-interactive session | pipe / SDK 场景下避免不可见的 fork 嵌套 |
### 路径
#### 路径
```text
主模型
-> Agent({ prompt }),没有 subagent_type
-> AgentTool.call()
-> isForkSubagentEnabled()
-> selectedAgent = FORK_AGENT
-> buildForkedMessages(...)
-> runAgent(... useExactTools: true, forkContextMessages: parent messages)
-> 注册 task / transcript / notification
```mermaid
graph TD
A["主模型"] --> B["AgentTool.call()"]
B --> C{"isForkSubagentEnabled()?"}
C -->|否| D["回退命名 agent"]
C -->|是| E["selectedAgent = FORK_AGENT"]
E --> F["buildForkedMessages()"]
F --> G["runAgent() useExactTools: true"]
G --> H["注册 task / transcript / notification"]
```
fork 的目标是让多个 worker 共享父请求的 prompt cache 前缀。它会:
@@ -499,7 +509,7 @@ fork 的目标是让多个 worker 共享父请求的 prompt cache 前缀。它
这就是为什么 fork path 和普通 agent path 在 tool pool、prompt 构造、模型继承上都不同。
### 递归保护
#### 递归保护
fork worker 保留 `Agent` 工具是为了让工具定义字节和父级一致,但代码会拒绝 fork 内再次 fork:
@@ -510,7 +520,7 @@ fork worker 保留 `Agent` 工具是为了让工具定义字节和父级一致
fork worker 应该直接完成任务,而不是继续委派。
## Slash command fork
### Slash command fork
slash command fork 是 skill / command 的执行模式。它由 skill frontmatter 控制:
@@ -527,31 +537,30 @@ allowed-tools:
加载 skill 时,`frontmatter.context === 'fork'` 会被解析成 command 的 `context: 'fork'`。执行 slash command 时:
```text
用户输入 /code-review
-> processSlashCommand(...)
-> command.context === 'fork'
-> executeForkedSlashCommand(...)
-> prepareForkedCommandContext(...)
-> runAgent(...)
```mermaid
graph TD
A["用户输入 /code-review"] --> B["processSlashCommand()"]
B --> C{"command.context === fork?"}
C -->|是| D["executeForkedSlashCommand()"]
D --> E["prepareForkedCommandContext()"]
E --> F["runAgent()"]
F --> G{"普通交互?"}
G -->|是| H["同步完成,返回命令输出"]
G -->|否| I["fire-and-forget,完成后 hidden prompt 入队"]
```
普通交互模式下,`executeForkedSlashCommand()` 会同步跑完子 Agent,显示 progress UI,然后把结果作为本地命令输出返回给主对话。
assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后,把结果包装成隐藏 prompt 重新放入 command queue。这样多个 scheduled task 不会在启动时串行阻塞用户输入。
## `runForkedAgent()`
### runForkedAgent()
`runForkedAgent()` 是内部服务用的执行器,不暴露给模型,也不产生 `Agent` tool_result。
它的输入是 `cacheSafeParams`、`promptMessages`、`canUseTool` 等运行时对象,直接跑 query loop:
```text
内部服务
-> runForkedAgent({ promptMessages, cacheSafeParams, ... })
-> createSubagentContext(...)
-> query(...)
-> 返回 ForkedAgentResult
```mermaid
graph TD
A["内部服务"] --> B["runForkedAgent()"]
B --> C["createSubagentContext()"]
C --> D["query()"]
D --> E["返回 ForkedAgentResult"]
```
常见调用方:
@@ -574,7 +583,7 @@ assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后
| 可见性 | 主模型会先看到 `async_launched`,完成后看到通知 | 结果由内部调用方处理 |
| 主要目标 | 并行 worker + prompt cache 共享 | 内部辅助任务复用 query loop |
## Worktree 隔离
### Worktree 隔离
`Agent` 工具支持 `isolation: "worktree"`。启用后,子 Agent 在临时 git worktree 中运行,适合实现型或实验型任务。
@@ -587,9 +596,10 @@ assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后
| fork + worktree | 额外注入路径翻译提示,提醒 worker 重新读取文件 |
| 清理 | 无变更则移除 worktree;有变更则保留并把路径返回给主模型 |
如果 worktree 是 hook-based,代码会保留它,因为无法可靠判断 VCS 变更。
> [!info]
> 如果 worktree 是 hook-based,代码会保留它,因为无法可靠判断 VCS 变更。
## 结果格式
### 结果格式
`AgentTool.mapToolResultToToolResultBlockParam()` 根据状态返回不同 tool result:
@@ -602,7 +612,7 @@ assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后
同步子 Agent 的 `completed` 结果直接成为当前 `Agent` tool call 的 `tool_result`。异步子 Agent 的首次 tool result 是 `async_launched`,最终输出通过 `<task-notification>` 回到模型。
### 输出字段
#### 输出字段
| 状态 | 关键字段 | 说明 |
|------|----------|------|
@@ -613,7 +623,7 @@ assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后
一次性内置 agent 可以省略 `agentId` / `SendMessage` hint 和 usage trailer,避免把不会继续通信的信息塞进上下文。
### outputSchema 与 tool_result
#### outputSchema 与 tool_result
`AgentTool` 的 `outputSchema` 描述的是 `call()` 返回的结构化 data;`mapToolResultToToolResultBlockParam()` 再把这些 data 映射成模型实际看到的 `tool_result` 文本块。读代码时可以按这个顺序看:
@@ -636,19 +646,23 @@ AgentTool.call()
这里的 `status` 是结果分发的主轴。后面 catch / finally 中的 failed、killed、cleanup 逻辑不会改写已经返回的同步 `tool_result`;后台路径会通过 task state 和 notification 把终态再交给主模型。
## 生命周期状态机
### 生命周期状态机
把本地子 Agent 当成 task 看,核心状态可以这样理解:
```text
AgentTool.call()
-> resolve route
-> create optional worktree
-> register foreground 或 register async task
-> runAgent()
-> completed / failed / killed
-> tool_result 或 task-notification
-> cleanup agent-scoped state
```mermaid
graph TD
A["AgentTool.call()"] --> B["resolve route"]
B --> C["create optional worktree"]
C --> D["register foreground 或 register async task"]
D --> E["runAgent()"]
E --> F{"结果"}
F -->|completed| G["tool_result"]
F -->|failed| H["task-notification failed"]
F -->|killed| I["task-notification killed"]
G --> J["cleanup agent-scoped state"]
H --> J
I --> J
```
同步和异步的差别不在于是否调用 `runAgent()`,而在于谁等待 `runAgent()`:
@@ -662,9 +676,10 @@ AgentTool.call()
| slash command fork assistant / kairos | fire-and-forget 后台 runner 等 | 启动后主输入流程继续,完成后隐藏 prompt 回注 |
| `runForkedAgent()` | 内部调用方自己等 | 不进入主模型 tool_result 协议 |
所以“同步子 Agent 怎么等完成”最短答案是:外层工具执行器 `await tool.call()`,而 `AgentTool.call()` 内部持续消费 `runAgent()` 的 async iterator,直到 iterator `done` 或异常。
> [!question] 同步子 Agent 怎么等完成?
> 最短答案是:外层工具执行器 `await tool.call()`,而 `AgentTool.call()` 内部持续消费 `runAgent()` 的 async iterator,直到 iterator `done` 或异常。
## 等待与回注方式对照
### 等待与回注方式对照
子 Agent 结果回到主模型有三种主要机制:
@@ -674,11 +689,12 @@ AgentTool.call()
| `<task-notification>` | 异步 / 后台本地 Agent、remote task、后台 shell 等 | 统一 command queue 中的 task notification | 否 |
| hidden prompt / command queue prompt | assistant / kairos 的 slash command fork、scheduled task 等 | queue 中的 prompt 类消息 | 否 |
这里容易混淆的是:后台子 Agent 完成后不会“补写”原来的 `tool_result`。原来的 `Agent` tool call 已经返回了 `async_launched`;最终结果是新的一条队列消息,下一轮模型看到后再决定怎么整合。
> [!warning]
> 后台子 Agent 完成后不会"补写"原来的 `tool_result`。原来的 `Agent` tool call 已经返回了 `async_launched`;最终结果是新的一条队列消息,下一轮模型看到后再决定怎么整合。
## Progress、UI 与 Transcript
### Progress、UI 与 Transcript
子 Agent 有三条并行的“可观察输出”:给用户看的 progress、给模型看的最终结果、给系统恢复用的 transcript。
子 Agent 有三条并行的"可观察输出":给用户看的 progress、给模型看的最终结果、给系统恢复用的 transcript。
| 输出 | 同步路径 | 异步路径 | 用途 |
|------|----------|----------|------|
@@ -687,11 +703,11 @@ AgentTool.call()
| sidechain transcript | `runAgent()` 记录独立消息链 | 同样记录,且用于后台恢复 | `SendMessage`、resume、debug、summary 都依赖它 |
| task state | foreground task 注册表记录同步运行状态 | LocalAgentTask 记录 running / completed / failed / killed | UI、`TaskOutput`、通知防重都看这里 |
同步 progress 是“边跑边展示,最后一次性返回 tool_result”。异步 progress 是“边跑边写 task state,最后入队 task notification”。sidechain transcript 不等同于用户可见输出;它是系统用来重建 agent 上下文的消息日志。
同步 progress 是"边跑边展示,最后一次性返回 tool_result"。异步 progress 是"边跑边写 task state,最后入队 task notification"。sidechain transcript 不等同于用户可见输出;它是系统用来重建 agent 上下文的消息日志。
## 典型调用示例
### 典型调用示例
### 同步命名子 Agent
#### 同步命名子 Agent
```json
{
@@ -703,7 +719,7 @@ AgentTool.call()
适合短任务或必须立即拿结果才能继续的任务。主模型会等到子 Agent 输出 `completed`。
### 后台命名子 Agent
#### 后台命名子 Agent
```json
{
@@ -716,7 +732,7 @@ AgentTool.call()
适合长任务。主模型先收到 `async_launched`,其中会包含 `agentId` 和 `outputFile`。之后可以等待 `<task-notification>`,也可以用 `Read(outputFile)` 主动查看已有结果。
### 可继续通信的后台 Agent
#### 可继续通信的后台 Agent
```json
{
@@ -738,9 +754,10 @@ AgentTool.call()
}
```
如果时间隔得很久,优先使用 `async_launched` 或 `completed` 里返回的 raw `agentId`,因为 `name` registry 是运行时状态,而 sidechain transcript 更可能通过 `agentId` 被恢复。
> [!tip]
> 如果时间隔得很久,优先使用 `async_launched` 或 `completed` 里返回的 raw `agentId`,因为 `name` registry 是运行时状态,而 sidechain transcript 更可能通过 `agentId` 被恢复。
### Worktree 隔离实现
#### Worktree 隔离实现
```json
{
@@ -753,7 +770,7 @@ AgentTool.call()
适合让子 Agent 动手改代码但不污染主工作区。主模型拿到结果后,需要根据 worktree path 决定是否合并、复查或丢弃。
### AgentTool fork
#### AgentTool fork
```json
{
@@ -764,7 +781,7 @@ AgentTool.call()
只有 fork gate 开启且省略 `subagent_type` 时才是 fork。fork worker 继承父上下文和 exact tools,目标是并行分析和 prompt cache 复用,不适合写成长期稳定的专业角色。
### Slash command fork
#### Slash command fork
```md
---
@@ -781,16 +798,18 @@ Audit the authentication flow and return only correctness risks.
结果流:
```text
用户输入 /audit-auth
-> processSlashCommand()
-> executeForkedSlashCommand()
-> runAgent()
-> 普通交互:命令输出直接回到对话
-> assistant / kairos:完成后 hidden prompt 入队,下一轮模型消费
```mermaid
graph TD
A["用户输入 /audit-auth"] --> B["processSlashCommand()"]
B --> C["executeForkedSlashCommand()"]
C --> D["runAgent()"]
D --> E{"普通交互?"}
E -->|是| F["命令输出直接回到对话"]
E -->|否| G["完成后 hidden prompt 入队"]
G --> H["下一轮模型消费"]
```
## 排障清单
### 排障清单
| 现象 | 优先检查 |
|------|----------|
@@ -803,7 +822,7 @@ Audit the authentication flow and return only correctness risks.
| worktree 没清理 | 是否有未提交变更;是否 hook-based worktree;cleanup 是否被后台 task 保留到通知后处理 |
| `TaskOutput(block=true)` 一直等 | task 是否真的进入 terminal status;如果是 async path,确认状态更新是否发生在 classifier / cleanup 之前 |
## 选择哪条路径
### 选择哪条路径
| 需求 | 推荐路径 |
|------|----------|
@@ -814,7 +833,7 @@ Audit the authentication flow and return only correctness risks.
| 运行时内部需要一段轻量分叉推理 | `runForkedAgent()` |
| 需要隔离文件改动 | `isolation: "worktree"` |
## 常见误区
### 常见误区
| 误区 | 正确理解 |
|------|----------|
@@ -826,7 +845,7 @@ Audit the authentication flow and return only correctness risks.
| `cwd` 和 `isolation: "worktree"` 可以随便一起用 | schema 文案要求互斥;实现上 `cwd` 会优先覆盖运行目录,调用方应避免混用 |
| 读后台输出应该优先 `TaskOutput` | 当前提示建议优先 `Read(output_file)`;`TaskOutput` 保留兼容和阻塞等待能力 |
## 源码阅读路径
### 源码阅读路径
如果要从源码验证一条行为,建议按问题类型走不同入口:
@@ -840,7 +859,7 @@ Audit the authentication flow and return only correctness risks.
| slash command fork 为什么不走 Tool 协议 | skill load frontmatter -> `processSlashCommand()` -> `executeForkedSlashCommand()` |
| 内部 fork 为什么没有 tool result | `runForkedAgent()` -> `query()` -> 调用方消费 `ForkedAgentResult` |
## 维护提示
### 维护提示
更新子 Agent 行为时,优先同时检查这些位置:
@@ -856,3 +875,8 @@ Audit the authentication flow and return only correctness risks.
| `src/utils/processUserInput/processSlashCommand.tsx` | slash command fork |
| `src/utils/forkedAgent.ts` | 内部 `runForkedAgent()` |
| `src/skills/loadSkillsDir.ts` | skill frontmatter 中 `context: fork` 的解析 |
## 关联笔记
- [[coordinator-and-swarm]]
- [[worktree-isolation]]