本章从 s10 Task System 继续扩展共享任务与多人协调,不包含 s11 Background Tasks 或 s12 Cron Scheduler;这些机制到 s15 才统一集成。

把一个复杂目标拆给多个 Agent,并不只是同时启动几次模型调用。团队运行时还要回答:谁负责拆任务?谁能修改任务图?两个队友会不会认领同一项工作?消息如何投递?并行修改发生冲突怎么办?什么时候允许启动和关闭队友?

Agent Teams 把这些问题拆成几个协作组件:

用户


Lead Agent ──────── MessageBus ──────── Teammates
  │                                         │
  ├──────────── shared Task Store ──────────┤
  │                                         │
  └──────── task-bound Worktrees ───────────┘

多个 Agent Loop 只是执行单元。真正让它们形成团队的是共享状态、消息协议、原子认领、权限边界和生命周期管理。

Lead 和队友承担不同职责

当前课程采用一个 Lead 加多个 teammate:

  • Lead 与用户对话、设计分工、创建任务图、启动队友并汇总结果;
  • teammate 拥有独立上下文,认领任务、执行工具、上报结果并回到空闲状态;
  • Harness 负责消息投递、任务所有权、worktree 绑定和控制协议。

Lead 不是“能力更强的模型类型”,而是拥有不同 system prompt 和工具集合的 Agent 实例。角色边界主要由运行时暴露哪些工具来实现。

例如,只有 Lead 能创建任务、修改依赖和创建 worktree;队友只能列举、认领和完成任务,不能在团队运行期间任意改写任务图结构。

这种能力收窄比只在提示词里说“不要修改依赖”更可靠,但每个 handler 仍需在运行时检查身份和状态。

启动团队是一项需要授权的扩容动作

增加 teammate 会改变:

  • 模型调用成本;
  • 并发数量;
  • 可访问工作区的 Agent 数量;
  • 可能产生的文件修改和命令副作用;
  • 用户需要审阅的结果数量。

当前课程要求 Lead 先提出小型团队方案,等待用户确认后再调用 spawn_teammate。这是正确的产品边界。

但需要注意:当前示例主要依靠 system prompt 约束这一步,spawn_teammate handler 本身没有校验一个由宿主签发的“用户已批准”令牌。如果模型错误地提前调用,运行时仍可能启动线程。

生产系统更适合把授权机械化:

Lead 提出 team proposal

用户批准 proposal_id

运行时记录批准范围、人数和预算

spawn 必须携带有效 approval_id

批准还应有范围和时效,不能用一次“可以”无限扩容后续团队。

持久 teammate 与一次性 Subagent 的区别

Subagent 通常完成一个委派后就返回最终文本;teammate 则是长期运行单元:

WORK → 上报 result → IDLE → 接收消息或认领新任务 → WORK

                                             └→ shutdown

队友拥有自己的 messages[],可以跨多个任务保留上下文。它不会把完整工具历史复制给 Lead,也不会和其他队友共享同一消息数组。

这减少了上下文互相污染,但持久上下文本身也会不断增长。当前团队章节并不意味着自动继承此前 Context Compact 的全部机制;长期队友仍需要单独的压缩、预算和重启恢复策略。

持久身份还意味着必须定义:队友重启后是否仍是同一个 owner、旧消息如何恢复、未完成任务是否重新投递,以及过期上下文何时清空。

MessageBus 把通信放在上下文之外

课程为每个成员维护一个 JSONL 收件箱:

.mailboxes/
  lead.jsonl
  alice.jsonl
  bob.jsonl

每行是一个完整 JSON 对象。正文中的换行会由 JSON 编码为转义序列,不会天然把一条消息拆成多行。

当前消息结构将扩展信息放入 metadata,而不是用 msg.update(extra) 直接覆盖基础字段。这能避免额外数据意外改写 fromtotype

消息总线使用进程内 Condition 保护文件访问并唤醒等待线程。队友处于 IDLE 时不会高频空转,而是等待消息或按间隔检查 ready task。

这套锁主要保护当前 Harness 进程中的线程。若多个独立进程同时写同一收件箱,还需要跨进程文件锁、数据库或真正的消息队列,否则追加和消费语义不一定可靠。

观察和消费必须分开

读取 Lead 收件箱会消费并删除消息文件,因此只能有一个明确消费者。当前主循环通过 consume_lead_inbox() 读取事件、更新协议状态,再把格式化后的团队事件加入 Lead 历史。

调试界面的“查看收件箱”不应复用 destructive read,否则人工观察可能抢走运行时本该处理的消息。正确设计通常提供:

  • peek:只观察,不改变投递状态;
  • consume/ack:处理成功后确认;
  • retry/dead-letter:处理失败时重试或隔离;
  • offset/message ID:避免重复或漏读。

课程中的读后删除更接近进程内演示,不具备持久消息队列的至少一次投递保证。若进程在删除文件后、事件写入 Lead 上下文前崩溃,消息可能丢失。

团队事件会主动唤醒 Lead

与 Background Tasks 只在下一次现有 Agent Loop 收集不同,当前 CLI 同时等待终端输入和 Lead 收件箱。队友消息到达时,运行时会:

消费 Lead inbox

更新协议状态

追加 [Team events]

启动新的 Lead Agent Loop

因此,Lead 不需要反复调用 list_teammates 轮询结果。

产品化时仍要限制唤醒风暴:多个队友短时间连续发送消息,可以批量合并;低优先级状态事件可以延迟;每次唤醒都应受模型调用预算和会话生命周期约束。

result 和 idle 是两个不同事件

队友完成任务后,课程依次发送:

result: 任务产出了什么
idle_notification: 队友现在可以接新工作

“工作结果”和“执行单元状态”不应混为一句“完成了”。任务可能已经给出结果,但队友还在清理 assignment;队友也可能空闲,却没有成功完成上一任务。

更完整的事件模型可以包括:

  • task_started;
  • task_progress;
  • task_result;
  • task_failed;
  • teammate_idle;
  • teammate_shutdown;
  • teammate_error。

每个事件应带稳定 ID、任务 ID、发送者、时间和版本,便于去重与审计。

IDLE 时先处理控制消息,再找任务

队友进入 IDLE 后,当前优先级是:

  1. 等待并处理收件箱;
  2. 若没有消息,再扫描共享任务板;
  3. 找到 ready task 后尝试认领;
  4. 认领成功才回到 WORK。

这样,关机请求、计划审批和 Lead 的直接指令不会被临时发现的新任务长期饿死。

候选扫描只是快照。多个队友可能同时看见同一个 pending 任务,所以“发现”与“获得所有权”必须分开,真正决定胜负的是原子 claim。

原子认领比扫描结果更重要

当前课程对任务 Store 做了比单 Agent 章节更强的处理:claim 同时取得进程内锁和文件锁,在锁内重新读取任务并检查:

  • 任务仍为 pending 且无人拥有;
  • owner 当前没有另一项 in-progress 任务;
  • 所有依赖已经完成;
  • 任务绑定的 worktree 仍然有效。

只有检查全部通过,才写入 owner 和 in_progress。保存使用临时文件加原子替换,避免普通覆盖在崩溃时留下半个 JSON。

这让同一任务目录上的协作进程在遵守同一锁协议时更可靠。但文件锁仍有边界:

  • 只约束合作方,绕过 Store 直接写文件的进程不受保护;
  • 不同操作系统支持不同,当前实现使用 fcntl,主要面向 POSIX;
  • 网络文件系统的锁和原子替换语义需要单独验证;
  • 文件状态与外部副作用无法形成同一事务。

规模扩大或跨主机时,数据库条件更新通常更合适。

没有任务所有权,就不能操作工作区

课程将 teammate 的文件与 Shell 工具绑定到当前 assignment。没有认领任务的队友不能直接回退到仓库根目录继续修改。

这是重要的运行时约束:

claim task

获得 task_id + cwd lease

文件与 Bash 工具从 lease 解析工作目录

当前模型轮次结束后释放已完成 assignment

任务完成后不会立刻清除 cwd,因为同一次模型响应后面可能还有验证或读取工具。等当前轮次结束、队友回到 IDLE 时再释放,可以避免一批工具调用中途失去执行目录。

若持久任务记录中的 worktree 绑定失效,运行时直接失败,不应悄悄回退到主仓库,否则原本隔离的修改会落到错误目录。

Worktree 隔离 Git 修改,不隔离系统权限

Lead 可以为 pending、未认领任务创建并绑定 worktree。队友认领后,文件和 Shell 工具在该 checkout 中运行。

这能减少多个 Agent 同时修改同一工作树造成的 Git 冲突,但不等于安全沙箱:

  • worktree 共享同一个 Git 仓库元数据;
  • Shell 仍能访问父进程有权限访问的其他路径;
  • 多个任务仍可能争用端口、数据库、缓存和外部服务;
  • 创建分支和 worktree 不会自动合并成果;
  • 主仓库中的未提交状态和起始提交必须明确。

任务完成和 worktree 清理是两个动作。模型只能创建绑定,移除保留给宿主,以便先检查未提交、未跟踪和已忽略文件。即使目录被移除,本地分支也应保留,避免丢失已完成提交。

任何带丢弃改动的移除,都必须由用户明确授权。

关机需要握手,不是直接杀线程

课程使用类型化请求与响应:

Lead: shutdown_request(request_id)

teammate 完成当前安全步骤

shutdown_response(request_id)

pending → approved,线程退出

request_id 用于把响应关联到原请求,类型检查阻止计划响应误改关机状态,状态检查阻止同一响应重复生效。

平滑关机允许队友收尾,但不能无限等待。生产系统还需要截止时间、强制取消、未完成任务回收、文件状态报告和进程退出后的孤儿检测。

队友线程是 daemon thread,主进程异常退出时不保证完成握手或清理。若任务必须可靠存活,应使用独立 worker 和持久控制平面。

计划审批必须成为执行闸门

Lead 可以要求 teammate 先提交计划。计划状态为 required、pending 或 rejected 时,运行时阻止:

  • Bash;
  • write_file;
  • edit_file。

读取和 glob 仍可使用,让队友收集足够信息修订计划。审批通过后才开放修改型工具。

闸门还记录 task ID 和 work version。队友换任务后版本变化,旧任务的审批不能继续授权新任务,这避免了 stale approval。

审批机制仍有边界:

  • 通过只代表 Lead 同意计划,不代表用户批准了所有高风险副作用;
  • 工具分类必须完整,新增修改型工具时要加入闸门;
  • 只拦 Bash、写入和编辑,其他工具若能间接修改状态也需归类;
  • 计划正文可能遗漏风险,运行时权限检查仍不可省略。

计划闸门、用户授权和工具权限是三个不同层次。

队友不能直接向用户弹出权限询问

后台线程若同时调用终端 input(),会和 Lead 或其他队友争抢 stdin。当前课程让 teammate 在遇到危险命令或工作区外路径时返回“需要权限”,由 Lead 与用户协调。

这是正确方向,但生产协议最好生成结构化 permission request:

{
  "type": "permission_request",
  "requestId": "req_...",
  "agent": "alice",
  "taskId": "task_...",
  "operation": "bash",
  "risk": "network-write",
  "details": "..."
}

用户批准后,授权应绑定具体操作、参数摘要、任务和有效期,不能只回一句自由文本“可以”。

消息内容不能自动变成高优先级指令

收件箱、任务描述和 teammate result 都可能包含来自文件、外部系统或其他模型的不可信内容。Lead 把 [Team events] 注入对话后,应把它们视为协作数据,而不是 system instruction。

团队内部也需要防提示注入和身份冒充:

  • sender 由 MessageBus 根据已认证运行时身份填写;
  • teammate 不能自由伪造 from=lead
  • 控制消息必须校验 type、request ID、目标和当前状态;
  • 普通文本不能触发关机、审批或权限变化;
  • 日志与 UI 应转义消息内容。

旧版 msg.update(extra) 允许同名键覆盖基础字段,不适合控制协议。当前使用独立 metadata 是更清晰的边界。

团队运行时仍不是分布式系统

课程在单机进程中组合线程、文件锁、JSONL 和 Git worktree,足以展示核心协议,却没有提供完整的分布式保证:

  • team roster、pending request 和 teammate messages 主要在内存中;
  • 进程重启后线程消失,控制协议状态可能丢失;
  • mailbox 消费没有持久 ack 和重试;
  • 模型 API 失败可能让任务停留在 in-progress;
  • 外部副作用与任务状态更新不是事务;
  • 没有全局预算、公平调度或弹性扩缩容。

需要跨主机和长期运行时,应把任务状态、消息队列、租约、心跳和审计迁移到持久协调基础设施,并设计幂等 worker。

成本、冲突和最终验收仍由 Lead 负责

并行只在任务真正独立时缩短时间。更多 Agent 也会带来:

  • 更多输入输出 token;
  • 重复读取和重复推理;
  • 分支合并与接口漂移;
  • 测试环境争用;
  • 汇总错误和结果不一致。

Lead 应控制团队规模,明确模块边界和接口契约,并在最后执行整体验证。单个 teammate 报告“测试通过”不代表所有 worktree 合并后仍然通过。

最终 Gate 应基于合并后的真实代码、完整测试和用户验收,而不是队友消息数量。

建议覆盖的测试

Agent Teams 至少应验证:

  • 未经运行时授权不能启动 teammate;
  • 保留名称、大小写重名和非法成员名;
  • 初始任务认领失败时不会留下活跃线程;
  • 每个 teammate 拥有独立消息历史;
  • JSONL 中换行正确转义,metadata 不能覆盖基础字段;
  • peek 不消费,consume 只有一个消费者;
  • 消息到达能唤醒 Lead,批量事件不会形成唤醒风暴;
  • result 与 idle_notification 分别投递;
  • IDLE 优先处理控制消息,再扫描 ready task;
  • 多个 teammate 同时发现同一任务时只有一个 claim 成功;
  • 文件锁、原子替换、崩溃恢复和网络文件系统语义;
  • 无 assignment 时工作区工具被拒绝;
  • task-bound cwd 在完成轮次结束前保持有效;
  • worktree 失效时不会回退主仓库;
  • 并行 worktree 的合并、冲突和外部资源争用;
  • shutdown 请求关联、重复响应、超时和强制回收;
  • 计划审批绑定当前 task 和 work version;
  • 所有具有副作用的新工具都受计划与权限闸门控制;
  • teammate 权限请求由 Lead 安全转交用户;
  • 进程重启后的 mailbox、任务、lease 和协议恢复;
  • 全局 token、并发、时间和任务预算;
  • 最终集成验证不依赖 teammate 自报完成。

小结

Agent Teams 的核心不是“多开几个模型”,而是建立一个可协调的团队运行时:Lead 管理分工,持久 teammate 在 WORK 与 IDLE 之间切换,MessageBus 投递事件,共享任务 Store 原子认领工作,可选 worktree 隔离 Git 修改,类型化协议处理关机和计划审批。

这些机制让单机教学系统具备了团队雏形,但还不是可靠分布式平台。用户授权需要运行时强制,消息需要持久 ack,线程需要恢复与心跳,worktree 需要合并治理,任务完成需要集成验证。只有把状态、身份、权限、故障和成本都纳入协议,多 Agent 才真正比单 Agent 更可控。

参考资料