---
url: /wave-agent/specs/multi-agent/subagent.md
description: 加载 `.wave/agents/` 子代理并经 `Agent` 工具委派，支持自动委托、`@` 显式点名、上下文隔离与 worktree 隔离
---

# 功能规格说明：子代理支持

**创建日期**：2024-12-19

## 用户场景与测试 *（必填）*

### 用户故事：加载和解析用户创建的子代理（优先级：P1）

作为开发者，我希望 Wave Agent SDK 自动发现和加载我手动创建的子代理配置文件，以便我可以使用具有领域专业知识和适当工具访问权限的专门 AI 助手。

**为什么是这个优先级**：这是子代理系统的基础 - SDK 必须能够在任何委托发生之前发现、加载和解析用户创建的子代理配置。SDK 专注于读取能力，而用户通过首选编辑器管理创建。

**独立测试**：可以通过在正确位置手动创建带有有效 YAML frontmatter 的子代理配置文件并验证它可以被 SDK 发现、加载和解析来完整测试。

**验收场景**：

1. **假设** 我在 `.wave/agents/` 中手动创建了带有有效 YAML frontmatter 的 markdown 文件，**当** SDK 扫描子代理，**则** 子代理被发现并可用于使用
2. **假设** 我在 `~/.wave/agents/` 中有一个用户级子代理，**当** SDK 加载配置，**则** 该子代理在所有项目中可用
3. **假设** 我有同名的项目和用户子代理，**当** SDK 加载配置，**则** 项目级子代理优先
4. **假设** 我手动配置了带有特定工具的子代理，**当** 子代理被调用，**则** 它只能访问这些指定的工具
5. **假设** 我手动配置了带有特定模型的子代理，**当** 子代理被调用，**则** 它使用指定模型进行响应
6. **假设** 子代理的 frontmatter 用 YAML 块标量写多行值（`>`／`>-`／`|`／`|-` 等），**当** SDK 加载配置，**则** `description`／`model` 等字段必须是折叠／保留后的真实文本（不含 `>`、`|`、`-` 这类指示符），续行**不得**被解析成独立的 frontmatter 键；子代理列表与自动委托判据看到的描述都是解析后的结果

***

### 用户故事：自动任务委托（优先级：P2）

作为用户，我希望 Wave Agent 自动识别任务何时匹配子代理的专业领域并适当委托，这样我无需手动指定使用哪个子代理就能获得专业帮助。

**为什么是这个优先级**：这提供了使子代理真正有用的智能自动化 - 用户不必为每个任务手动管理使用哪个子代理。

**独立测试**：可以通过为子代理配置清晰的专业描述并发出匹配这些描述的任务，然后验证自动委托发生来测试。

**验收场景**：

1. **假设** 我配置了 test-runner 子代理，**当** 我要求"修复失败的测试"，**则** 任务自动委托给 test-runner 子代理
2. **假设** 我有多个具有不同专业领域的子代理，**当** 我描述一个任务，**则** 根据任务描述和子代理描述选择最合适的子代理
3. **假设** 我有一个描述中包含 "PROACTIVELY" 的子代理，**当** 提到任何相关任务，**则** 系统优先委托给该子代理
4. **假设** 没有子代理匹配我的任务描述，**当** 我请求帮助，**则** 主代理直接处理任务

***

### 用户故事：显式子代理调用（优先级：P3）

作为用户，我希望在输入框里用 `@` 直接点名一个子代理，以便把这条任务明确交给它，而不是完全依赖主代理自己判断该用谁。

**为什么是这个优先级**：自动委托（P2）覆盖大多数情形；显式点名是"我知道该用谁"时的兜底，也是用户对子代理选择唯一的确定控制手段。低于自动委托，是因为它要求用户先知道有哪些子代理、且在一个已经很长的 `@` 下拉里再分出一类条目。

**独立测试**：在输入框输入 `@`、选中子代理 `Explore`、发送，验证主代理这一轮以 `subagent_type=Explore` 调用了 `Agent` 工具（对话里出现该子代理的工具块）。

**归属与形态**：

* **入口是现有 `@` 下拉，不新增交互概念**：子代理条目与文件、IDE 上下文条目同列，排在文件搜索结果之前、IDE 固定条目之后。选中后插入原子标签（不可在标签内编辑，退格整体删除），标签可见文本是 `@<名称>`。
* **两条正文拼写，一套解析**：webview 侧落地为 `[@agent:<名称>]`，与 `[@file:…]`、`[@ide:…]` 同族——草稿还原与消息气泡复用同一套标记解析；CLI 没有标签机制，插入 `@agent:<名称>`。SDK 用一条正则同时认这两种拼写。
* **判定在 SDK，不在 UI**：宿主只负责下发候选、把用户选中的标记原样放进消息；"这条消息点名了哪个子代理"由 SDK 在每轮首回合判定并注入提醒。因此 CLI、桌面端、插件端、远控页四端语义一致，手打标记与从下拉里选中等价。
* **软指定，不是硬绑定**：注入的是 system-reminder（点名该子代理、要求把这条任务委派给它），最终怎么切分上下文、是否并行多次调用仍由主代理决定。与 Claude Code 的 `agent_mention` 同一语义（`@agent-<name>` 文本 + "用户希望调用该代理"提醒）。

**验收场景**：

1. **假设** 我已配置了多个子代理，**当** 我在输入框输入 `@`，**则** 下拉里除文件外还列出各子代理（名称为主文案、`description` 为说明），且排在文件搜索结果之前
2. **假设** `@` 下拉里已有子代理条目，**当** 我继续输入过滤文本，**则** 子代理条目按名称与描述过滤，文件搜索照常进行，两者互不影响
3. **假设** 我选中某个子代理，**当** 标签插入输入框，**则** 它以原子标签形式呈现（可见 `@<名称>`），发送后正文里含 `[@agent:<名称>]`（CLI 为 `@agent:<名称>`）
4. **假设** 消息里点名了已注册的子代理，**当** 主代理开始这一轮，**则** 上下文中出现一条提醒，点名该子代理并要求以 `subagent_type=<名称>` 委派，主代理据此调用 `Agent` 工具
5. **假设** 消息里点名的名字不是任何已注册子代理（手打错名或子代理已被删除），**当** 主代理开始这一轮，**则** 提醒改为列出可用子代理名，不报错、不中断这一轮
6. **假设** 同一条消息里点名了两个不同子代理，**当** 主代理开始这一轮，**则** 两个都被点名到同一条提醒里
7. **假设** 带子代理标签的消息已发送，**当** 它在消息历史里重放，**则** 标签仍然可读（内容来自消息正文本身，不重新查询子代理列表）
8. **假设** 带子代理标签的内容留在输入框里未发送，**当** 切走会话再切回（草稿恢复），**则** 标签仍在，发送时照常按点名处理
9. **假设** 子代理在会话进行中被删除或被插件卸载，**当** 我再次输入 `@`，**则** 该子代理不再出现在候选里

***

### 用户故事：子代理上下文隔离（优先级：P2）

作为用户，我希望每个子代理维护与主对话分离的独立上下文窗口，以便专业代理可以专注于其特定任务而不受不相关对话历史的影响。

**为什么是这个优先级**：上下文隔离对子代理的有效性至关重要 - 它防止子代理被不相关的对话历史混淆，并允许它们保持对专业领域的专注。

**独立测试**：可以通过进行长对话，然后委托给子代理并验证它不引用不相关的先前对话元素来测试。

**验收场景**：

1. **假设** 我与主代理有长对话，**当** 我将任务委托给子代理，**则** 子代理仅使用其任务的相关上下文运行
2. **假设** 我按顺序使用多个子代理，**当** 每个子代理运行，**则** 它们不互相干扰上下文
3. **假设** 子代理完成任务，**当** 控制权返回主代理，**则** 主代理可以访问子代理的结果但维护自己的上下文

***

### 用户故事：子代理活动显示（优先级：P2）

作为用户，我希望在主对话流中看到子代理活动的反映，以便我无需单独的消息历史视图就能追踪它们的进度。

**为什么是这个优先级**：清晰的子代理活动反馈对用户理解和调试至关重要。用户需要实时了解子代理正在运行什么工具。

**独立测试**：可以通过触发子代理并验证 `Agent` 工具块的 short result 动态更新工具名称和 token 使用情况来测试。

**验收场景**：

1. **假设** 子代理被触发，**当** 它处理任务，**则** `Agent` 工具块在其 `shortResult` 中显示实时更新
2. **假设** 子代理正在运行工具，**当** 我查看消息列表，**则** 我在 `Agent` 工具块的 `shortResult` 中看到最近执行的子代理工具
3. **假设** 子代理任务完成，**当** 任务结束，**则** 最终文本结果作为工具输出返回并显示在主对话中
4. **假设** 子代理任务完成，**当** 我查看工具块，**则** 我看到任务完成摘要，包括使用的总 token 和执行的工具

***

### 用户故事：子代理 worktree 隔离（优先级：P1）

作为开发者，我希望让子代理在它自己的 git worktree 中运行，以便多个子代理（或子代理与主会话）并行修改同一个仓库时不会互相踩文件、也不会污染主会话的工作区。

**为什么是这个优先级**：并行委派是子代理最主要的用法，但当前所有子代理共享父会话的工作目录——两个子代理并行改同一批文件会互相覆盖，一个子代理跑构建或改配置也会直接影响主会话。对齐 Claude Code 的 `isolation: worktree`。

**独立测试**：在 `.wave/agents/` 创建带 `isolation: worktree` 的子代理并派它改文件，验证写入落在 `.wave/worktrees/<name>` 且父工作目录零改动；完成后有改动时 worktree 被保留、结果里给出路径/分支/变更统计；再派一个只读任务，验证无改动时 worktree 与临时分支被自动删除。

**验收场景**：

1. **假设** 子代理定义文件的 frontmatter 含 `isolation: worktree`，**当** 主代理通过 `Agent` 工具委派它，**则** 该子代理的文件与命令工具（Read/Write/Edit/Bash/Grep/Glob、后台任务）都以新 worktree 目录为工作目录。
2. **假设** 隔离子代理启动，**当** worktree 被创建，**则** 目录为 `<主仓库>/.wave/worktrees/<name>`、分支为 `worktree-<name>`、基线按 `worktree.baseRef`（`fresh` 默认基于 `origin/<默认分支>`，`head` 基于父会话当前 HEAD），与 CLI `-w` / `EnterWorktree` 同一套规则。
3. **假设** 配置了 `WorktreeCreate` hook，**当** 隔离子代理启动，**则** 由 hook 创建（路径取第一个成功 hook 的 stdout）；未配置则走 git；新建 worktree 时复制 `.wave/settings.local.json` 与 `.worktreeinclude` 列出的文件。
4. **假设** 隔离子代理在 worktree 内运行，**当** 它用 Write/Edit 修改父仓库（worktree 之外）的文件，**则** 被拒绝并返回 worktree 安全拦截消息（与父会话 worktree 同一条规则，`bypassPermissions` 下同样生效）。
5. **假设** 子代理结束且本次新建的 worktree 内没有任何改动（0 个变更文件、0 个新提交），**当** 收尾执行，**则** worktree 目录与 `worktree-<name>` 分支被自动删除，结果中说明已清理。
6. **假设** 子代理结束且 worktree 内有改动（未提交文件或新提交），**当** 收尾执行，**则** worktree 与分支被保留，结果中给出 worktree 路径、分支名、变更文件数与提交数，并说明这些改动**没有**应用到父工作目录。
7. **假设** 子代理失败、被停止（`TaskStop` / 后台任务停止）或被清理，**当** 收尾执行，**则** 同样按「有改动保留、无改动删除」处理，且收尾失败不改变原有的成功/失败结果（只记日志）。
8. **假设** 变更无法判定（`git status` / `rev-list` 执行失败），**当** 收尾执行，**则** 保留 worktree（fail-safe，不删），结果提示路径与无法判定。
9. **假设** 隔离子代理运行中，**当** 它调用 `EnterWorktree` 或 `ExitWorktree`，**则** 工具被拒绝——worktree 生命周期由父级管理，子代理不得自行进出或删除。
10. **假设** 父会话本身已在一个 worktree 中，**当** 派发隔离子代理，**则** 子代理 worktree 仍创建在主仓库的 `.wave/worktrees/` 下，默认基线为 `origin/<默认分支>`（与父会话 worktree 无嵌套关系）。
11. **假设** 工作目录不是 git 仓库且未配置 `WorktreeCreate` hook，**当** 派发隔离子代理，**则** 委派失败并返回明确错误，**不得**静默退回父工作目录执行。

***

### 用户故事：按次指定 worktree 隔离（优先级：P2）

作为用户，我希望在不改子代理定义文件的前提下，把某一次委派放进独立 worktree，以便临时并行跑几单改代码的任务。

**为什么是这个优先级**：定义文件只能给「某个子代理」永久开启隔离；临时并行（例如一次派三个 general-purpose 改不同模块）如果每次都要先改定义文件就不实用。对齐 Claude Code「让 Claude 用 worktree 跑子代理」的动态路径。

**独立测试**：对不带 `isolation` 的 general-purpose 子代理，主代理调用 `Agent` 时传 `isolation: "worktree"`，验证该次运行在独立 worktree 中、收尾规则与上一条故事一致；再不带该参数调用一次，验证仍跑在父工作目录。

**验收场景**：

1. **假设** 子代理定义没有 `isolation`，**当** `Agent` 调用带 `isolation: "worktree"`，**则** 该次运行使用独立 worktree，其余行为不变。
2. **假设** 子代理定义含 `isolation: worktree`，**当** `Agent` 调用不带该参数，**则** 仍然隔离（定义是下限，调用参数不能关闭）。
3. **假设** `Agent` 调用传入除 `"worktree"` 以外的 `isolation` 值，**则** 入参校验失败并返回错误，不启动子代理。
4. **假设** 子代理以后台方式运行（`run_in_background`），**当** 它完成或被停止，**则** 隔离与收尾规则与前台一致，完成通知里包含 worktree 摘要。

***

### 用户故事：定义文件的上下文开关与工具黑名单（优先级：P2）

作为子代理作者，我希望像 Claude Code 那样在 frontmatter 里声明「这个子代理不加载项目记忆」和「排除掉某几个工具」，这样一次性搜索类子代理不必携带主会话的 `AGENTS.md`，工具面也不必逐个列白名单。

**为什么是这个优先级**：白名单只能加、不能减——将来新增任何一个工具，白名单式子代理都看不到它（内置 Explore 因此至今抓不了网页）；而项目记忆对一次性的搜索子代理是纯粹的成本。字段名取 Wave 语义（`omitAgentsMd`：Wave 有 `AGENTS.md`，「`CLAUDE.md`」只是回退），同时把 Claude Code 的 `omitClaudeMd` 认作别名——用户从 Claude Code 抄过来的子代理文件不会静默变成「照样加载项目记忆」；`disallowedTools` 沿用 Claude Code 原名。

**独立测试**：写一个带 `omitAgentsMd: true` 与 `disallowedTools: [Edit, Write]` 的子代理，派发后验证首条消息与 system prompt 里没有 `AGENTS.md` 内容、`Edit`/`Write` 不在工具列表且调用返回 not found，其余工具（含 `Bash`、`Read`）正常。

**验收场景**：

1. **假设** 子代理 frontmatter 含 `omitAgentsMd: true`（或 CC 别名 `omitClaudeMd: true`），**当** 子代理启动并发出第一次请求，**则** 不注入项目 `AGENTS.md`（含 `CLAUDE.md` 回退）、用户记忆以及 `.wave/rules` 的无条件规则；不带该字段（或值不是 `true`）时行为不变，照常注入。
2. **假设** 子代理 frontmatter 含 `disallowedTools`（`[Edit, Write]`、`Edit, Write`、块列表三种写法均可），**当** 子代理初始化自己的 `ToolManager`，**则** 这些工具不注册、调用返回 `Tool '<name>' not found`，未列出的工具照常可用。
3. **假设** 同一个子代理既声明 `tools` 又声明 `disallowedTools`，**当** 它初始化，**则** 先按白名单取交集再减黑名单；两边都出现的工具一律不可用。
4. **假设** 子代理只声明 `disallowedTools`、没有 `tools`，**当** 主模型读 `Agent` 工具描述，**则** 该子代理的工具摘要写成 `All except <名单>`，不得写成误导性的 `*`。
5. **假设** `disallowedTools` 里写了平台不存在的工具名，**当** 子代理初始化，**则** 该项被忽略，解析与启动都不失败。

***

### 边界情况

* 子代理配置文件的 YAML frontmatter 无效时会怎样？
* 系统如何处理循环委托（子代理尝试委托回主代理或另一个子代理）？
* 子代理配置了不存在的工具时会怎样？
* 当项目和用户目录都包含同名代理时系统如何表现？
* 子代理指定的模型不可用或无效时会怎样？
* 系统如何处理存在但没有内容或缺少必需字段的子代理文件？
* 用户请求的子代理存在但缺少访问所需工具的权限时会怎样？
* 子代理尝试访问 Task 管理工具（TaskCreate/TaskGet/TaskUpdate/TaskList）时会怎样？→ 系统必须拒绝：这些工具不注册到子代理的 ToolManager，子代理无法调用，防止其修改主代理的共享任务列表。
* 隔离子代理撞上已存在的同名 worktree 时会怎样？→ 复用该目录（`isNew: false`）、不执行 post-creation 文件复制，避免覆盖既有本地状态；复用的 worktree 不是本次创建的，收尾一律保留并在结果里报告变更统计。
* 隔离子代理保留的 worktree 由谁清理？→ 本期不做定时清扫（Claude Code 的 `cleanupPeriodDays` 清扫不在范围内）。结果里给出路径与分支，由用户或后续会话用 `git worktree remove`（必要时加 `--force`）与 `git branch -D worktree-<name>` 手工清理。
* hook-based worktree 收尾时没有配 `WorktreeRemove` hook 会怎样？→ 保留 worktree 并记 warning（沿用现有语义），结果里给出路径。
* 子代理在 worktree 内 checkout 了别的分支会怎样？→ 收尾只删 `worktree-<name>`，其它分支与其提交保留（沿用现有语义）。
* 子代理用 Bash `cd` 到主仓库改文件会怎样？→ 不在拦截范围（现有 worktree 安全拦截只覆盖 Write/Edit），与父会话 worktree 会话一致，属已知边界而非本次新增缺口。
* 隔离子代理的指令文件（`AGENTS.md`、记忆规则）从哪来？→ 来自父会话（子容器解析到父级 `MemoryRuleManager`），不读取 worktree 内的版本，与 Claude Code「指令文件取自主对话」一致。
* 点名提醒会不会进子代理的上下文？→ 不会。子代理不能再派子代理（层级限制 1 层），点名只对主代理这一轮生效。
* 子代理名字里出现冒号（插件子代理注册为 `<插件名>:<名称>`）会破坏解析吗？→ 不会：标记解析按「首字符是字母、其后是字母/数字/下划线/连字符/冒号」取名字，冒号本就在字符集内。
* `@agent:<名称>` 会被当成文件提及去触发条件规则吗？→ 不会：`@` 文件提及的扫描必须排除 `agent:` 前缀，否则会拿 `agent` 这个词去查文件。
* 用户手打或从别处粘贴 `@agent:<名称>`、不经 `@` 下拉，还生效吗？→ 生效，与从下拉里选中完全等价（判定在 SDK 侧，只看正文标记）。

## 子代理 Worktree 隔离（2026-02-27 添加）

* **配置面**：定义文件 frontmatter 新增 `isolation`，唯一有效值 `"worktree"`（其它值忽略、不报错，与 Claude Code 一致）；`Agent` 工具新增可选 `isolation` 参数，仅能在定义未开启时「加开」，不能关闭定义里已开启的隔离。
* **生效范围**：本轮只覆盖 `Agent` 工具派发的子代理（前台与后台）。Skill 的 `context: fork` 与 Workflow 的 `agent()` 暂不启用（Workflow 的结构化结果契约需要单独设计 worktree 摘要的承载位）。
* **工作目录**：隔离子代理的 `MessageManager` / `AIManager` / `BackgroundTaskManager` 以及子容器的 `Workdir` 槽都指向 worktree 路径；子容器注册自己的 `WorktreeSession`（含 `originalCwd` = 父工作目录），使 worktree 安全拦截与 `EnterWorktree` 的「已在 worktree 中」判断都基于子代理自己的状态。
* **变更判定基线**：以 worktree 创建后自身的 HEAD 为基线统计新提交，而不是父会话 HEAD——`fresh` 基线从 `origin/<默认分支>` 起，可能与父 HEAD 有前后差异，用父 HEAD 会把基线自带的历史误算成子代理的提交。
* **收尾时机**：正常完成、失败、被 `TaskStop` / 后台任务停止、实例清理四条路径统一走同一个收尾函数；收尾是 best-effort，失败只记日志，不覆盖原有结果。
* **不做的事**：不自动把 worktree 改动合并/应用到父工作目录（与 Claude Code 一致，只报告路径与统计）；不做定时清扫。

## 子代理失败如何返回主代理（2026-11-02 添加）

主代理从子代理那里只拿到两样东西：子代理最后一条助手消息的**正文**，以及「连一条助手消息都没有」时的报错。

* **已产出内容后失败**（模型报错、hook 拦截、中途异常，但子代理已经说过话）：按**正常完成**返回，内容只含 `text` 块。失败本身不进主代理的上下文——它记在子代理日志与子代理面板里，由人看。
* **一条助手消息都没有**：工具调用失败，主代理拿到 `Agent delegation failed: <原因>`；隔离子代理还会附上 worktree 收尾摘要，说明有没有留下文件。
* **提示（notice）永不跨边界**：轮次计数、错误提醒这类旁注是给人看的，既不当作子代理的产出，也不当作失败原因。
* **后台子代理不同**：结束时一条 `<task-notification>` 明确带 `status` 为 `failed` 和原因原文，主代理下一轮就会读到。
* 对齐 Claude Code 的同步分支（`AgentTool.tsx`）：有助手消息就按完成返回，没有才把错误抛出去。

## 子代理生命周期（2025-01-10 添加）

此功能为子代理提供细粒度消息回调并确保严格的生命周期管理。

### 关键变更

1. **SubagentManagerCallbacks 接口**：子代理事件的新接口：
   * `onSubagentUserMessageAdded`
   * `onSubagentAssistantMessageAdded`
   * `onSubagentAssistantContentUpdated`
   * `onSubagentToolBlockUpdated`
   * `onSubagentMessagesChange`

2. **重构的 SubagentManager**：
   * 使用 `callbacks: SubagentManagerCallbacks` 转发事件
   * 通过 `cleanupInstance(subagentId)` 确保子代理实例清理

3. **移除持久子代理 UI 状态**：
   * `SubagentBlock` 组件及其关联的 `subagentMessages` 上下文状态从 CLI 中移除
   * 活动现在通过 `Agent` 工具块的 `shortResult` 报告

## 澄清

### 2024-12-19 会议

* 问：子代理触发时，子代理消息块是否应在主对话流中内联显示？ → 答：否，它在主对话流中表示为 `Agent` 工具块
* 问：当子代理产生错误或未能完成任务时系统应如何处理任务委托？ → 答：返回错误消息给主代理。子代理是工具调用，因此可以返回成功内容或错误消息（2026-11-02 修订：只有「一条助手消息都没有」时才返回错误；已产出内容的按完成返回，失败只记日志与面板，见上一节）
* 问：区分子代理活动的视觉指示器应该是什么？ → 答：`Agent` 工具块的 `shortResult` 动态更新工具执行和 token 使用信息
* 问：Wave Agent SDK 是否应提供创建子代理配置文件的功能？ → 答：否，SDK 仅读取/加载/解析用户创建的子代理文件
