> ## Documentation Index
> Fetch the complete documentation index at: https://ccb.agent-aura.top/llms.txt
> Use this file to discover all available pages before exploring further.

# 子 Agent 机制 - 权限、流程、同步/异步与 Fork

> 从源码角度解析 Claude Code 子 Agent：AgentTool 的执行链路、权限模式、同步与异步生命周期、任务通知队列、AgentTool fork、slash command fork 与 runForkedAgent 的边界。

## 先分清四个概念

Claude Code 里常被一起称为"子 Agent"的东西，其实有四类执行路径：

| 类型                 | 谁触发                                                  | 是否经过 Tool 协议                | 结果怎么回来                                                | 典型入口                                                                      |
| ------------------ | ---------------------------------------------------- | --------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------- |
| 命名子 Agent          | 主模型调用 `Agent(...)`，并提供 `subagent_type`               | 是，属于一次 `tool_use`           | 当前 turn 的 `tool_result`，或后台完成后的 `<task-notification>` | `src/tools/AgentTool/AgentTool.tsx`                                       |
| AgentTool fork     | 主模型调用 `Agent(...)`，省略 `subagent_type`，且 fork gate 开启 | 是，仍然是 `Agent` 工具            | 先返回 `async_launched`，完成后通过任务通知回到主模型                   | `src/tools/AgentTool/AgentTool.tsx`、`src/tools/AgentTool/forkSubagent.ts` |
| Slash command fork | 用户执行 `context: fork` 的 slash command / skill         | 否，不是模型发出的 `Agent` tool\_use | 普通模式同步返回命令输出；assistant 模式后台回注隐藏 prompt                | `src/utils/processUserInput/processSlashCommand.tsx`                      |
| `runForkedAgent()` | 运行时内部服务直接分叉一条执行支线                                    | 否，内部 API                    | 调用方内部消费结果                                             | `src/utils/forkedAgent.ts`                                                |

一句话记忆：

`AgentTool` fork 是给模型使用的工具语义；`runForkedAgent()` 是给运行时内部能力使用的实现细节；slash command fork 是 skill / command 的执行模式。

## AgentTool 主流程

模型看到的 `Agent` 工具最终会进入 `AgentTool.call()`。一条普通命名子 Agent 的执行链如下：

```text theme={null}
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
```

关键源码入口：

| 代码                                            | 作用                                                          |
| --------------------------------------------- | ----------------------------------------------------------- |
| `src/tools/AgentTool/AgentTool.tsx`           | `Agent` 工具定义、路由、同步/异步生命周期                                   |
| `src/tools/AgentTool/runAgent.ts`             | 子 Agent 的 query loop、system prompt、MCP、sidechain transcript |
| `src/services/tools/toolExecution.ts`         | 外层工具执行器，`await tool.call(...)` 的地方                          |
| `src/query.ts`                                | 主 agentic loop，收集 tool results 并进入下一轮模型调用                   |
| `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | 后台本地 Agent task 的注册、状态更新、完成通知                               |

## AgentTool 输入参数

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

### 基础参数

| 参数                  | 类型                              | 必填 | 作用                                   | 影响路径                                                                                  |
| ------------------- | ------------------------------- | -- | ------------------------------------ | ------------------------------------------------------------------------------------- |
| `description`       | `string`                        | 是  | 3-5 个词的任务短描述，用于 UI、任务列表、日志、后台通知和输出摘要 | 不参与子 Agent 的实际 prompt 推理，但会影响 task 展示和通知                                              |
| `prompt`            | `string`                        | 是  | 子 Agent 要执行的完整任务说明                   | 普通 agent 会变成子 Agent 的 user message；fork path 会嵌入 fork directive；remote path 会作为远程初始消息 |
| `subagent_type`     | `string`                        | 否  | 指定命名 agent 类型                        | 有值时走命名 agent；省略时 fork gate 开启则走 AgentTool fork，否则回退到 `general-purpose`                |
| `model`             | `'sonnet' \| 'opus' \| 'haiku'` | 否  | 这次调用的模型覆盖                            | 普通命名 agent 中优先级高于 agent definition 的 `model`；coordinator mode 下忽略；fork path 继承父模型     |
| `run_in_background` | `boolean`                       | 否  | 请求后台运行                               | 为 `true` 时走异步 task；如果后台任务被禁用或 fork gate 开启，这个字段会从 schema 中隐藏                          |

### 多 Agent / Teammate 参数

| 参数          | 类型              | 必填 | 作用                                                     | 影响路径                                                                                               |
| ----------- | --------------- | -- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `name`      | `string`        | 否  | 给 spawned agent 命名，使其可被 `SendMessage({ to: name })` 定向 | 与 `team_name` 或当前 team context 一起出现时触发 teammate spawn；普通后台子 Agent 中也会注册 `name -> agentId` 方便后续发送消息 |
| `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 是扁平结构。

### 隔离与工作目录参数

| 参数          | 类型                              | 必填 | 作用                        | 影响路径                                                                  |
| ----------- | ------------------------------- | -- | ------------------------- | --------------------------------------------------------------------- |
| `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，因此调用方应视为互斥参数。

### 参数可见性与实际效果

| 参数                    | 可能不可见的情况                                                         | 说明                                                      |
| --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------- |
| `run_in_background`   | `DISABLE_BACKGROUND_TASKS` 生效，或 `isForkSubagentEnabled()` 为 true | fork gate 开启时所有 `AgentTool` spawn 都会被强制异步，所以不需要让模型再选择   |
| `cwd`                 | 非 `KAIROS` 构建 / 模式                                               | schema 会 omit 掉，但实现类型仍保留该字段                             |
| `isolation: "remote"` | 非内部构建                                                            | 外部构建只接受 `worktree`                                      |
| `model`               | coordinator mode 或 fork path                                     | coordinator 会清空 model override；fork 需要继承父模型以保持请求前缀和行为一致 |

### 参数与 agent definition 的优先级

| 配置项      | 调用参数                | agent definition               | 最终规则                                                                        |
| -------- | ------------------- | ------------------------------ | --------------------------------------------------------------------------- |
| agent 类型 | `subagent_type`     | 默认 / active agents             | 显式 `subagent_type` 优先；省略时由 fork gate 决定 fork 或 `general-purpose`            |
| 模型       | `model`             | `selectedAgent.model`          | 普通命名 agent 中调用参数优先；没有参数则用定义；再没有则继承父模型                                       |
| 后台运行     | `run_in_background` | `selectedAgent.background`     | 任一为 true 都会异步；还有 coordinator、assistant、fork gate 等强制异步条件                    |
| 隔离       | `isolation`         | `selectedAgent.isolation`      | 调用参数优先                                                                      |
| 权限模式     | 无本地覆盖参数             | `selectedAgent.permissionMode` | 普通子 Agent 用 definition 的 `permissionMode`，默认 `acceptEdits`；fork 使用 `bubble` |
| 工具集合     | 无调用参数               | `selectedAgent.tools`          | 普通子 Agent 在 `runAgent()` 里按 definition 过滤；fork 使用父级 exact tools             |

## Agent Definition 字段

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

### 常用 frontmatter

| 字段                | 类型                               | 作用                     | 运行时影响                                                     |
| ----------------- | -------------------------------- | ---------------------- | --------------------------------------------------------- |
| `name`            | `string`                         | agent 类型名              | 模型通过 `subagent_type` 匹配它；插件 agent 可能带命名空间前缀               |
| `description`     | `string`                         | 使用场景说明                 | 进入可用 agent 列表，帮助主模型选择                                     |
| `tools`           | `string[]`                       | 允许的工具集合                | `runAgent()` 内经 `resolveAgentTools()` 过滤；`['*']` 表示全量可用工具 |
| `disallowedTools` | `string[]`                       | 禁用工具集合                 | JSON agent 支持该字段，用于从允许集合中排除                               |
| `prompt`          | `string`                         | agent system prompt 主体 | 普通命名子 Agent 会用它构建自己的 system prompt                        |
| `model`           | `string`                         | 默认模型                   | 可被 `Agent({ model })` 覆盖；`inherit` 表示继承父模型                |
| `effort`          | effort level 或 number            | 推理努力级别                 | 传给 agent 运行配置                                             |
| `permissionMode`  | permission mode                  | 默认权限模式                 | 普通子 Agent 工具池组装时使用；省略则默认 `acceptEdits`                    |
| `background`      | `boolean`                        | 是否总是后台运行               | 为 true 时，即使调用参数没有 `run_in_background` 也走异步                |
| `isolation`       | `'worktree'` / `'remote'`        | 默认隔离模式                 | 可被调用参数 `isolation` 覆盖                                     |
| `maxTurns`        | positive integer                 | 最大 agentic turns       | 传给 `query()`，防止子 Agent 无限循环                               |
| `color`           | agent color                      | UI 颜色                  | 用于 grouped UI、任务面板、teammate 展示                            |
| `memory`          | `'user' \| 'project' \| 'local'` | 持久记忆作用域                | 在 system prompt 中追加 agent memory，并按 scope 读写目录            |

示例：

```md theme={null}
---
name: code-reviewer
description: Review a code change and find correctness risks
tools:
  - Read
  - Grep
  - Glob
model: sonnet
permissionMode: acceptEdits
background: true
maxTurns: 8
memory: project
---

You are a focused code reviewer. Prioritize bugs, regressions, and missing tests.
```

### MCP、Hooks、Skills

| 字段                   | 作用                     | 说明                                                           |
| -------------------- | ---------------------- | ------------------------------------------------------------ |
| `requiredMcpServers` | 启动前必须存在的 MCP server 模式 | `AgentTool.call()` 会等待 pending server，最长约 30 秒；没有可用工具则报错     |
| `mcpServers`         | agent 专属 MCP server    | `runAgent()` 初始化，生命周期跟随该子 Agent                              |
| `hooks`              | agent 生命周期内注册的 hooks   | `runAgent()` 会注册 frontmatter hooks；agent 停止时清理 session hooks |
| `skills`             | 预加载 skill 名称           | `runAgent()` 会解析并注入对应 skill；插件 skill 支持命名空间或后缀匹配             |
| `initialPrompt`      | 首个 user turn 前置内容      | 可用于启动时固定注入额外说明                                               |

这些字段属于 agent definition，不是 `Agent(...)` 调用参数。调用方不能在一次 `Agent` tool\_use 里临时传入 `tools`、`hooks` 或 `skills` 来覆盖 agent 定义。

### runAgent() 扩展点

`runAgent()` 不只是把 prompt 丢给模型。它会在进入 query loop 前后挂载一组 agent 级扩展点：

| 扩展点                   | 时机                         | 作用                                                                     |
| --------------------- | -------------------------- | ---------------------------------------------------------------------- |
| `SubagentStart` hooks | 子 Agent query loop 启动前     | 允许 hook 修改或补充启动上下文                                                     |
| frontmatter `hooks`   | agent session 初始化时注册       | 只在这个子 Agent 的 session 内生效，结束后清理                                        |
| preload `skills`      | system prompt / skill 解析阶段 | 把指定 skill 的说明和资源注入 agent 可见上下文                                         |
| agent `memory`        | system prompt 构建时          | 按 `user` / `project` / `local` scope 读取 agent memory，并追加到 agent prompt |
| sidechain transcript  | query loop 运行时             | 记录子 Agent 的独立消息链，供恢复、调试和 `SendMessage` 续跑使用                            |

这些扩展点解释了为什么同样是 `runAgent()`，不同 agent definition 会表现出不同的工具边界、启动行为和长期上下文。

## 路由规则

`AgentTool.call()` 首先决定这次调用到底要跑哪一种 agent：

```text theme={null}
subagent_type 有值
  -> 使用命名 agent

subagent_type 省略 && isForkSubagentEnabled() 为 true
  -> 使用 fork agent

subagent_type 省略 && fork gate 关闭
  -> 回退到 general-purpose
```

命名 agent 来自内置 agent、用户配置目录、插件 agent 等定义。fork agent 是代码里内置的特殊 agent，定义在 `forkSubagent.ts`，它不是普通专业角色，而是"继承父上下文的 worker"。

## 权限模型

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

### 启动权限

`AgentTool` 自身是一个工具调用，因此先经过普通工具权限系统。随后 `AgentTool.call()` 还会做 agent 级过滤：

| 检查                     | 说明                                                         |
| ---------------------- | ---------------------------------------------------------- |
| `filterDeniedAgents()` | 根据权限规则过滤被禁止的 agent 类型                                      |
| `requiredMcpServers`   | 如果 agent 声明必需 MCP server，会等待它们连接，失败或超时则停止                  |
| teammate 限制            | in-process teammate 不能继续 spawn teammate，也不能 spawn 后台 agent |
| fork 递归保护              | fork worker 里不能再次 fork                                     |

被权限规则 deny 的命名 agent 会直接报错，而不是退回到别的 agent。这样可以避免模型绕过用户或配置里的拒绝规则。

### 工具池权限

普通命名子 Agent 不直接继承父 agent 当前那一轮的工具池限制。它会用自己的权限模式重新组装工具池：

```ts theme={null}
const workerPermissionContext = {
  ...appState.toolPermissionContext,
  mode: selectedAgent.permissionMode ?? 'acceptEdits',
}

const workerTools = assembleToolPool(
  workerPermissionContext,
  appState.mcp.tools,
)
```

这里有几个重要含义：

| 维度                   | 行为                                                   |
| -------------------- | ---------------------------------------------------- |
| 默认权限模式               | 如果 agent 定义没有写 `permissionMode`，默认使用 `acceptEdits`   |
| 全局 allow / deny 规则   | 仍然来自 `appState.toolPermissionContext`                |
| agent 自己的 `tools` 字段 | 在 `runAgent()` 内通过 `resolveAgentTools()` 继续过滤        |
| MCP 工具               | 来自当前 AppState 中已经连接的 MCP 工具；agent 也可以声明专属 MCP server |

fork agent 是例外。它为了保持父子请求的 prompt cache 前缀一致，会使用父级 exact tools：

```text theme={null}
useExactTools: true
availableTools: toolUseContext.options.tools
```

因此 fork 的权限策略不是"重新组装工具池"，而是"继承父工具定义，并用 `bubble` 权限模式把权限请求上浮到父终端"。

### 权限模式速览

| 模式                  | 子 Agent 中的意义                  |
| ------------------- | ----------------------------- |
| `acceptEdits`       | 默认模式。通常允许读和编辑类安全路径，危险操作仍走权限系统 |
| `default` / 其他普通模式  | 按主权限系统规则询问或放行                 |
| `bypassPermissions` | 显式危险模式，只有用户启用跳过权限时才应出现        |
| `bubble`            | fork 专用思路：权限请求冒泡到父级会话处理       |

## 同步子 Agent

同步子 Agent 是默认路径：没有显式 `run_in_background: true`，agent 定义也没有 `background: true`，并且没有被 coordinator / assistant mode / fork gate 等机制强制异步。

同步等待发生在普通工具调用链里。外层 `toolExecution.ts` 会执行：

```ts theme={null}
const result = await tool.call(...)
```

如果这个工具是 `AgentTool`，那么 `AgentTool.call()` 会在内部跑完整个子 Agent：

```text theme={null}
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 } }
```

返回后，`mapToolResultToToolResultBlockParam()` 把 `completed` 结果转成当前 turn 的 `tool_result`。然后 `query.ts` 把这个 tool result 放进消息列表，进入下一轮模型调用。

也就是说，同步子 Agent 不通过统一队列回注结果。主模型是在这次 `Agent` tool call 上等待，直到拿到最终 `tool_result` 才继续。

### 同步子 Agent 的可后台化

同步子 Agent 注册为 foreground task，因此它可以中途被后台化。循环里会同时等待下一条子 Agent 消息和后台化信号：

```ts theme={null}
const raceResult = await Promise.race([
  nextMessagePromise.then(result => ({ type: 'message', result })),
  backgroundPromise,
])
```

如果后台化信号先到，当前前台 iterator 会被清理，新的后台 `runAgent(..., isAsync: true)` 接管剩余工作。此时 `AgentTool.call()` 不再等待最终结果，而是返回 `async_launched`，后续完成结果走任务通知队列。

## 异步子 Agent

异步子 Agent 的触发条件包括：

| 条件                          | 说明                                   |
| --------------------------- | ------------------------------------ |
| `run_in_background: true`   | 模型显式要求后台运行                           |
| agent 定义 `background: true` | 该 agent 总是后台运行                       |
| coordinator mode            | worker 统一异步，方便编排                     |
| fork subagent gate 开启       | 当前实现会强制所有 `AgentTool` spawn 使用异步通知模型 |
| assistant / kairos mode     | 避免同步子任务阻塞输入队列                        |
| proactive active            | 主动循环下也可能强制异步                         |

异步路径不会等待子 Agent 完成：

```text theme={null}
AgentTool.call()
  -> registerAsyncAgent(...)
  -> void runAsyncAgentLifecycle(...)
  -> return { status: "async_launched", agentId, outputFile }
```

后台生命周期在 `runAsyncAgentLifecycle()` 中完成：

```text theme={null}
runAsyncAgentLifecycle()
  -> for await message of runAgent(...)
  -> updateAsyncAgentProgress(...)
  -> finalizeAgentTool(...)
  -> completeAsyncAgent(...)
  -> enqueueAgentNotification(...)
```

异步 Agent 使用独立 `AbortController`。普通 ESC 取消主线程不会自动杀掉后台 Agent；后台 Agent 需要通过任务停止、bulk kill 或 task 管理命令显式结束。

## 完成通知与统一队列

后台 Agent 完成后，`enqueueAgentNotification()` 会生成一条 XML 形态的 `<task-notification>`：

```xml theme={null}
<task-notification>
  <task-id>...</task-id>
  <tool-use-id>...</tool-use-id>
  <output-file>...</output-file>
  <status>completed</status>
  <summary>Agent "..." completed</summary>
  <result>...</result>
  <usage>...</usage>
</task-notification>
```

这条消息通过 `enqueuePendingNotification({ mode: 'task-notification' })` 进入统一 command queue。

### 队列什么时候消费

| 场景                 | 消费方式                                                                                  |
| ------------------ | ------------------------------------------------------------------------------------- |
| REPL / TUI         | `useQueueProcessor()` 订阅队列；当 query 空闲且没有本地 JSX UI 阻塞时，调用 `processQueueIfReady()`      |
| CLI / SDK headless | `print.ts` 中的 `drainCommandQueue()` 在 turn 之间持续消费；如果还有后台任务运行，会继续等待并 drain 新通知         |
| 子 Agent 内部         | `query.ts` 会消费带有当前 `agentId` 的 `task-notification`，主线程只消费 `agentId === undefined` 的消息 |

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

### 还有哪些消息走同一队列

统一队列不只用于后台 Agent。常见来源包括：

| 来源                                        | mode                  | 用途                  |
| ----------------------------------------- | --------------------- | ------------------- |
| 用户在当前 turn 未结束时继续输入                       | `prompt` / `bash`     | 排队到下一轮处理            |
| 后台 shell / monitor 结束或卡住提醒                | `task-notification`   | 通知模型命令状态            |
| remote agent / ultraplan / ultrareview 完成 | `task-notification`   | 把远程结果交给本地模型         |
| scheduled task / cron                     | `prompt`              | 定时触发主模型任务           |
| Chrome / MCP channel 推送                   | `prompt`              | 外部系统主动注入消息          |
| hook 阻塞错误                                 | `task-notification`   | 唤醒模型处理 stop hook 错误 |
| orphaned permission response              | `orphaned-permission` | 处理工具权限回复比原请求更晚到达的情况 |

队列优先级是 `now > next > later`。`enqueue()` 默认 `next`，`enqueuePendingNotification()` 默认 `later`，这样系统通知不会抢在用户输入前面。

## 继续通信与任务控制

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

### SendMessage

`SendMessage` 用来给运行中或曾经启动过的 agent 追加消息。它可以通过两种地址找到本地后台 agent：

| 地址            | 来源                                             | 行为                       |
| ------------- | ---------------------------------------------- | ------------------------ |
| `name`        | `Agent({ name, ... })` 注册到 `agentNameRegistry` | 先解析成 agentId，再发送         |
| raw `agentId` | `async_launched` 或 `completed` tool result 中返回 | 直接定位对应 task 或 transcript |

发送 plain text message 时必须提供 `summary`，因为 UI 和权限摘要需要一个短描述。`to: "*"` 表示广播给 teammate team；结构化消息不能广播。

`SendMessage` 对本地后台 agent 的行为分三种：

| 目标状态                 | 行为                                              | 结果                                                                  |
| -------------------- | ----------------------------------------------- | ------------------------------------------------------------------- |
| task 仍在 `running`    | 调用 `queuePendingMessage(agentId, message, ...)` | 消息进入该 task 的 `pendingMessages`，在子 Agent 下一次 tool round / loop 边界被投递 |
| task 已停止但还在 AppState | 调用 `resumeAgentBackground(...)`                 | 用这条消息把 agent 后台恢复运行，完成后仍通过通知回来                                      |
| task 已从 AppState 清掉  | 仍尝试 `resumeAgentBackground(...)`                | 如果 sidechain transcript 还在，就从 transcript 恢复；否则返回失败                  |

这意味着 `SendMessage` 不是只能在 agent 正在跑时使用。隔了很久以后，只要调用方还知道 `name` 或 `agentId`，并且对应 transcript 没被清理，就可能恢复并继续这个 agent。反过来，如果 task 状态和 transcript 都没了，`SendMessage` 无法凭空重建上下文。

几个容易误会的点：

| 点                          | 说明                                                 |
| -------------------------- | -------------------------------------------------- |
| running agent 不会立刻中断当前工具调用 | 消息先排进 `pendingMessages`，等 agent loop 到安全边界再处理      |
| stopped agent 会变成新的后台运行    | `resumeAgentBackground()` 返回 output file，之后靠完成通知回注 |
| `name` 只在注册还在时可靠           | name registry 是运行时状态；跨很久恢复时 raw `agentId` 更稳定      |
| cross-session send 有额外限制   | `bridge:` / `uds:` 地址只支持 plain text，且可能需要显式权限或连接状态 |

### TaskOutput

`TaskOutput` 是旧式读取后台任务输出的工具，当前 prompt 明确建议优先使用 `Read` 读取任务返回的 `output_file`。它仍然可用，主要行为如下：

| 参数             | 行为             |
| -------------- | -------------- |
| `task_id`      | 要读取的后台任务 id    |
| `block: false` | 非阻塞读取当前状态和已有输出 |
| `block: true`  | 等待任务完成，默认行为    |
| `timeout`      | 阻塞等待的最大时长      |

如果 `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 会做这些清理：

| 清理                              | 作用                                    |
| ------------------------------- | ------------------------------------- |
| 清空 background hint UI           | 避免前台提示残留                              |
| `stopForegroundSummarization()` | 停止前台摘要定时器                             |
| `unregisterAgentForeground()`   | 子 Agent 未后台化时，从 foreground task 注册表移除 |
| SDK task notification           | 给 SDK / VS Code 面板发完成、失败或 stopped 事件  |
| `clearInvokedSkillsForAgent()`  | 清理 agent 作用域 skill 状态                 |
| `clearDumpState()`              | 清理 dump/transcript 调试状态               |
| `cleanupWorktreeIfNeeded()`     | 未后台化时清理或保留 worktree                   |

### 异步路径

异步路径由 `runAsyncAgentLifecycle()` 兜住异常：

| 情况           | 状态更新                      | 通知                                                          |
| ------------ | ------------------------- | ----------------------------------------------------------- |
| 正常完成         | `completeAsyncAgent(...)` | `enqueueAgentNotification(status: completed)`               |
| `AbortError` | `killAsyncAgent(...)`     | `enqueueAgentNotification(status: killed)`，带 partial result |
| 其他错误         | `failAsyncAgent(...)`     | `enqueueAgentNotification(status: failed)`，带 error          |

代码会先更新 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，例如开发时显式设置：

```powershell theme={null}
$env:FEATURE_FORK_SUBAGENT='1'; bun run dev
```

即使 feature 打开，以下场景也会强制关闭：

| 场景                      | 原因                           |
| ----------------------- | ---------------------------- |
| coordinator mode        | coordinator 已有自己的委派模型        |
| non-interactive session | pipe / SDK 场景下避免不可见的 fork 嵌套 |

### 路径

```text theme={null}
主模型
  -> Agent({ prompt })，没有 subagent_type
  -> AgentTool.call()
  -> isForkSubagentEnabled()
  -> selectedAgent = FORK_AGENT
  -> buildForkedMessages(...)
  -> runAgent(... useExactTools: true, forkContextMessages: parent messages)
  -> 注册 task / transcript / notification
```

fork 的目标是让多个 worker 共享父请求的 prompt cache 前缀。它会：

| 维度                       | fork 行为                            |
| ------------------------ | ---------------------------------- |
| system prompt            | 使用父级已经渲染好的 system prompt           |
| 对话历史                     | 传入父级完整 `toolUseContext.messages`   |
| tools                    | 使用父级 exact tools，不重新过滤             |
| thinking config          | 继承父级配置，避免 cache key 变化             |
| placeholder tool\_result | 多个 fork 使用相同占位文本，只有最后 directive 不同 |
| 权限                       | `permissionMode: 'bubble'`         |

这就是为什么 fork path 和普通 agent path 在 tool pool、prompt 构造、模型继承上都不同。

### 递归保护

fork worker 保留 `Agent` 工具是为了让工具定义字节和父级一致，但代码会拒绝 fork 内再次 fork：

| 保护                                     | 说明                      |
| -------------------------------------- | ----------------------- |
| `querySource === 'agent:builtin:fork'` | 直接识别当前已经在 fork worker 内 |
| `<fork-boilerplate>` 扫描                | 兜底识别 fork 指令已经存在于上下文    |

fork worker 应该直接完成任务，而不是继续委派。

## Slash command fork

slash command fork 是 skill / command 的执行模式。它由 skill frontmatter 控制：

```md theme={null}
---
name: code-review
context: fork
allowed-tools:
  - Read
  - Grep
  - Glob
---
```

加载 skill 时，`frontmatter.context === 'fork'` 会被解析成 command 的 `context: 'fork'`。执行 slash command 时：

```text theme={null}
用户输入 /code-review
  -> processSlashCommand(...)
  -> command.context === 'fork'
  -> executeForkedSlashCommand(...)
  -> prepareForkedCommandContext(...)
  -> runAgent(...)
```

普通交互模式下，`executeForkedSlashCommand()` 会同步跑完子 Agent，显示 progress UI，然后把结果作为本地命令输出返回给主对话。

assistant / kairos 模式下，它会 fire-and-forget：后台 runner 完成后，把结果包装成隐藏 prompt 重新放入 command queue。这样多个 scheduled task 不会在启动时串行阻塞用户输入。

## `runForkedAgent()`

`runForkedAgent()` 是内部服务用的执行器，不暴露给模型，也不产生 `Agent` tool\_result。

它的输入是 `cacheSafeParams`、`promptMessages`、`canUseTool` 等运行时对象，直接跑 query loop：

```text theme={null}
内部服务
  -> runForkedAgent({ promptMessages, cacheSafeParams, ... })
  -> createSubagentContext(...)
  -> query(...)
  -> 返回 ForkedAgentResult
```

常见调用方：

| 调用方                             | 用途           |
| ------------------------------- | ------------ |
| compact                         | 对话压缩         |
| extractMemories / sessionMemory | 记忆抽取和维护      |
| promptSuggestion / speculation  | 提示建议和预测      |
| sideQuestion                    | 不打扰主上下文的临时问答 |
| agentSummary                    | 后台 agent 摘要  |
| autoDream                       | 后台记忆整合       |

它和 AgentTool fork 的共同点是"分叉执行"，但边界完全不同：

| 维度   | AgentTool fork                            | `runForkedAgent()`  |
| ---- | ----------------------------------------- | ------------------- |
| 调用者  | 模型通过 `Agent` 工具调用                         | 运行时服务直接调用           |
| 协议层  | 经过 Tool schema / tool\_use / tool\_result | 不经过 Tool 协议         |
| 可见性  | 主模型会先看到 `async_launched`，完成后看到通知          | 结果由内部调用方处理          |
| 主要目标 | 并行 worker + prompt cache 共享               | 内部辅助任务复用 query loop |

## Worktree 隔离

`Agent` 工具支持 `isolation: "worktree"`。启用后，子 Agent 在临时 git worktree 中运行，适合实现型或实验型任务。

生命周期：

| 阶段              | 行为                                                       |
| --------------- | -------------------------------------------------------- |
| 创建              | 使用 agent id 派生 slug，创建独立 worktree                        |
| CWD 覆盖          | `runWithCwdOverride(worktreePath, fn)` 让工具在 worktree 内执行 |
| fork + worktree | 额外注入路径翻译提示，提醒 worker 重新读取文件                              |
| 清理              | 无变更则移除 worktree；有变更则保留并把路径返回给主模型                         |

如果 worktree 是 hook-based，代码会保留它，因为无法可靠判断 VCS 变更。

## 结果格式

`AgentTool.mapToolResultToToolResultBlockParam()` 根据状态返回不同 tool result：

| 状态                 | 结果                                            |
| ------------------ | --------------------------------------------- |
| `completed`        | 子 Agent 输出内容，可附带 `agentId`、worktree 信息和 usage |
| `async_launched`   | 后台 agent id、output file 路径、等待完成通知的说明          |
| `teammate_spawned` | teammate id、name、team name                    |
| `remote_launched`  | remote task id、session URL、output file        |

同步子 Agent 的 `completed` 结果直接成为当前 `Agent` tool call 的 `tool_result`。异步子 Agent 的首次 tool result 是 `async_launched`，最终输出通过 `<task-notification>` 回到模型。

### 输出字段

| 状态                 | 关键字段                                                                    | 说明                                           |
| ------------------ | ----------------------------------------------------------------------- | -------------------------------------------- |
| `completed`        | `content`、`agentId`、`totalTokens`、`totalToolUseCount`、`totalDurationMs` | 同步子 Agent 的最终结果；普通 agent 会附带可继续通信的 `agentId` |
| `async_launched`   | `agentId`、`description`、`prompt`、`outputFile`、`canReadOutputFile`       | 后台 agent 已启动；最终结果稍后通过通知到达                    |
| `teammate_spawned` | `teammate_id`、`name`、`team_name`                                        | teammate 已启动，后续通过 mailbox / SendMessage 协作   |
| `remote_launched`  | `taskId`、`sessionUrl`、`outputFile`、`description`                        | remote CCR agent 已启动，完成后走 remote task 通知     |

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

### outputSchema 与 tool\_result

`AgentTool` 的 `outputSchema` 描述的是 `call()` 返回的结构化 data；`mapToolResultToToolResultBlockParam()` 再把这些 data 映射成模型实际看到的 `tool_result` 文本块。读代码时可以按这个顺序看：

```text theme={null}
AgentTool.call()
  -> return { data: { status, ...fields } }
  -> mapToolResultToToolResultBlockParam(data, toolUseID)
  -> ToolResultBlockParam
  -> query.ts 把 tool_result 放进下一轮消息
```

四类结果的字段重点：

| status             | data 字段                                                           | 模型可见信息                                            |
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------- |
| `completed`        | `content`、`agentId`、usage、可选 worktree result                      | 子 Agent 最终输出；如果可继续通信，会提示可用 `SendMessage`          |
| `async_launched`   | `agentId`、`description`、`prompt`、`outputFile`、`canReadOutputFile` | 后台已启动；提示等待通知或读取 output file                       |
| `teammate_spawned` | `teammate_id`、`name`、`team_name`                                  | teammate 已加入 team；后续通过 mailbox / `SendMessage` 协作 |
| `remote_launched`  | `taskId`、`sessionUrl`、`outputFile`、`description`                  | remote task 已启动；本地模型等待 remote task notification   |

这里的 `status` 是结果分发的主轴。后面 catch / finally 中的 failed、killed、cleanup 逻辑不会改写已经返回的同步 `tool_result`；后台路径会通过 task state 和 notification 把终态再交给主模型。

## 生命周期状态机

把本地子 Agent 当成 task 看，核心状态可以这样理解：

```text theme={null}
AgentTool.call()
  -> resolve route
  -> create optional worktree
  -> register foreground 或 register async task
  -> runAgent()
  -> completed / failed / killed
  -> tool_result 或 task-notification
  -> cleanup agent-scoped state
```

同步和异步的差别不在于是否调用 `runAgent()`，而在于谁等待 `runAgent()`：

| 路径                                    | 谁等待                                           | 主模型什么时候继续                                |
| ------------------------------------- | --------------------------------------------- | ---------------------------------------- |
| 同步子 Agent                             | `AgentTool.call()` 自己 `for await` 子 Agent 消息流 | 子 Agent 完成并返回 `tool_result` 后            |
| 自动后台化                                 | 前台先等；超时后前台 iterator 退出，后台 lifecycle 接管        | `AgentTool.call()` 返回 `async_launched` 后 |
| 异步子 Agent                             | `runAsyncAgentLifecycle()` 在后台等               | 主模型收到 `async_launched` 后立即继续             |
| slash command fork 普通交互               | `executeForkedSlashCommand()` 等               | slash command 完成后                        |
| slash command fork assistant / kairos | fire-and-forget 后台 runner 等                   | 启动后主输入流程继续，完成后隐藏 prompt 回注               |
| `runForkedAgent()`                    | 内部调用方自己等                                      | 不进入主模型 tool\_result 协议                   |

所以“同步子 Agent 怎么等完成”最短答案是：外层工具执行器 `await tool.call()`，而 `AgentTool.call()` 内部持续消费 `runAgent()` 的 async iterator，直到 iterator `done` 或异常。

## 等待与回注方式对照

子 Agent 结果回到主模型有三种主要机制：

| 机制                                   | 适用路径                                                     | 回注载体                                  | 是否阻塞当前 turn |
| ------------------------------------ | -------------------------------------------------------- | ------------------------------------- | ----------- |
| `tool_result`                        | 同步命名子 Agent                                              | 当前 `Agent` tool\_use 对应的 tool result  | 是           |
| `<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`；最终结果是新的一条队列消息，下一轮模型看到后再决定怎么整合。

## Progress、UI 与 Transcript

子 Agent 有三条并行的“可观察输出”：给用户看的 progress、给模型看的最终结果、给系统恢复用的 transcript。

| 输出                   | 同步路径                                           | 异步路径                                                    | 用途                                      |
| -------------------- | ---------------------------------------------- | ------------------------------------------------------- | --------------------------------------- |
| progress UI          | `AgentTool.call()` 消费子 Agent 消息时实时转发给 UI / SDK | `runAsyncAgentLifecycle()` 更新 task progress state       | 让用户看到子 Agent 正在做什么                      |
| output file          | 同步路径也会写入 side output，方便调试和恢复                   | 后台 task 的主要可读输出，`async_launched` 会返回路径                  | 主模型可用 `Read(outputFile)` 查看             |
| 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 上下文的消息日志。

## 典型调用示例

### 同步命名子 Agent

```json theme={null}
{
  "description": "review parser bug",
  "prompt": "Review the parser changes and identify correctness risks.",
  "subagent_type": "code-reviewer"
}
```

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

### 后台命名子 Agent

```json theme={null}
{
  "description": "run regression suite",
  "prompt": "Run the regression tests and summarize failures.",
  "subagent_type": "general-purpose",
  "run_in_background": true
}
```

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

### 可继续通信的后台 Agent

```json theme={null}
{
  "description": "investigate flaky tests",
  "prompt": "Investigate flaky tests without editing files yet.",
  "subagent_type": "general-purpose",
  "name": "flaky-investigator",
  "run_in_background": true
}
```

后续可以用：

```json theme={null}
{
  "to": "flaky-investigator",
  "message": "Focus on the Windows-only failures and compare the last two runs.",
  "summary": "focus Windows failures"
}
```

如果时间隔得很久，优先使用 `async_launched` 或 `completed` 里返回的 raw `agentId`，因为 `name` registry 是运行时状态，而 sidechain transcript 更可能通过 `agentId` 被恢复。

### Worktree 隔离实现

```json theme={null}
{
  "description": "prototype parser fix",
  "prompt": "Implement a candidate fix in isolation and report the changed files.",
  "subagent_type": "general-purpose",
  "isolation": "worktree"
}
```

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

### AgentTool fork

```json theme={null}
{
  "description": "scan auth paths",
  "prompt": "Analyze the auth flow and report likely race conditions."
}
```

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

### Slash command fork

```md theme={null}
---
name: audit-auth
context: fork
allowed-tools:
  - Read
  - Grep
  - Glob
---

Audit the authentication flow and return only correctness risks.
```

结果流：

```text theme={null}
用户输入 /audit-auth
  -> processSlashCommand()
  -> executeForkedSlashCommand()
  -> runAgent()
  -> 普通交互：命令输出直接回到对话
  -> assistant / kairos：完成后 hidden prompt 入队，下一轮模型消费
```

## 排障清单

| 现象                           | 优先检查                                                                                              |
| ---------------------------- | ------------------------------------------------------------------------------------------------- |
| 模型看不到后台结果                    | task 是否已经 enqueue notification；队列是否在当前模式 drain；`task.notified` 是否已被 `TaskOutput(block=true)` 提前标记 |
| `SendMessage` 找不到目标          | `name` 是否还在 registry；是否可以改用 raw `agentId`；sidechain transcript 是否仍存在                              |
| 子 Agent 没有某个工具               | agent definition 的 `tools` 是否过滤掉了；MCP server 是否连接；fork path 是否用了 exact tools                      |
| 子 Agent 权限和预期不同              | 普通 agent 看 `permissionMode`；teammate 的 `mode` 不是普通子 Agent 权限覆盖；fork 看 `bubble`                    |
| fork 没触发                     | `FORK_SUBAGENT` feature 是否打开；是否在 coordinator 或 non-interactive；是否传了 `subagent_type`               |
| slash command 没有 fork        | skill frontmatter 是否写 `context: fork`；加载后 command.context 是否为 `fork`                              |
| worktree 没清理                 | 是否有未提交变更；是否 hook-based worktree；cleanup 是否被后台 task 保留到通知后处理                                       |
| `TaskOutput(block=true)` 一直等 | task 是否真的进入 terminal status；如果是 async path，确认状态更新是否发生在 classifier / cleanup 之前                    |

## 选择哪条路径

| 需求                                    | 推荐路径                    |
| ------------------------------------- | ----------------------- |
| 需要专业角色、有限上下文、明确工具集                    | 命名子 Agent               |
| 需要长任务但不阻塞主模型                          | 异步子 Agent               |
| 需要多个 worker 共享完整父上下文并最大化 prompt cache | AgentTool fork          |
| 需要把一个 slash command / skill 隔离执行      | slash command fork      |
| 运行时内部需要一段轻量分叉推理                       | `runForkedAgent()`      |
| 需要隔离文件改动                              | `isolation: "worktree"` |

## 常见误区

| 误区                                      | 正确理解                                                                                   |
| --------------------------------------- | -------------------------------------------------------------------------------------- |
| `mode` 可以覆盖普通子 Agent 权限                 | `mode` 只影响 teammate spawn 的 plan 模式；普通子 Agent 权限来自 agent definition 的 `permissionMode` |
| `SendMessage` 只能发给 running agent        | running 时排队，stopped / evicted 时会尝试从 transcript 后台恢复                                    |
| 后台 agent 完成会直接改当前 tool\_result          | 后台完成走 `<task-notification>` 队列，下一轮模型才会看到                                               |
| fork 默认开启                               | fork 默认关闭，需要 `FORK_SUBAGENT` feature，且 coordinator / non-interactive 会禁用               |
| fork 是内部 `runForkedAgent()`             | AgentTool fork 经过 Tool 协议；`runForkedAgent()` 是内部运行时 API                                |
| `cwd` 和 `isolation: "worktree"` 可以随便一起用 | schema 文案要求互斥；实现上 `cwd` 会优先覆盖运行目录，调用方应避免混用                                             |
| 读后台输出应该优先 `TaskOutput`                  | 当前提示建议优先 `Read(output_file)`；`TaskOutput` 保留兼容和阻塞等待能力                                  |

## 源码阅读路径

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

| 问题                               | 阅读顺序                                                                                                     |
| -------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `Agent(...)` 参数为什么这样生效           | `AgentTool.tsx` 的 schema -> `AgentTool.call()` 参数解构 -> 路由规则                                              |
| 普通子 Agent 为什么同步等待                | `toolExecution.ts` 的 `await tool.call()` -> `AgentTool.call()` 同步分支 -> `runAgent()`                      |
| 后台完成为什么会通知主模型                    | `registerAsyncAgent()` -> `runAsyncAgentLifecycle()` -> `enqueueAgentNotification()` -> queue processor  |
| `SendMessage` 为什么能恢复旧 agent      | `SendMessageTool.ts` 地址解析 -> `queuePendingMessage()` / `resumeAgentBackground()` -> sidechain transcript |
| fork 为什么不是普通 agent               | `isForkSubagentEnabled()` -> `FORK_AGENT` -> `buildForkedMessages()` -> `useExactTools`                  |
| slash command fork 为什么不走 Tool 协议 | skill load frontmatter -> `processSlashCommand()` -> `executeForkedSlashCommand()`                       |
| 内部 fork 为什么没有 tool result        | `runForkedAgent()` -> `query()` -> 调用方消费 `ForkedAgentResult`                                             |

## 维护提示

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

| 文件                                                   | 为什么重要                                   |
| ---------------------------------------------------- | --------------------------------------- |
| `src/tools/AgentTool/AgentTool.tsx`                  | 路由、权限、同步/异步、结果映射都在这里汇合                  |
| `src/tools/AgentTool/forkSubagent.ts`                | AgentTool fork 的 gate、FORK\_AGENT、消息构造  |
| `src/tools/AgentTool/runAgent.ts`                    | 子 Agent 真正的运行循环                         |
| `src/tasks/LocalAgentTask/LocalAgentTask.tsx`        | 后台 Agent 状态和通知                          |
| `src/utils/messageQueueManager.ts`                   | 统一 command queue                        |
| `src/utils/queueProcessor.ts`                        | REPL 队列消费规则                             |
| `src/cli/print.ts`                                   | headless / SDK 队列消费和后台等待                |
| `src/utils/processUserInput/processSlashCommand.tsx` | slash command fork                      |
| `src/utils/forkedAgent.ts`                           | 内部 `runForkedAgent()`                   |
| `src/skills/loadSkillsDir.ts`                        | skill frontmatter 中 `context: fork` 的解析 |
