Hi,大家好,欢迎来到维元码簿。
本文属于 《Claude Code 源码 Deep Dive》 系列,专注于多 Agent 协作中的 任务系统与 Agent 间通信 板块。如果你想了解整个系列,可以先看系列开篇 | Claude Code 源码架构概览:51万行代码的模块地图。
本文讲一件事:子 Agent 跑起来后,主 Agent 怎么知道它的状态、怎么跟它通信、怎么把结果收回来。
读完全文,你将能回答这几个问题:
- Claude Code 的 7 种 Task 类型分别处理什么场景? 从后台 Bash 命令到远程 Agent 到后台记忆整理------Task 是对"任何后台运行的东西"的统一抽象。
- 主 Agent 怎么给正在运行的子 Agent 发消息? SendMessage------通过 agent ID 或 name 寻址,写入 mailbox 文件,子 Agent 在下一轮处理。
- Agent 之间的消息是"聊天"吗? 不是。是结构化的 task-notification------不是自由对话,是任务级信号传递。
本篇覆盖的源码范围
| 模块 | 核心文件 | 核心代码行 | 文件总行 | 职责 |
|---|---|---|---|---|
| Task 类型定义 | src/Task.ts |
L6-13(7 种 TaskType)+ L22-29(状态终端判断)+ L44-57(TaskStateBase) | 126 行 | 7 种 TaskType、TaskStatus 状态机、Task ID 安全生成 |
| Task 管理框架 | src/utils/task/framework.ts |
散布 | --- | registerTask()、updateTaskState()、evictTerminalTask() |
| Task 工具集 | src/tools/TaskCreateTool/ 等 6 个目录 |
各自 ~100 行 | ~600 行 | 创建/查询/更新/列表/停止/输出 6 个工具 |
| SendMessage 工具 | src/tools/SendMessageTool/SendMessageTool.ts |
L46-180(消息类型定义)+ L300-500(路由与写入) | 918 行 | Agent 间消息、mailbox 写入、shutdown 协议 |
| TodoWrite 工具 | src/tools/TodoWriteTool/ |
--- | ~200 行 | 共享 todo 板、agentId 作用域 |
| 消息邮箱 | src/utils/teammateMailbox.ts |
--- | 散布 | mailbox JSON 文件读写、空闲通知、DM 摘要 |
前情提要:从隔离到通信
在姊妹篇[上下文隔离与权限边界](./05-Claude Code深度拆解-多Agent协作 2-上下文隔离与权限边界.md)中,我们看到了每个子 Agent 在独立边界内运行------自己的 AsyncLocalStorage、自己的权限视图、自己的 AbortController。但边界之外,Agent 之间还需要通信。
主 Agent 需要知道三件事:子 Agent 现在在干什么(状态)、我能给它追加指令吗(通信)、它做完了结果在哪(回流)。Task 系统、SendMessage 和通知机制分别是这三件事的答案。

Task 系统:后台一切的统一抽象
Task 不是 Agent。Task 是"对任何后台运行的东西的统一抽象"。 一个后台 Bash 命令是 Task,一个异步子 Agent 是 Task,一个远程 Agent 是 Task,甚至一个 Swarm Teammate 也是 Task。
7 种 Task 类型
typescript
// src/Task.ts L6-13
export type TaskType =
| 'local_bash' // 后台 Bash 命令
| 'local_agent' // 后台子 Agent
| 'remote_agent' // 远程 Agent(Bridge 协议)
| 'in_process_teammate'// 同进程 Swarm 队友
| 'local_workflow' // 工作流编排
| 'monitor_mcp' // MCP Server 健康监控
| 'dream' // 后台记忆整理(实验性)
按运行位置分类:
| 分类 | Task 类型 | 运行位置 | 典型场景 |
|---|---|---|---|
| 本机进程内 | local_bash | 同进程 | npm run build 后台执行 |
| 本机进程内 | local_agent | 同进程 | 后台子 Agent 做代码搜索 |
| 本机进程内 | in_process_teammate | 同进程(ALS 隔离) | Swarm 队友并行执行 |
| 本机进程内 | local_workflow | 同进程 | 多步骤工作流 |
| 本机进程内 | dream | 同进程 | 后台记忆巩固 |
| 本机进程间 | monitor_mcp | 独立子进程 | 监控 MCP Server 健康 |
| 远程 | remote_agent | 远程容器/机器 | Bridge 远程执行 |
Task ID 的安全设计
Task ID 不是随意的 UUID------它有精心设计的安全考虑:
typescript
// src/Task.ts L79-106
const TASK_ID_PREFIXES = {
local_bash: 'b', // 向后兼容,保持 'b' 前缀
local_agent: 'a',
remote_agent: 'r',
in_process_teammate: 't',
local_workflow: 'w',
monitor_mcp: 'm',
dream: 'd',
}
// 36^8 ≈ 2.8 万亿组合,足够抵御暴力 symlink 攻击
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix
for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
}
return id
}
为什么需要 randomBytes(8) 而不是自增 ID?Task ID 被用作文件路径的一部分(output file 存储在 task-results/<taskId>.json)。如果 ID 可预测,恶意进程可能构造 symlink 攻击。36 个字符的字母表 + 8 位随机 → 2.8 万亿组合------暴力碰撞在物理上不可行。
Task 状态机
Task 的生命周期是一个简单的五状态机:
pending → running → completed
→ failed
→ killed
isTerminalTaskStatus() 判断三个终态(completed/failed/killed),用于:
- 防止向已死 Agent 注入消息
- 从 AppState 中驱逐已完成任务
- 孤儿清理路径
每个 Task 有一个 notified 标志------用于控制 task-notification XML 消息的发送。只通知一次,避免重复通知。
SendMessage:Agent 之间的"电话"
SendMessage 不是聊天工具------它是任务级通知系统。 Agent 之间不"对话",只传递结构化信号。
消息类型
typescript
// src/tools/SendMessageTool/SendMessageTool.ts(简化)
const StructuredMessage = z.discriminatedUnion('type', [
z.object({ type: z.literal('shutdown_request'), reason: z.string().optional() }),
z.object({ type: z.literal('shutdown_approved') }),
z.object({ type: z.literal('shutdown_rejected'), reason: z.string().optional() }),
// ... 通用消息
])
四种核心消息类型:
- shutdown_request:请求目标 Agent 优雅关闭
- shutdown_approved:同意关闭请求
- shutdown_rejected:拒绝关闭请求(附原因)
- 通用消息:文本消息,作为 user message 注入到目标 Agent
消息路由流程
SendMessage 的路由不是"Agent 直接互发消息"------而是通过文件系统 mailbox 异步传递:
- 寻址 :
parseAddress(to)解析目标------支持 agentId(如 "a-abc123")或 agentName(如 "explore-agent") - 查找:在 AppState.tasks 中查找目标 Task,确认其存在且非终态
- 写入 :
writeToMailbox()写入 mailbox JSON 文件 - 检测:目标 Agent 的 inbox poller 在下一次循环中检测到新消息
- 注入:将消息作为 user message 注入到目标 Agent 的 messages[]

Resume 模式:SendMessage 可以"唤醒" Agent
SendMessage 的一个特别设计是可以"唤醒"一个已完成或暂停的子 Agent。当 Coordinator 判断"继续这个 Agent 比新建更好"时,它发一条 SendMessage 给已暂停的 Agent------Agent resume 后,复用已有上下文,继续执行。
Coordinator 的决策矩阵(来自 System Prompt):
| 情境 | 机制 | 原因 |
| 研究恰好探索了需要编辑的文件 | Continue | Worker 已有文件上下文 + 明确计划 |
| 研究很广泛但实现范围很窄 | Spawn | 避免带入探索噪声;上下文更干净 |
| 纠错或延续近期工作 | Continue | Worker 有错误上下文和尝试记录 |
| 验证另一个 Worker 的代码 | Spawn | 验证者应用新眼光看代码 |
Shutdown 协议:优雅关闭
SendMessage 的 shutdown_request/approved/rejected 三件套构成了一个完整的优雅关闭协议:
- Coordinator 发
shutdown_request给 Worker - Worker 收到后触发 Stop Hook → 决定是
shutdown_approved还是shutdown_rejected - 如果 approved,Worker 清理资源后停止
- 如果 rejected,Worker 告知原因,Coordinator 可以重新决策
TodoWrite:共享任务板
TodoWrite 是所有 Agent 共享的状态------但每个 Agent 的 todo 独立 scope。
typescript
// src/tools/AgentTool/runAgent.ts L839-843
// 子 Agent 结束时清理其 todo 项
rootSetAppState(prev => {
if (!(agentId in prev.todos)) return prev
const { [agentId]: _removed, ...todos } = prev.todos
return { ...prev, todos }
})
这段 finally 块中的清理代码说明了一个重要设计:todo 项按 agentId 作用域存储。 子 Agent 可以有自己的 todo 列表,完成后自动清理。如果不清理,每次子 Agent 调用都在 AppState.todos 中留下一个 key------即使值已经是空数组 [],key 本身也是内存泄漏。
结果回流:task-notification 的消息格式
子 Agent 完成后,它的最后一条消息不会直接"变"成父 Agent 的消息。而是被包装成一条 <task-notification> 格式的 XML 用户消息,注入到父 Agent 的 messages[] 中。
这个设计有两个意图:
- 格式区分:父 Agent 明确知道"这是子 Agent 的完成通知",不是用户的输入
- 信息裁剪:通知只包含关键摘要(做了什么、结果是什么),不包含子 Agent 的完整对话------保持父上下文干净
Fork Agent 的铁律三是"不要偷看"------主 Agent 不应该去读子 Agent 的 output_file。这个设计确保了即使主 Agent 好奇,子 Agent 的执行细节也不会污染主上下文。
本章小结
- Task 系统是统一的后台抽象------7 种类型覆盖从 Bash 到远程 Agent 到记忆整理,Task ID 的随机化设计抵御 symlink 攻击
- SendMessage 是结构化的 Agent 间通信------不是聊天,是任务级通知。shutdown 协议提供优雅关闭能力
- TodoWrite 按 agentId 作用域隔离------子 Agent 的 todo 不会泄露到其他 Agent
- 结果回流通过 task-notification XML 消息------保持主上下文干净,子 Agent 的执行细节不污染主 Agent
如果这篇文章对你有帮助,欢迎点赞收藏 支持一下。如果你对 Claude Code 源码感兴趣,欢迎关注本系列 后续更新。有任何想法或疑问,欢迎评论区留言讨论 👋