2.5 Sub-agent¶
本节摘要
从State Management角度看,Sub-agent 机制回答的核心问题是:子 Agent 继承父 Agent 的哪些状态、隔离哪些、怎么把结果交还给父 Agent? 本节按「继承/隔离什么 → 怎么交换状态 → Coordinator 的状态分工」的主线展开。
一、Sub-agent 继承/隔离了哪些状态¶
Sub-agent 创建时,最关键的决策是:父 Agent 的哪些状态传给子 Agent、哪些隔离开。Claude Code 通过 Fork/Normal 两种模式提供不同的继承策略。
Fork vs Normal:两种状态继承模式¶
| 状态 | Fork 模式 | Normal 模式 |
|---|---|---|
| System Prompt(静态区域) | ✅ 共享(前缀对齐,命中 Prompt Cache) | ❌ 独立构建 |
| System Prompt(动态区域) | ✅ 共享 | ❌ 独立构建 |
| userContext / systemContext | ✅ 共享 | ❌ 独立 |
| 对话历史 | ✅ 继承父级消息 + 占位 tool_result | ❌ 空白开始 |
| 工具定义 | ✅ 继承(经过滤) | ✅ 继承(经过滤) |
| readFileState | ❌ 隔离(独立追踪读取的文件) | ❌ 隔离 |
| contentReplacementState | ❌ 隔离(工具结果替换状态独立) | ❌ 隔离 |
| agentId | ❌ 独立分配 | ❌ 独立分配 |
| abort 控制 | 同步共享 / 异步独立 | 独立 |
| setAppState | 条件共享(部分状态可回写父级) | 独立 |
Fork 的核心价值:通过 CacheSafeParams(forkedAgent.ts)确保子 Agent 与父 Agent 的 API 请求在 system prompt + context + 工具定义 + 消息前缀上完全对齐,从而命中 Prompt Cache。buildForkedMessages 插入占位 tool_result 来维持消息序列的前缀一致性。
隔离的边界:即使在 Fork 模式下,readFileState 和 contentReplacementState 也是隔离的——子 Agent 独立追踪自己读取了哪些文件、工具结果的替换状态,不会污染父 Agent 的状态。
工具集:三层过滤收紧¶
Sub-agent 看到的工具集是父 Agent 的子集,通过三层过滤逐步收紧:
| 层级 | 过滤规则 | 示例 |
|---|---|---|
| 1 | ALL_AGENT_DISALLOWED_TOOLS — 全局黑名单 |
AgentTool 自身(防递归) |
| 2 | CUSTOM_AGENT_DISALLOWED_TOOLS — 按类型黑名单 |
特定 Agent 不允许的工具 |
| 3 | ASYNC_AGENT_ALLOWED_TOOLS — 异步白名单 |
后台 Agent 仅允许特定工具 |
MCP 工具始终透传——用户主动配置的外部能力不被框架层过滤。
Memory:独立目录¶
每种 Agent 类型维护独立的Memory 目录(agentMemory.ts),按 user / project / local 三种 scope 映射到不同路径。不同类型的 Sub-agent Memory 互不干扰。
Attachment:部分共享¶
Sub-agent 共享全线程通用 attachment(Skill 列表、文件变更、工具 Delta 等),但不获取主线程独占 attachment(IDE 选中、诊断信息、token 用量等)。详见 2.6 Attachment 系统。
二、Sub-agent 怎么与父 Agent 交换状态¶
Sub-agent 执行完毕后,需要把结果交还给父 Agent。这涉及两个问题:什么时候返回 和 以什么形式返回。
同步返回(默认)¶
Sub-agent 在前台执行,父 Agent 阻塞等待。结果直接作为 tool_result 返回到父 Agent 的对话中。
120 秒自动后台化¶
如果同步执行超过 120 秒,Sub-agent 自动转为后台任务——这是一个状态机转换:
graph LR
Sync["前台同步\n父 Agent 阻塞"] -->|"< 120s"| Return["tool_result\n直接返回"]
Sync -->|"> 120s"| BG["自动后台化\nbackgroundSignal"]
BG --> Notify["<task-notification>\n异步注入父会话"]
Notify --> Done["最终结果回传"]
后台化后,Sub-agent 通过 <task-notification> XML 标签注入父 Agent 的对话——这实际上是在父 Agent 的消息历史中追加新的状态信息(任务进度、最终结果)。
异步 Sub-agent¶
从一开始就在后台执行,始终通过 <task-notification> 通信。与 120 秒后台化的区别是:异步 Sub-agent 有独立的 AbortController(父 Agent 中止不影响它)。
三、Coordinator 模式:状态的重新分工¶
Coordinator 模式从根本上改变了状态的分布——父 Agent(Coordinator)主动裁剪自己的状态空间,只保留编排能力:
| 状态维度 | Coordinator | Worker |
|---|---|---|
| 可用工具 | 仅 4 个:AgentTool、SendMessage、TaskStop、SyntheticOutput | 全量工具(30+) |
| 职责 | 任务分解、分配、汇总 | 实际执行(文件读写、Shell 等) |
| Context 内容 | 任务描述 + Worker 返回的结果摘要 | 具体子任务的完整 Context |
这是一种「状态分治」:Coordinator 的Context Window不被具体执行细节填满,可以专注于全局编排;Worker 的 Context只包含分配到的子任务。
Coordinator 与 Fork 互斥
Coordinator 模式下 Sub-agent 使用 Normal 模式(独立 Context),不使用 Fork——因为 Worker 的任务Context 与 Coordinator 完全不同,共享缓存没有意义。
四、补充机制¶
三条执行路径¶
| 路径 | 环境 | 适用场景 |
|---|---|---|
| Local(默认) | 当前进程内 | 大多数子任务 |
| Remote (CCR) | Claude Code Remote 远程环境 | 需要访问远程环境 |
| Teammate | tmux pane / 进程内后端 | 可见的并行协作 |
7 种任务类型¶
TaskRegistry(进程内 Map)管理的后台任务涵盖:LocalShellTask、LocalAgentTask、RemoteAgentTask、InProcessTeammate、LocalWorkflow、MonitorMcp、DreamTask。每种类型有独立的生命周期管理。
状态持久化¶
Sub-agent 状态是进程内的内存结构,不持久化到磁盘。CLI 场景下,进程就是会话——进程结束,Sub-agent 状态自然消失。这与 OpenClaw 的磁盘持久化注册表形成对比。
关键文件索引¶
| 文件 | 职责 |
|---|---|
src/utils/forkedAgent.ts |
CacheSafeParams + createSubagentContext(状态继承/隔离的核心) |
src/tools/AgentTool/forkSubagent.ts |
Fork 模式 + buildForkedMessages(消息前缀对齐) |
src/tools/AgentTool/AgentTool.tsx |
三路径入口 + Fork/Normal 选择 |
src/tools/AgentTool/runAgent.ts |
Local Agent 主循环 |
src/coordinator/coordinatorMode.ts |
Coordinator 模式(工具裁剪) |
src/constants/tools.ts |
三层工具过滤规则 |
src/tools/AgentTool/agentMemory.ts |
Sub-agent 独立Memory 目录 |
src/tasks/LocalAgentTask/LocalAgentTask.tsx |
后台化状态机(~680 行) |