前言
基于泄露的Claude Code最新版本:v2.1.88 ,使用GPT 5.4进行分析。不绕弯子,直接复制原始回答。
Claude Code CLI 多 Agent 协作机制深度研究
研究对象:
claude-code-cli仓库中与多 agent 协作直接相关的实现。研究方法:以源码静态阅读为主,重点梳理
AgentTool、runAgent、LocalAgentTask、SendMessageTool、TeamCreateTool、spawnMultiAgent、inProcessRunner、teammateMailbox、coordinatorMode等链路。说明:本文关注“多 agent 如何协作”,不再展开记忆系统、普通单 agent REPL、MCP 生态的全量分析。本文结论主要基于源码结构与控制流,不包含完整运行时压测结论。
一句话结论
这个仓库里的“多 agent”并不是“给模型加一个 spawn_worker() 工具”这么简单。它实际上做了两套彼此相关、但定位不同的协作系统:
- 后台子代理系统(background subagent)
适合“把一个具体任务交出去,让它异步做完,再把结果回传给主线程”。 - 团队队友系统(swarm teammate)
适合“让多个常驻代理长期存在、反复收消息、彼此通信、等待继续工作”。
这两套系统共享的底层思想是:agent 不是一次性的函数调用,而是一个被 runtime 管理的执行体。这个执行体有自己的身份、上下文、转录、生命周期、权限、消息通道、可恢复能力,以及回流主线程的协议。
如果用最通俗的话讲:
- 后台子代理更像“你临时外包出去的一次任务工单”。
- 团队队友更像“你手下长期在线的几个工程师”。
而这个仓库最有技术深度的地方在于:它并没有把两者混成一锅粥,而是给了它们不同的状态机、不同的通信模型、不同的恢复方式,却又让它们最终都能回到同一个主循环里,被主 agent 继续理解和调度。
1. 先建立正确的心智模型
很多人第一次看这类项目,会自然地把多 agent 理解成:
- 主 agent 调一个工具。
- 子 agent 跑一下。
- 子 agent 结束后把字符串结果返回。
这个理解对于本仓库来说太浅了。更接近真实情况的模型是下面这样:
| 角色 | 在仓库中的真实含义 |
|---|---|
| 主线程 / 主代理 | 当前用户直接交互的会话循环,负责最终对用户说话 |
| 后台子代理 | 由 AgentTool 派生出的异步任务,有 transcript、有任务状态、可恢复 |
| 队友 teammate | 团队中的长期存在执行体,可以闲置、被再次唤醒、能收发消息 |
| Task | runtime 管理 agent 生命周期的统一壳层,不只是 UI 项 |
| Mailbox | 队友之间的文件消息通道,不依赖特定进程形态 |
| Task Notification | 后台任务完成后重新注入主消息循环的 XML 消息 |
| TeamFile | 团队控制平面的持久化配置文件 |
| Backend | 队友实际运行的宿主:tmux、iTerm2、或同进程 in-process |
| Permission Broker | 谁能用什么工具、谁来批准、批准信息怎么回写 |
这套系统真正要解决的问题,不是“怎么多开几个模型”,而是:
- 怎么让它们有身份;
- 怎么让它们有生命周期;
- 怎么让它们能继续之前的工作;
- 怎么让它们能在不同运行基座上表现一致;
- 怎么让它们的结果重新成为主模型可理解的输入;
- 怎么在多人协作式 agent 场景里维持权限、安全、UI 和持久化的一致性。
这也是为什么这个仓库的多 agent 代码会横跨工具层、任务层、消息队列、磁盘存储、权限系统和系统提示词,而不是只藏在一个 agents/ 目录里。
2. 它其实不是一套多 Agent,而是两套
2.1 第一套:后台子代理
这一套以 src/tools/AgentTool/AgentTool.tsx 为入口,以 src/tools/AgentTool/runAgent.ts 为执行核心,以 src/tasks/LocalAgentTask/LocalAgentTask.tsx 为生命周期容器。
它的特点是:
- 可以同步跑,也可以转为后台异步跑;
- 每个 agent 有独立 transcript 和 metadata;
- 结果不是直接回调给父函数,而是变成
<task-notification>进入全局消息队列; - 后续可以用
SendMessage按 agent 名称或 agentId 继续对话; - 如果 agent 已停止,还可以基于 transcript 继续恢复执行。
这套更像“任务型委派系统”。
2.2 第二套:团队队友
这一套以 src/tools/TeamCreateTool/TeamCreateTool.ts 建立团队,以 src/tools/shared/spawnMultiAgent.ts 负责生成队友,以 src/utils/swarm/inProcessRunner.ts 或 pane backend 负责长期运行,以 src/utils/teammateMailbox.ts 负责通信。
它的特点是:
- 队友是长期存在的,不是一次性执行完就销毁;
- 队友可以处于
idle,等待下一条消息,而不是必须结束; - 队友之间、队友与 leader 之间用 mailbox 协议通信;
- 队友可以跑在
tmux/iTerm2的外部 pane,也可以跑在同一 Node 进程里; - 即便是同进程队友,也维持了独立身份、独立控制器、独立任务状态;
- 队友能处理结构化控制消息,如关机请求、计划审批响应等。
这套更像“多成员常驻团队系统”。
2.3 为什么要分成两套
这是理解整个设计最关键的一点。
如果只做“后台子代理”,你能委派任务,但很难自然地表达:
- 某个代理长期扮演某个专家角色;
- 我后面还要继续找同一个代理;
- 多个代理之间需要互相发消息;
- 它们在空闲时依然是“活着的成员”。
如果只做“常驻队友”,你又会让很多一次性任务变得过于重:
- 为一个很小的调查创建完整团队成员,成本太高;
- 一次性子任务也未必需要 mailbox、队伍、pane、idle 状态。
所以作者把它拆成:
- 短生命周期、结果导向:后台子代理;
- 长生命周期、协作导向:团队队友。
这不是重复造轮子,而是把两类完全不同的协作语义拆开了。
为了避免后文阅读时不断在脑内来回切换,这里先给出一张最实用的对照表:
| 维度 | 后台子代理 | 团队队友 |
|---|---|---|
| 主要入口 | AgentTool 普通调用 |
TeamCreateTool + spawnMultiAgent |
| 默认目标 | 完成一个具体任务 | 成为长期协作者 |
| 生命周期 | 任务导向,可结束后再恢复 | 常驻导向,可 idle、可继续 |
| 默认回传方式 | 自动发 <task-notification> |
不自动回传,需要显式协作 |
| 继续交互方式 | SendMessage 排队或 resume |
mailbox 持续通信 |
| 外壳状态 | LocalAgentTask |
InProcessTeammateTask 或 pane teammate task |
| 执行宿主 | 当前进程中的子运行时 | tmux/iTerm2 pane 或 in-process |
| 典型使用感受 | “帮我去做这件事” | “你以后就是这个团队的一员” |
3. 后台子代理是怎么工作的
这一部分是理解“委派任务”能力的主线。
3.1 AgentTool 不是单纯的 spawn,它先做路由判断
src/tools/AgentTool/AgentTool.tsx 里的 call() 并不是“收到 prompt 就开跑”。它首先要决定这次调用到底属于哪一种 agent 行为。
它会先处理几个关键分支:
- 是否要走 team spawn:
当存在team_name且提供了name时,这不是普通子代理,而是要生成一个 teammate。 - 是否是 队友试图再生队友:
代码里明确阻止这种情况,原因是TeamFile.members是扁平数组,嵌套队友会让团队来源关系变得混乱。 - 是否是 in-process teammate 试图开后台 agent:
这也被显式禁止。实现里写得很清楚:in-process teammate 的生命周期依附于 leader 所在进程,因此不能再随意生成自己的后台代理;而 pane/tmux 队友因为是独立进程,约束没这么强。 - 普通子代理是否要 同步 还是 异步/后台:
这由run_in_background、agent 定义里的background: true、coordinator 模式、assistant/KAIROS 模式、主动模式等因素共同决定。
换句话说,AgentTool 本质上是一个调度路由器。它先决定“这是什么类型的多 agent 行为”,再选择下面哪条执行链。
3.2 前台代理和后台代理,其实共享一套壳
很多系统会把“同步子代理”和“后台子代理”做成两套完全不同的逻辑。本仓库没有这么做。
它采用的方式是:
- 前台启动:
registerAgentForeground(...) - 后台启动:
registerAsyncAgent(...)
二者都在 src/tasks/LocalAgentTask/LocalAgentTask.tsx 中。
registerAgentForeground(...) 的设计非常值得注意。它并不是“纯前台,不留痕迹”,而是:
- 也会注册一个 task;
- 也会初始化 output symlink;
- 也会创建
AbortController; - 也会给这个 agent 安装清理逻辑;
- 额外构造一个
backgroundSignalPromise。
这个 backgroundSignal 的意义是:前台运行中的 agent 可以在运行过程中被切换成后台。也就是说,前台和后台不是两种完全不同的 agent,而更像是同一种 agent 的两种可切换显示/管理形态。
backgroundAgentTask(...) 做的事情也很关键:
- 把任务状态改成
isBackgrounded: true; - resolve 对应的
backgroundSignal; - 让调用方知道它应该从“阻塞式等待”切换为“后台继续跑”。
这说明作者把“后台化”做成了一种运行时状态,而不是一次 spawn 时不可变的硬编码选择。
3.3 LocalAgentTask:后台子代理真正的外壳
LocalAgentTask 这一层很重要,因为它决定了后台子代理不会退化成“一个 Promise”。
它的状态里不只有 status,还包括:
agentIdpromptselectedAgentagentTypeabortControllerprogressresultmessagespendingMessagesisBackgroundedretaindiskLoadednotified
这个状态结构透露出一个很强的设计意图:agent 结果不是唯一重要的东西,过程状态同样重要。
比如:
pendingMessages允许你在 agent 运行途中继续给它发消息;messages让 UI 能查看它的转录;progress支持任务进度展示;notified用来避免重复完成通知;diskLoaded和 transcript 机制一起支撑“任务状态不在内存也能恢复”。
这一层还维护了几个很关键的行为:
3.3.1 结果不是回调,而是通知
enqueueAgentNotification(...) 会构造一段 XML:
<task-notification>
<task-id>...</task-id>
<output-file>...</output-file>
<status>completed|failed|killed</status>
<summary>...</summary>
<result>...</result>
<usage>...</usage>
</task-notification>
然后通过 enqueuePendingNotification(...) 放进全局命令队列,而且默认优先级是 later,避免系统消息饿死用户输入。
这件事的意义非常大:
- 它让后台代理完成事件进入与主线程同构的消息通道;
- 它让“代理结果”成为主模型下一轮可见的上下文,而不是宿主程序偷偷消费的内部回调;
- 它也给 SDK、TUI、日志等多个上层消费方一个统一事件形态。
3.3.2 继续对话不是黑魔法,而是显式消息队列
queuePendingMessage(...) 和 drainPendingMessages(...) 让运行中的后台子代理可以在安全点收到后续消息。
这和很多人想的不一样:不是“外部强行塞一条消息给一个正在推理的模型”,而是把消息先排进 task 自己的 pendingMessages,等 agent 到达合适轮次再交给它。
所以它更像是给后台 agent 维护了一个轻量级 inbox,只是这个 inbox 不走团队 mailbox 文件,而是挂在 task 状态里。
3.4 runAgent(...):子代理不是 helper,而是完整会话运行时
理解 src/tools/AgentTool/runAgent.ts,几乎就理解了这个仓库里“agent”真正是什么。
它不是一个简单的 model.generate(prompt) 包装,而是完整地为子 agent 重建一套运行环境:
- 解析 agent 定义;
- 组装系统提示词;
- 初始化 agent 专属 MCP server;
- 合并工具池;
- 构造 agent 专属
ToolUseContext; - 记录 transcript 与 metadata;
- 跑完整的
query(...)循环; - 在 finally 中执行大规模清理。
这里最关键的几个技术点如下。
3.4.1 它真的给子代理做了独立上下文
createSubagentContext(...) 会创建新的子上下文。
从注释和参数能看出作者是有意区分两类 agent 的:
- 同步子代理:
可以共享一部分父级回调,比如setAppState; - 异步子代理:
尽量使用隔离状态,比如独立AbortController、独立读文件状态等。
这意味着“子 agent 继承父 agent 的一切”并不成立。它继承的是某些必要能力,但在文件缓存、取消控制、状态归属上有刻意隔离。
3.4.2 它会控制 thinking、工具和 prompt cache 前缀
普通 subagent 默认关闭 thinking,以控制输出 token 成本;而 fork 子代理在 useExactTools 路径下会继承父级 thinking 配置,目的是让 API 请求前缀尽量字节级一致,复用 prompt cache。
这说明这里的 agent 设计并不仅是“功能正确”,还深度考虑了缓存命中、成本控制、prefix 稳定性。
3.4.3 它会为 agent 记录 sidechain transcript 和 metadata
在 query 循环开始前,它会 fire-and-forget 地写:
recordSidechainTranscript(initialMessages, agentId)writeAgentMetadata(agentId, { agentType, worktreePath, description })
运行中每个可记录消息也会继续增量写入 transcript。
这带来的能力是:
- agent 可以被 resume;
- transcript 可以被查看;
- worktree、description、agentType 等附加信息可以跨进程、跨内存恢复。
3.4.4 清理逻辑非常“工程化”
finally 里的清理并不敷衍,包含:
- 清理 agent 自己创建的 MCP server;
- 清 session hook;
- 清 prompt cache 跟踪状态;
- 清 read file cache;
- 清 transcript 子目录映射;
- 清 todos 条目;
- 杀掉该 agent 启动的后台 shell 任务;
- 在某些特性开启时,连 monitor MCP 任务也一并杀掉。
这说明作者非常清楚一个事实:多 agent 系统最大的敌人不是“不会跑”,而是“跑完以后留下半死不活的资源”。
3.5 后台子代理的真正完成流程
可以把它概括成下面这条链:
主线程调用 AgentTool
-> 决定走普通子代理
-> registerForeground / registerAsyncAgent
-> runAgent 建立完整子运行时
-> query 循环持续执行
-> LocalAgentTask 更新进度/消息/状态
-> completeAgentTask / failAgentTask
-> enqueueAgentNotification 生成 XML 通知
-> 全局消息队列
-> 主线程下一轮把通知重新喂给模型
注意最后两步:不是“宿主收到结果然后替你决定怎么办”,而是“结果被转成一条新的、主模型能读懂的系统性消息”。这个设计非常 agent-native。
3.6 为什么 SendMessage 能继续一个已完成的后台代理
src/tools/SendMessageTool/SendMessageTool.ts 里有一段很关键的路由逻辑:
- 如果
to能在agentNameRegistry或格式化 agentId 里命中; - 且该目标是
LocalAgentTask; - 如果它还在运行,就
queuePendingMessage(...); - 如果它已经停止,就走
resumeAgentBackground(...); - 如果它已经从 AppState 里被逐出,也会尝试从磁盘 transcript 恢复。
这个行为说明了一件很重要的事:
后台子代理在语义上是“可继续的会话”,而不是“一次性函数调用”。
3.7 resumeAgentBackground(...) 的本质:重建而不是复活
src/tools/AgentTool/resumeAgent.ts 不是简单地“把旧 Promise 接回来”。它做的是一次基于 transcript 的重建。
它会:
- 读取磁盘上的 transcript 和 metadata;
- 过滤掉空白 assistant 消息、孤立 thinking-only 消息、未完成 tool_use;
- 重建 content replacement 状态;
- 恢复原 worktree(如果还存在);
- 根据 metadata 重新选择 agent 类型;
- 再次
registerAsyncAgent(...); - 用恢复后的消息历史重新调用
runAsyncAgentLifecycle(...)和runAgent(...)。
这意味着“resume”不是魔法暂停恢复,而是“拿旧状态重新构造一个新运行实例”。这是一种更稳妥、更可持久化的设计。
4. 团队队友系统是怎么工作的
如果说后台子代理解决的是“委派一次任务”,那团队队友系统解决的就是“形成一个长期协作组织”。
4.1 团队首先是一个持久化控制平面
src/tools/TeamCreateTool/TeamCreateTool.ts 不只是把 teamName 存到内存里。
它做了很多事情:
- 检查当前 leader 是否已经在带一个 team;
- 生成唯一 team 名称;
- 为 team lead 生成确定性的
leadAgentId,格式是team-lead@teamName; - 读取当前主会话模型,写入 team member 信息;
- 在磁盘上写出
TeamFile; - 重置并创建 task list 目录;
- 把 team context 注册进
AppState; - 记录 analytics 事件。
这里的 TeamFile 定义在 src/utils/swarm/teamHelpers.ts,里面的信息很多:
namedescriptioncreatedAtleadAgentIdleadSessionIdteamAllowedPathsmembers[]
而 members[] 里又包含:
agentIdnameagentTypemodelpromptcolorplanModeRequiredjoinedAttmuxPaneIdcwdworktreePathsessionIdsubscriptionsbackendTypeisActivemode
从这个结构就能看出来:作者把“team”设计成了一个完整的控制平面对象,而不是 UI 上的一组标签。
4.2 队友实际跑在哪儿,是由 backend registry 决定的
队友并不总是同一种运行形态。
src/utils/swarm/backends/registry.ts 负责解决“这个环境下队友该怎么运行”。
它的策略大致是:
- 如果会话明确配置为
in-process,那就用同进程执行; - 如果明确配置为
tmux,那就走 pane backend; - 如果是默认
auto:- 在
tmux里时,优先 pane backend; - 在 iTerm2 里时,也优先 pane backend;
- 否则使用
in-process;
- 在
- 非交互会话直接强制
in-process; - 如果原本想走 pane backend,但环境不具备条件,也能记录 fallback,改成
in-process。
这一步非常关键,因为它让“teammate”这个抽象从宿主环境里解耦了。
换句话说:
- 对上层编排来说,teammate 只是 teammate;
- 至于它到底是跑在
tmux pane、iTerm2 pane,还是同一进程的 ALS 上下文里,是 backend 决定的实现细节。
这就是典型的执行基座抽象。
4.3 spawnMultiAgent:真正的团队生成总控
src/tools/shared/spawnMultiAgent.ts 是团队生成的总控模块。
它负责的事情非常多:
- 解析队友名字、颜色、model、agent_type;
- 处理继承的 CLI flags 和 env;
- 决定使用哪个 backend;
- 在需要时创建
tmuxsession / pane; - 在
AppState和TeamFile里登记队友; - 针对不同 backend 采用不同的启动方式;
- 对 pane 模式下的初始 prompt 进行 mailbox 投递。
这一层最值得强调的,是它天然分成了两部分:
4.3.1 控制平面
控制平面关心的是:
- 谁是这个 agent;
- 它属于哪个 team;
- 它的
agentId是什么; - 它在 UI 里如何显示;
- 它在哪个 backend 上;
- 团队配置文件如何更新;
- 出错时如何清理。
4.3.2 数据平面
数据平面关心的是:
- 初始 prompt 怎么送到对方手里;
- 后续消息如何送达;
- 响应如何回来。
在 pane 模式下,这两件事是分离的:先把队友“建出来”,再通过 mailbox 发送初始消息。这种分层让实现更稳定,也减少了“spawn 和 prompt 注入耦死”的问题。
还有一个经常被忽略、但非常关键的点:pane 模式下生成出来的 teammate 并不会因为“它不在当前进程里”就失去系统管理能力。spawnMultiAgent.ts 会额外调用 registerOutOfProcessTeammateTask(...),把它注册成一条可观察、可终止、可在任务 UI 中展示的任务状态。
也就是说,作者追求的是:
- 执行宿主可以不同;
- 但系统管理界面和控制抽象要尽量统一。
4.4 pane teammate:另一种并列执行基座
如果 backend registry 决定当前环境适合 pane 模式,spawnMultiAgent 会进入 split-pane 或 separate-window 路径。
根据源码注释和控制流,它的大致行为是:
- 在 tmux 内部:leader 和 teammates 共享一个窗口布局,leader 在左,队友在右;
- 在 tmux 外部但可用 tmux 时:创建外部 swarm session,让多个队友在独立 pane/window 中运行;
- 在 iTerm2 且 it2 CLI 可用时:走原生 iTerm2 backend;
- 在 auto 模式下如果 pane backend 不可用:回退到
in-process,并记录 fallback 状态,让 UI 也反映真实模式。
这条路径并不是“开一个 pane 然后把 prompt 打进去”那么粗糙。它更像这样:
- 先创建 pane 或 session;
- 再把这个 teammate 登记到
AppState和TeamFile; - 补全
agentId / paneId / backendType / cwd / model等元数据; - 通过 mailbox 投递初始消息;
- 让远端 teammate 自己的 inbox poller 读取第一条消息并开始执行。
这个顺序很能说明作者的架构取向:先把控制平面建好,再让数据平面流动。
此外,out-of-process teammate 的 task 上依然挂了 AbortController。当它被触发时,系统会根据 backendType 调回对应 backend 去执行 killPane(...)。这再次说明 task 不是 UI 装饰,而是跨 backend 的统一控制入口。
4.5 spawnInProcessTeammate(...):同进程队友不是假的
很多系统说自己支持 in-process worker,本质只是“同步调用另一个函数”。这里不是。
src/utils/swarm/spawnInProcess.ts 做的事情足够严肃:
- 生成确定性的
agentId; - 生成独立
taskId; - 创建独立
AbortController; - 构建
TeammateIdentity; - 创建
TeammateContext,用于AsyncLocalStorage语义隔离; - 注册 Perfetto trace;
- 创建
InProcessTeammateTaskState; - 把它注册进 AppState;
- 安装清理逻辑。
这说明 in-process teammate 不是“普通子代理换了个名字”,而是同一进程中的常驻同行执行体。
4.6 InProcessTeammateTask:队友任务和后台子代理任务不是一种东西
src/tasks/InProcessTeammateTask/types.ts 很直白地显示了两者的差异。
队友任务状态有一些后台子代理没有的字段:
identityawaitingPlanApprovalpermissionModecurrentWorkAbortControllerpendingUserMessagesisIdleshutdownRequestedonIdleCallbacks
而且注释里写得很清楚:
- in-process teammate 支持 plan mode 审批流;
- 可以 idle,不必终止;
- conversation history 是用于 zoomed view 的 UI mirror,而完整对话保存在本地
allMessages和磁盘 transcript 中。
这说明队友的抽象核心不是“完成结果”,而是“持续存在、可切换工作状态”。
4.7 runInProcessTeammate(...):真正的常驻队友主循环
src/utils/swarm/inProcessRunner.ts 是整个多 agent 系统里最值得细读的文件之一。
它揭示了 in-process teammate 的真实模型:常驻 worker loop。
这个函数的关键特征如下。
4.7.1 它有自己的系统提示词体系
它会先构造 teammateSystemPrompt:
- 默认系统提示词;
- 再加上
TEAMMATE_SYSTEM_PROMPT_ADDENDUM; - 如果给定自定义 agent 定义,再附加 agent 自己的 prompt;
- 如果用户给了 append/replace 型 systemPrompt,也按模式合并。
这意味着队友在“人格和职责”上不是主代理的简单复制,而是有自己的运行准则。
4.7.2 它会强行注入团队协作必需工具
即便队友指定了显式工具列表,它也会补齐这些团队级工具:
SendMessageTeamCreateTeamDeleteTaskCreateTaskGetTaskListTaskUpdate
这件事很说明问题:作者认为“队友”首先是协作者,其次才是具体工作者。没有这些工具,它就不能真正参与团队协议。
4.7.3 它默认把 permissionMode 设为 default
这一点很值得注意。实现里有明确注释:队友应该始终保有完整工具访问能力,不应简单继承 leader 的限制模式。
这不是在“放大权限”,而是在配合后续的审批转发机制。因为 in-process teammate 并不是偷偷绕过审批,而是把需要 ask 的工具使用请求转给 leader 来批准。
4.7.4 它持有跨多轮的 allMessages
这是它和普通后台子代理最大的区别之一。
后台子代理通常是一项任务跑到底,完成就算。而 in-process teammate 会:
- 保存累计消息历史
allMessages; - 在每次收到新 prompt 后,把旧消息当上下文继续推理;
- 因此它是“持续存在的会话”,不是重复生成的新 agent。
4.7.5 它会自动 compact 自己的历史
当 allMessages token 数超阈值时,它会调用 compactConversation(...) 做压缩。
压缩时又非常谨慎:
- 会克隆
readFileState; - 不影响主会话的 UI 回调;
- 压缩后重建
allMessages; - 重置 microcompact 状态和 content replacement state;
- 同步更新 task.messages 的镜像,防止 AppState UI mirror 无边界膨胀。
这表明作者非常清楚“常驻 teammate 最大的资源风险就是历史无限增长”。
4.7.6 它有双层中断控制
这也是一个非常成熟的设计细节。
它同时维护:
- 生命周期级别的
abortController
用于终止整个 teammate; - 当前工作轮次的
currentWorkAbortController
用于只停止当前一轮工作,而不杀死这个 teammate 本身。
这意味着 UI 上“停掉当前工作”和“关闭整个 teammate”在底层是两种不同操作。
4.7.7 它重用 runAgent(...)
尽管 teammate 是常驻模型,但每一轮真正执行时,它仍然调用的是 runAgent(...)。
这说明作者没有维护两套完全独立的 agent 执行内核,而是:
- 用
runAgent(...)作为统一的单轮执行引擎; - 在外层增加一个 teammate-specific 的常驻循环。
这是一个非常聪明的复用方式:共享核心推理循环,差异放在生命周期壳层。
4.7.8 它执行完成后不会自动把结果“塞回 leader”
这点非常重要。
一轮工作做完后,队友会:
- 更新自己的 task 状态;
- 标记自己 idle;
- 通过 mailbox 通知 leader 自己空闲了;
- 然后继续等待下一条 prompt 或 shutdown。
但是它不会默认把完整工作产物自动回传给 leader。如果想把结果发给 leader,需要队友显式使用 SendMessage 等协作工具。
这和后台子代理完全不同。后台子代理的默认语义是“做完就发 task-notification”;队友的默认语义是“我是常驻成员,做完当前轮次后进入空闲状态,是否汇报由协作协议决定”。
这也是两套系统的本质差异之一。
4.8 waitForNextPromptOrShutdown(...):队友为什么能像常驻成员一样“等活”
这段逻辑几乎可以被看作一个小型 agent scheduler。
它每 500ms 轮询一次,并且按优先级处理不同来源的工作:
- 先看内存里的
pendingUserMessages; - 再查 mailbox;
- mailbox 里优先处理
shutdown_request; - 再优先处理 team lead 的消息;
- 其他 peer message 再按 FIFO。
这套优先级设计非常合理:
- 关机请求是控制面最高优先级;
- leader 消息代表用户意图,应避免被队友之间闲聊饿死;
- peer 消息是协作补充,不应抢 leader 的调度权。
换句话说,这里的 teammate 不是“消息来一条就执行一条”的被动消费者,而是一个有内部优先级策略的常驻 agent。
5. 多 Agent 之间到底怎么通信
5.1 背景子代理和队友,通信方式并不相同
这个仓库没有强行统一所有通信路径,而是按语义分工:
- 后台子代理
- 继续对话:
LocalAgentTask.pendingMessages - 完成回传:
<task-notification>+ 全局命令队列
- 继续对话:
- 团队队友
- 日常通信:mailbox 文件
- 控制消息:结构化 mailbox 消息
- 工作流回报:由队友主动使用协作工具发送
这是一种很实用的设计:对于短命 task,没必要上完整 mailbox;对于常驻 teammate,没有 mailbox 就会很别扭。
5.2 SendMessageTool 是两套系统的统一路由器
src/tools/SendMessageTool/SendMessageTool.ts 是整个多 agent 系统的交通枢纽。
它能处理三类目标:
- 后台子代理
- 正在运行:
queuePendingMessage(...) - 已停止:
resumeAgentBackground(...)
- 正在运行:
- 团队队友
- 通过
writeToMailbox(...)发送到 inbox
- 通过
- 跨会话目标
uds:、bridge:之类目标
这意味着 SendMessage 不是简单的“发一条文本”,而是一个多协议路由器。
5.3 队友 mailbox 是文件协议,不是内存事件总线
src/utils/teammateMailbox.ts 里能看到完整实现:
- inbox 路径:
~/.claude/teams/{team}/inboxes/{agent}.json - 每条消息都有
from / text / timestamp / read / color / summary - 写入前先确保目录和 inbox 文件存在;
- 通过 lockfile 和重试保证并发写入安全;
- 读消息、标记已读也都有明确函数。
这套设计的优点是:
- 与进程模型解耦;
- tmux pane、iTerm2 pane、in-process teammate 都能共用;
- 出了问题可以直接检查磁盘文件;
- 不依赖一个中央常驻 broker 进程。
它的代价也很明显:
- 有轮询延迟;
- 文件 IO 与锁开销比纯内存队列大;
- 语义更偏“可靠投递”而非“低延迟流式通信”。
但对 CLI agent 团队来说,这其实是很合适的折中。
5.4 结构化消息揭示了它不是“聊天”,而是“协作协议”
SendMessageTool 不只发字符串,还支持结构化消息:
shutdown_requestshutdown_responseplan_approval_response
这说明队友间通信不是自然语言聊天那么简单,而是有一层显式控制协议。
例如 shutdown 流程:
- leader 发送
shutdown_request; - teammate 决定批准或拒绝;
- 如果批准,给 leader 发确认;
- in-process teammate 还会直接触发自己的 abort controller。
这是一套完整的控制面协议,而不仅仅是“Hey,停一下”。
6. 这套系统最妙的一点:结果如何重新回到主模型
这是本仓库多 agent 设计里最值得反复琢磨的一点。
6.1 后台任务完成后,并不是宿主代码偷偷消费结果
如前所述,后台子代理结束后会调用 enqueueAgentNotification(...),把 XML 推进全局消息队列。
而在 src/query.ts 里,主循环会在适当时机从命令队列中取出这些待消费项。
实现里明确写了:
- 主线程只消费
agentId === undefined的命令; - 子代理只消费发给自己的
task-notification; prompt类型命令仍然只进主线程,不会乱灌给子代理。
这个按 agent 作用域划分的队列消费策略,非常像一个轻量级 actor mailroom。
6.2 task-notification 会被包装成模型可读消息
在 src/utils/messages.ts 里,wrapCommandText(...) 对 task-notification 的包装是:
A background agent completed a task:
{raw_xml}
也就是说,这个通知最终会以附件/消息的形式进入模型上下文。
6.3 print.ts 会发 SDK 事件,但仍然继续喂给模型
src/cli/print.ts 对 task-notification 的处理也非常有意思:
- 先解析 XML;
- 给 SDK 消费者发系统事件;
- 然后仍然继续调用
ask(),让模型看到这条结果并自己决定下一步行动。
这是一个特别“agentic”的选择。
它意味着:
- 系统不替主模型做最后决策;
- 主模型能基于 worker 回来的结果继续规划;
- worker 结果天然进入主线程因果链,而不是游离在外。
6.4 为什么这比传统 callback 更强
传统 callback 模型的思路是:
- worker 完成;
- host 拿到结果;
- host 决定是否提醒主 agent。
而本仓库的做法是:
- worker 完成;
- 结果转成结构化消息;
- 结果进入消息总线;
- 主 agent 在自己的语境里重新“看到”它。
这带来几个好处:
- 主模型拥有上下文连续性
它不是被动收到一个宿主总结,而是直接看到原始完成信号。 - 编排逻辑可以继续由模型承担
比如继续追问同一个 worker、综合多个 worker 结果、停止另一个 worker。 - 上层 UI/SDK 和下层 agent loop 共享同一事件来源
不需要分别维护一套 callback 协议和一套模型消息协议。
从架构视角看,这是一种把“agent runtime”真正做成“消息驱动系统”的方法。
7. 权限、隔离与控制:这套协作为什么没有失控
多 agent 系统一旦做复杂,最容易失控的就是权限和边界。本仓库在这方面下了很多功夫。
7.1 子代理默认不是无脑继承父代理权限
runAgent(...) 在构建子代理上下文时,会根据 agent 定义、调用形态、是否异步、是否 fork 等条件,重建工具池和上下文。
这意味着:
- 可以限制某个 agent 能看到的工具;
- 可以根据 permission mode 调整行为;
- fork 子代理可以为了 cache 一致性保留 exact tools;
- 普通 agent 则可以用更受控的工具集运行。
7.2 in-process teammate 的权限请求会冒泡给 leader
createInProcessCanUseTool(...) 是整个权限协作设计的关键。
当工具权限检查结果是 ask 时,它不会让 teammate 自己偷偷决定,而是:
- 先看 bash classifier 能不能自动批准;
- 如果不行,优先使用 leader 的
ToolUseConfirmUI 队列; - 如果没有 bridge/UI,再退回 mailbox 请求。
也就是说,同进程队友虽然和 leader 在一个进程里,但它依然走了一条经由 leader 审批的受控路径。
这非常重要,因为它避免了一个常见问题:
“既然是在同进程里,那不就什么都能干了吗?”
源码给出的答案是:不是。同进程只是执行宿主同一个 Node 进程,不代表权限治理就被绕过。
7.3 队友不是完全隔离,也不是完全共享
这是这套设计里最成熟的现实主义部分。
它没有追求虚假的“绝对沙箱”。相反,它做的是有边界的共享:
- 共享:
- 同一进程资源;
- 一些主会话上下文;
- 统一权限 UI;
- 全局消息队列;
- 隔离:
agentId/TeammateIdentityAbortControllerallMessages- task state
- transcript
AsyncLocalStorageteammate context
这比“全共享”安全,也比“全隔离”实用。
7.4 planModeRequired 和独立 permissionMode
队友状态里会记录:
planModeRequiredpermissionMode
而 leader 还能对不同队友独立切换 permission mode。
这说明权限不是会话级唯一值,而是成员级运行属性。这对多 agent 团队非常关键,因为不同成员可能正在做不同风险级别的事。
8. 这套多 Agent 系统为什么能持久、可恢复、可观察
这是它和很多“研究原型式 agent 框架”的最大区别。
8.1 每个 agent 都有自己的 transcript
后台子代理和队友都会在不同程度上维护 transcript。
这带来的好处是:
- agent 不是黑盒;
- UI 能查看历史;
- agent 可以 resume;
- 调试时可以追踪真实对话过程;
- 运行时中断不至于把一切状态清空。
8.2 任务输出有独立 output file / symlink
LocalAgentTask 注册时会初始化 output symlink,这让外部系统和 UI 能直接找到任务输出路径。
这其实是一种“把 agent 结果实体化到文件系统”的思路。对于 CLI 产品来说非常实用,因为文件路径天然适合:
- tail / 查看;
- 外部工具集成;
- 会话恢复。
8.3 团队有 TeamFile
队伍不是只活在内存里,团队成员与元数据会落到 config.json 一类的持久文件中。
这让系统能够:
- 发现已有团队;
- 管理成员;
- 更新 mode;
- 做 session cleanup;
- 在 UI 和 backend 之间共享控制平面状态。
8.4 队友有 idle 状态,不必非得 terminate
这是一个非常产品化的细节。
很多系统的 worker 完成一次推理后就算结束,而这里的 teammate 可以处于:
runningidleshutdownRequested- terminal status
这让“长期协作”真正成立。否则你每次都只是重新 spawn 一个新 agent,那就不叫 teammate,只是更重的 subagent。
8.5 Cleanup 是架构的一部分,不是补丁
无论是 runAgent(...) 的 finally,还是 task 注册时的 cleanup handler,都说明作者从一开始就把“如何收尾”视为架构设计的一部分。
这是多 agent 系统能长期稳定运行的关键。
9. coordinatorMode:这套系统不只是 runtime,还有一套“工作哲学”
src/coordinator/coordinatorMode.ts 里最有意思的地方,不是一个布尔开关,而是它提供的系统提示词。
这个提示词明确告诉 coordinator:
- 你的职责是编排,不是自己做所有事;
- worker 结果会以
<task-notification>形式回来; - 不要用一个 worker 去盯另一个 worker;
- 研究任务可以并行;
- 写代码和验证要分阶段;
- worker 完成后应考虑继续同一个 worker,而不是总重开新的;
- 验证必须是真验证,不是形式化过场。
这说明作者对多 agent 的理解不是“加几个 API 就行”,而是:
- runtime 要提供协作基础设施;
- system prompt 要教会模型如何正确使用这些基础设施。
这两者缺一不可。
如果没有 runtime,prompt 再聪明也没用;
如果只有 runtime,没有 prompt 约束,模型也很容易把多 agent 用得一团乱。
所以 coordinatorMode 本质上是这套产品的操作手册内化版本。
10. 这套设计最值得称道的地方
10.1 它把“agent”提升成了系统级实体
在很多框架里,agent 只是函数包装、链式调用对象或 prompt 模板。
而在这里,agent 拥有:
- 身份;
- 生命周期;
- 磁盘转录;
- 持久化元数据;
- 权限路径;
- 通信协议;
- UI 表示;
- 可恢复能力。
这已经很接近一个小型“agent OS”了。
10.2 它没有迷信单一协作模型
后台子代理和团队队友分开,是非常正确的工程判断。
现实里确实存在两类需求:
- 一次性委派;
- 长期团队协作。
把两者硬做成同一抽象,最后通常会两边都不好用。
10.3 它对执行基座做了抽象
pane teammate 和 in-process teammate 共享的是概念模型,不共享的是宿主实现。
这使得产品可以根据环境灵活选择:
- 有终端 UI 时,用 pane 获得更直观的物理分离;
- 没有条件时,用 in-process 保持功能可用;
- 对上层用户来说,协作体验尽量一致。
10.4 它把结果回流做成消息,而不是宿主内部黑箱
这是非常 agent-native 的设计,也是本文最想强调的一点。
因为一旦你把 worker 结果变成宿主内部 callback,主模型就失去了“自然接续”的能力;而把它做成结构化消息重新注入上下文,才真正把多 agent 纳入统一推理空间。
10.5 它对资源泄漏和状态膨胀非常警觉
从 transcript 清理、shell task kill、队友 messages cap、auto compact、fallback 标记等细节可以看出,这套系统不是只追求 demo 成功,而是明显考虑了长会话、重并发、鲸鱼级别 session 的稳定性问题。
11. 这套设计的代价和复杂度
说优点的同时,也必须承认它的成本。
11.1 概念面很多
要真正理解这套多 agent 系统,你至少得同时理解:
- AgentTool
- runAgent
- LocalAgentTask
- InProcessTeammateTask
- SendMessageTool
- TeamFile
- mailbox
- command queue
- coordinatorMode
这显然比“单文件 worker pool”复杂得多。
11.2 同进程隔离始终不是硬隔离
即便用了 AsyncLocalStorage、独立 abort controller、独立 task state,同进程 teammate 终究还是共享一个 Node 进程。
这带来的好处是低成本、集成强;坏处是边界永远比不上真正独立进程。
作者的策略不是假装它是硬隔离,而是通过权限代理、状态分层和协议限制来尽量控制风险。这是现实主义做法,但也意味着系统复杂度会上升。
11.3 mailbox 轮询不是零延迟
文件 mailbox 的优点是通用和稳定,缺点是:
- 需要轮询;
- 有锁和 IO;
- 不适合特别高频的细粒度通信。
不过从 CLI 工程协作的使用模式看,这个代价多数时候是可以接受的。
11.4 TeamFile 的扁平 roster 带来结构约束
代码里明确禁止队友再生队友,就是因为目前 team roster 是扁平的。
这意味着:
- 团队结构简单;
- 但嵌套组织、代理来源追踪、树状管理能力会受限。
这是一个明显的工程取舍:先把简单 team model 做扎实,而不是一开始追求无限嵌套。
11.5 Runtime 与 Prompt 是强耦合的
这套系统之所以好用,一个重要原因是 coordinatorMode 等 prompt 明确教模型如何工作。
这同时也意味着:如果 prompt 层失配、退化或被错误覆盖,runtime 再强,也可能被模型用歪。
不过在 agent 产品里,这种“prompt-runtime 双轮驱动”其实很常见,也很难完全避免。
12. 如果把整个多 Agent 系统压缩成一张图
12.1 后台子代理路径
用户/主线程
-> AgentTool
-> 判断:普通子代理 / teammate / remote / worktree
-> registerAgentForeground 或 registerAsyncAgent
-> runAgent
-> 组 prompt / 工具 / MCP / 子上下文
-> 跑 query 循环
-> 记录 transcript / metadata
-> LocalAgentTask 持续更新状态
-> 完成/失败/停止
-> enqueueAgentNotification(XML)
-> 全局命令队列
-> 主线程下一轮读取 task-notification
-> 主模型决定下一步:汇总 / 继续 / 再派活 / 停止其他 agent
12.2 团队队友路径
主线程
-> TeamCreateTool
-> 写 TeamFile / AppState.teamContext / task list
-> spawnMultiAgent
-> backend registry 决定 in-process 或 pane
-> spawnInProcessTeammate 或 tmux/iTerm2 spawn
-> 队友注册为 task
-> 队友进入 runInProcessTeammate 常驻循环
-> 等消息 / 等任务 / 等审批 / 等 shutdown
-> 每次收到 prompt 时调用 runAgent 执行一轮
-> 完成后进入 idle
-> 通过 SendMessage / mailbox 参与协作
12.3 两条线最终如何汇合
汇合点不是“同一个 spawn API”,而是:
- 都有 task 状态;
- 都能被 UI 观察;
- 都有 transcript;
- 都受权限系统约束;
- 都最终通过消息机制参与主线程决策。
因此,更准确的说法是:
这个仓库并不是“实现了多 agent 工具调用”,而是“实现了一个围绕任务、消息、权限、转录和团队控制平面的 agent 协作运行时”。
13. 作为源码阅读者,最值得继续深挖的点
如果要继续往下研究,我认为最值得进一步精读的是这几组文件:
13.1 任务型多 agent 主线
src/tools/AgentTool/AgentTool.tsxsrc/tools/AgentTool/runAgent.tssrc/tasks/LocalAgentTask/LocalAgentTask.tsxsrc/tools/AgentTool/resumeAgent.ts
这四个文件合起来,基本就是“后台子代理”完整机制。
13.2 团队型多 agent 主线
src/tools/TeamCreateTool/TeamCreateTool.tssrc/tools/shared/spawnMultiAgent.tssrc/utils/swarm/backends/registry.tssrc/utils/swarm/spawnInProcess.tssrc/utils/swarm/inProcessRunner.tssrc/utils/teammateMailbox.ts
这几组文件决定了 teammate 是怎么被生成、如何存活、如何通信、如何在不同 backend 上保持抽象一致。
13.3 协作结果回流主线
src/tools/SendMessageTool/SendMessageTool.tssrc/query.tssrc/utils/messageQueueManager.tssrc/utils/messages.tssrc/cli/print.tssrc/coordinator/coordinatorMode.ts
这里能看清楚“为什么 worker 结果不是回调,而是重新进入主消息循环”。
14. 最终结论
如果只用一句更技术化的话来概括这个仓库的多 agent 设计,我会这样说:
它把 agent 从“模型调用的别名”提升成了“带身份、带状态、带存储、带协议、带权限治理的运行时实体”,并且把短命任务代理与长命团队成员这两种协作语义明确拆开,再通过统一的消息回流机制让它们共同服务于主线程编排。
而如果换成更通俗的话,那就是:
这里的多 agent 不是“多开几个模型窗口”,而是真的在做一个小型协作操作系统。
这也是为什么它看起来复杂,但复杂得很有道理。因为它解决的已经不是“能不能开 worker”,而是“worker 作为系统成员,如何活着、如何沟通、如何被管理、如何被继续使用、以及如何把结果自然地带回主线程”。
Comments NOTHING