Claude Code 深度拆解:多 Agent 协作 3 — Task 状态机、SendMessage 与消息邮箱

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 异步传递:

  1. 寻址parseAddress(to) 解析目标------支持 agentId(如 "a-abc123")或 agentName(如 "explore-agent")
  2. 查找:在 AppState.tasks 中查找目标 Task,确认其存在且非终态
  3. 写入writeToMailbox() 写入 mailbox JSON 文件
  4. 检测:目标 Agent 的 inbox poller 在下一次循环中检测到新消息
  5. 注入:将消息作为 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 三件套构成了一个完整的优雅关闭协议:

  1. Coordinator 发 shutdown_request 给 Worker
  2. Worker 收到后触发 Stop Hook → 决定是 shutdown_approved 还是 shutdown_rejected
  3. 如果 approved,Worker 清理资源后停止
  4. 如果 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[] 中。

这个设计有两个意图:

  1. 格式区分:父 Agent 明确知道"这是子 Agent 的完成通知",不是用户的输入
  2. 信息裁剪:通知只包含关键摘要(做了什么、结果是什么),不包含子 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 源码感兴趣,欢迎关注本系列 后续更新。有任何想法或疑问,欢迎评论区留言讨论 👋

相关推荐
Slow菜鸟1 小时前
Claude Code教程(九)| MCP 之 Playwright
claude code
二哈赛车手1 小时前
新人笔记---实现简易版的rag的bm25检索(利用ES),以及RAG上传时的ES与向量数据库双写
java·数据库·笔记·spring·elasticsearch·ai
AI进化营-智能译站2 小时前
ROS2 C++开发系列07-高效构建机器人决策逻辑,运算符与控制流实战
开发语言·c++·ai·机器人
Robot_Nav2 小时前
AI 编程助手 Skill 完全指南:VS Code · Trae CN · Claude Code
人工智能·vscode·skill·trae·claude code
运维小子2 小时前
Claude Code 权限配置完全指南
ai·claude
AI进化营-智能译站3 小时前
ROS2 C++开发系列13-运算符重载让ROS2消息处理更自然
java·开发语言·c++·ai
翔云1234563 小时前
大模型训练框架全景解析(2026最新)
ai·大模型
Slow菜鸟3 小时前
Claude Code教程(八)| MCP 之 Context7
claude code
AI趣实验3 小时前
Hermes Agent LLM Wiki + Obsidian Git 免费替代 Obsidian Sync:保姆级配置教程
aigc·agent