本章从 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) 直接覆盖基础字段。这能避免额外数据意外改写 from、to 或 type。
消息总线使用进程内 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 后,当前优先级是:
- 等待并处理收件箱;
- 若没有消息,再扫描共享任务板;
- 找到 ready task 后尝试认领;
- 认领成功才回到 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 更可控。