Learn-Claude-Code | 笔记 | Collaboration | s10 Team Protocols
Learn-Claude-Code | 笔记 | Collaboration | s10 Team Protocols
目录
前言
在上篇文章 Learn-Claude-Code | 笔记 | Collaboration | s09 Agent Teams 中,我们介绍了开源项目 learn-claude-code 第九个章节 s09: Agent Teams 的内容,这篇文章我们继续跟着教程文档来学习协作后续内容,记录下个人学习笔记,和大家一起分享交流😄
Note:本篇文章主要学习记录教程第五部分 Collaboration 中 s10: Team Protocols 章节的内容。
github:https://github.com/shareAI-lab/learn-claude-code
reference:https://chatgpt.com/
1. s10: Team Protocols
如果说 s09 的 Agent Teams 解决的是 “多个持久存在的 teammate 如何被组织起来,并通过文件邮箱彼此通信”,那么到了 s10,这一节开始继续沿着 Collaboration 这条主线往前走,但它要回答的问题已经比 s09 更进一步了:
当队友之间已经能够发消息、收消息、各自独立工作之后,这些通信本身该不该有规矩?如果某些协作动作本质上不是普通聊天,而是一种需要确认、需要关联请求与响应、甚至需要明确状态转移的 “握手过程”,那系统该怎么表达它?
前面 s09 我们已经看到,一个团队里可以有 lead、coder、reviewer 这样的命名成员;每个成员都有自己的收件箱,也都有自己的 agent loop,彼此之间通过 JSONL inbox 文件进行通信。这一步已经非常重要了,因为它第一次让系统从 “单体 Agent” 长成了 “多 Agent 协作系统”。
但 s09 的通信本质上仍然是比较自由的:你可以给队友发普通消息,也可以广播,但这些消息更多只是 “内容被送到了某人的 inbox”,而不是 “通信双方已经围绕某个明确协议完成了一次结构化握手”。这一点从 s09 的消息类型也能看出来,虽然已经有 message、broadcast 这样的结构化外壳,但大多数协作仍然停留在 “发了一条消息,期待对方自己决定怎么理解” 的层面。
这在简单协作里当然够用,但一旦我们开始考虑更严肃的团队动作,就会立刻发现问题。比如说:
- lead 想让某个 teammate 优雅退出
- teammate 想先提交一个高风险计划,等待 lead 批准后再开始执行
- 系统需要知道某次请求到底是还在 pending,还是已经 approved,还是被 rejected
- 收到一条响应时,系统必须能判断:它到底是在回复哪一次请求?
这些场景的共同点在于,它们都不是 “随便发句自然语言” 就能稳定处理的,因为它们天然需要两件事:
第一,需要 请求与响应之间的关联关系。
也就是说,一条响应不能只是 “我同意” 或者 “我拒绝”,它还必须能说明:我是在回复哪一次请求。
第二,需要 明确的状态转移。
也就是说,请求发出去之后,并不是 “有没有消息回来” 这么简单,而是会经历:
pending -> approved
pending -> rejected
这其实就是一个最小的 有限状态机(FSM)。
Note:有限状态机(FSM)又称有限状态自动机(FSA),简称状态机,是表示有限个状态以及这些状态之间的转移和动作等行为的数学计算模型,更多介绍大家可以参考 [Wikipedia]。
所以 s10 想解决的问题,已经不再只是 “队友能不能通信”,而是:
当团队协作里出现需要确认、需要关联请求与响应、需要明确状态流转的动作时,系统该如何把这些动作从普通消息提升成协议?
这也是为什么这一节叫 Team Protocols。它不是简单地在 s09 上再加几个新消息类型,而是第一次正式把 “队友之间的某些互动” 建模成了结构化握手协议。文档里对这一点说得很直接:这一节要实现的是 shutdown protocol 和 plan approval protocol,这两者虽然作用领域不同,但都建立在同一个核心模式上 — 用相同的 request_id 关联请求和响应。
2. 问题
我们先来看一个很朴素的问题:为什么 s09 那种 “大家已经能发消息了” 的团队协作,到这里还是不够用?🤔
从直觉上看,s09 已经很强了。lead 可以 spawn 队友,队友可以在自己的线程里跑 loop,也可以通过收件箱互相发消息。那按理说,lead 如果想让某个 teammate 停下来,直接发一句:
Please stop working.
不就行了吗?
或者某个 teammate 想在大改代码前先征求意见,直接给 lead 发一句:
Here is my plan...
不也能工作吗?
问题就在于,这种方式虽然 “看上去能沟通”,但它缺少一种非常关键的东西:结构化协调。
文档里把这个问题拆得很清楚,主要体现在两个方面:
第一,是 关机 / 退出 这件事。
如果要让某个 teammate 停下来,最粗暴的方式当然是直接杀线程。但这样做会留下很多问题:
- 可能正在写文件写到一半
- 可能还没把状态写回 config.json
- 可能 inbox 里还有未处理消息
- 可能系统根本不知道它是 “正常退出” 还是 “被强行中断”
所以,真正合理的方式不应该是直接 kill,而应该是一个 礼貌的请求-响应握手:
- lead 发出 shutdown_request
- teammate 自己决定 approve 还是 reject
- 如果 approve,就先收尾,再干净退出
- 如果 reject,就继续保持工作状态
也就是说,这里需要的不是一句自然语言,而是一个带状态流转的协议。
第二,是 计划审批 这件事。
团队协作里,并不是所有动作都应该 “想到就直接干”。尤其是一些高风险变更,比如重构、删除、迁移、批量替换,理论上更合理的流程应该是:
- teammate 先提交计划
- lead 查看计划内容
- lead approve 或 reject
- teammate 根据审批结果再决定是否正式执行
如果没有一套协议来支撑这个流程,那么 teammate 和 lead 之间就只能靠自由文本来 “猜测” 某条消息是不是一次正式审批,这显然不稳定。
更关键的是,这两类问题表面上不同,但本质结构其实是一样的。文档里把它们总结得非常精炼:
两者结构一样:一方发带唯一 ID 的请求,另一方引用同一 ID 响应。
也就是说,无论是 shutdown 还是 plan approval,它们都不是单条消息问题,而是一个 最小请求-响应协议问题。这里至少需要三样东西:
1. 一个唯一的 request_id
2. 一个可以关联 request_id 的响应
3. 一个清楚的状态机,描述它当前是 pending、approved 还是 rejected
所以 s10 这一节真正想解决的问题,可以概括成一句话:
团队协作里,有些消息不是 “聊天”,而是 “协议”;而协议的核心,不是文本内容本身,而是请求-响应关联关系与状态流转。
3. 解决方案
明确问题之后,s10 给出的解法非常漂亮,而且它延续了这个项目一贯的风格:不是去发明一套很重的分布式协调框架,而是在 s09 的文件邮箱通信基础上,进一步加上一层结构化协议和状态跟踪器。
它的核心思路可以概括为一句话:
同一个 request_id 关联请求与响应,同一个 FSM 表达 pending -> approved/rejected。
文档里给出的两个协议非常清楚:
Shutdown Protocol Plan Approval Protocol
================== ======================
Lead Teammate Teammate Lead
| | | |
|--shutdown_req-->| |--plan_req------>|
| {req_id:"abc"} | | {req_id:"xyz"} |
| | | |
|<--shutdown_resp-| |<--plan_resp-----|
| {req_id:"abc", | | {req_id:"xyz", |
| approve:true} | | approve:true} |
Shared FSM:
[pending] --approve--> [approved]
[pending] --reject---> [rejected]
也就是说,shutdown protocol 和 plan approval protocol 虽然应用场景不同,但底层模式完全一样:
- 请求一方生成一个
request_id - 把请求发出去,并把请求状态登记成
pending - 响应一方在回复时必须带回同一个
request_id - 系统根据 approve / reject 更新状态为
approved或rejected
为了支撑这个模式,代码里新增了两个 tracker:
shutdown_requests = {}
plan_requests = {}
_tracker_lock = threading.Lock()
也就是说,从 s10 开始,团队协作第一次不再只是 “消息被送到了某个 inbox”,而是系统侧还会专门维护两张请求状态表:
shutdown_requestsplan_requests
它们的职责不是存消息正文,而是存 请求的跟踪状态。文档里也给出了这种 tracker 结构:
shutdown_requests = {req_id: {target, status}}
plan_requests = {req_id: {from, plan, status}}
这一步变化非常重要,因为它说明 s10 真正新增的,不只是几种消息类型,而是:
系统开始在消息之外,显式维护协作过程的中间状态。
这就意味着,一次请求发出去之后,不再只是 “看看对方会不会回一条消息”,而是正式变成了一个可以被查询、被更新、被判断当前阶段的 FSM 实例。
更具体地说,s10 的解决方案有三层:
第一层,是 把某些通信动作升级成协议消息。
s09 的 message / broadcast 更多还是普通协作消息,而 s10 则正式引入:
shutdown_requestshutdown_responseplan_approval_response
这些消息类型本身就已经不是 “自由文本” 了,而是带有明确协议语义的结构化消息。
第二层,是 用 request_id 建立请求-响应关联。
这一步是整个协议层最核心的东西。没有 request_id,你就无法判断一条回复到底是在回应哪一次 shutdown 或哪一次 plan。加上它之后,系统就第一次具备了真正的 “相关性跟踪” 能力。
第三层,是 把协议过程显式地建模成一个共享 FSM。
无论是 shutdown 还是 plan approval,本质上都共享同一个最小状态机:
pending -> approved
pending -> rejected
这意味着 s10 并不是为每一类协作动作单独写死一套专用逻辑,而是第一次抽象出一种可复用的 “请求-响应 + FSM” 模式。文档里把这点总结得非常到位:
One FSM, two domains.
同样的
pending -> approved | rejected状态机,可以套用到任何请求-响应协议上。
这一步变化的意义其实非常大。因为从这里开始,团队协作不再只是 “大家能说话”,而开始变成 “大家按照统一规则完成某些正式协商”。也就是说,s10 真正让这个多 Agent 系统长出了一种更接近组织运行的能力:不是所有话都一样,有些话是协议,有些动作必须走握手。
4. FSM Team Protocols 流程图分析
这一节教程配了 4 张 FSM Team Protocols 的图。如果说 s09 的 Agent Team Mailboxes 图是在说明 “队友如何通过文件邮箱通信”,那么 s10 的这一组图,重点就在于说明:当通信变成协议时,消息的关键不再只是 “送到了没有”,而是 “请求和响应如何通过 request_id 被串成一个完整的 FSM 过程”。

先看第一张图。图里只有两条竖向生命线:
- Leader
- Teammate
上方有两个可切换标签:
Shutdown ProtocolPlan Approval Protocol
底部说明写的是:
Structured Protocols
Protocols define structured message exchanges with correlated request IDs.
这张图其实对应的是 s10 整节课最根本的出发点:团队里虽然已经能发消息了,但并不是所有消息都只是 “普通文本”;有些通信必须升级成一种 结构化交换。这里最重要的不是协议已经开始执行,而是先把 “协议” 这件事从普通 mailbox 聊天中区分出来了。协议的关键不是多发几条消息,而是 请求和响应要能被同一个 request_id 关联起来。

第二张图开始进入 Shutdown Protocol。此时 Leader 向 Teammate 发出一条蓝色箭头消息:
shutdown_request
request_id: req_abc
底部说明写的是:
Shutdown Request
The leader initiates shutdown. The request_id links the request to its response.
这一张图非常关键,因为它第一次把 “关机 / 退出” 从一种粗暴操作,变成了一次正式的协议发起。Leader 并不是直接把 teammate 杀掉,而是先发出一条带 request_id 的 shutdown_request。
这里最核心的不是请求内容本身,而是这个 request_id。因为一旦协议开始存在多个实例,比如同一时间系统里对不同 teammate 发了多个 shutdown 请求,那么没有 request_id 就根本没办法判断后面的响应到底是在回复谁。也就是说,request_id 是这次协商过程的身份标识。

第三张图里,Teammate 收到请求后,旁边出现了一个菱形决策节点:
?
approve
reject
底部说明写的是:
Teammate Decides
The teammate can accept or reject. It's not a forced kill -- it's a polite request.
这一步几乎把 shutdown protocol 的真正含义讲透了。因为它说明 s10 引入的并不是 “更复杂的 kill 线程方式”,而是 一次礼貌的协商。
也就是说,leader 发出 shutdown_request 之后,teammate 并不是必须无条件立即退出,它可以根据当前上下文自己决定:
- approve:我同意,我会收尾并退出
- reject:我拒绝,我现在还不能停
这和很多传统系统里的 “强制 kill” 完全不同。这里的设计更像一种组织内部的 polite request,而不是高优先级的强制中断。
从协议视角看,这一步正对应了 FSM 的分叉:
pending -> approved
pending -> rejected
也就是说,收到请求本身并不意味着协议完成,真正的关键在于 teammate 的决策把状态推进到了哪一个终点。

第四张图给出了一个 approve 的完整闭环。图中除了最初的 shutdown_request 外,又出现了一条绿色返回箭头:
shutdown_response { approve: true }
request_id: req_abc
同时 teammate 生命线底部有一个红色叉号 exit,底部说明写的是:
Approved
Same request_id in the response. Teammate exits cleanly.
这张图其实就是 s10 对 shutdown protocol 的总收束。它最关键地强调了两件事:
第一,响应里必须带回同一个 request_id。
这样 leader 才能明确知道:这不是一条普通消息,而是在回应刚才那一次具体的 shutdown_request。
第二,approve 并不只是 “嘴上同意”,而是会驱动 teammate 的生命周期真正进入 shutdown 并退出线程。
也就是说,协议不是停留在消息交换层面,而是会影响系统状态:成员状态变成 shutdown,线程停止运行。
这也正是为什么这一节叫 FSM Team Protocols。因为它不是在强调 “消息来回发了两次”,而是在强调:一次协议就是一条带 request_id 的请求-响应对,加上一次从 pending 到 approved/rejected 的状态转移。
虽然这组图展示的是 shutdown protocol,但文档和代码都明确说明,plan approval protocol 复用的是完全相同的相关性模式:只是这次请求方向变成 teammate 向 lead 提交计划,而 lead 决定 approve 还是 reject。换句话说,s10 真正引入的不是两个零散功能,而是一种统一的协议模板。
完整动画演示如下图所示:

5. 工作原理(代码分析)
看完流程图之后,接下来我们正式进入 agents/s10_team_protocols.py。和前面几节一样,文件顶部先给出了这一节的设计意图说明:
#!/usr/bin/env python3
# Harness: protocols -- structured handshakes between models.
"""
s10_team_protocols.py - Team Protocols
Shutdown protocol and plan approval protocol, both using the same
request_id correlation pattern. Builds on s09's team messaging.
...
Key insight: "Same request_id correlation pattern, two domains."
"""
这段注释其实已经把 s10 的主题说得非常清楚了。这里最重要的关键词不是 shutdown 或 plan 本身,而是:same request_id correlation pattern, two domains。
也就是说,这一节真正要教我们的,并不是两个孤立的小功能,而是一种统一的协议建模方式:不同领域的协作动作,都可以被压缩成同一种 “请求-响应 + request_id 关联 + FSM 状态流转” 的模式。
然后,最先出现的关键变化,就是两个 tracker:
shutdown_requests = {}
plan_requests = {}
_tracker_lock = threading.Lock()
这三行代码非常短,但作用极大。因为它们意味着从 s10 开始,系统第一次正式在消息收发之外,维护了两张 协议状态表。这里最关键的不是字典本身,而是它们承担的职责:
shutdown_requests:记录所有 shutdown 协议实例的目标和状态plan_requests:记录所有计划审批协议实例的提交者、计划内容和状态_tracker_lock:保证多线程读写这些共享跟踪表时的安全性
这一步其实非常重要,因为它说明:协议不是纯消息现象,而是系统状态现象。消息只是把请求和响应送来送去,而真正决定 “当前协议走到哪一步” 的,是这些 tracker 里的 status 字段。
这一节依然保留了 s09 的 MessageBus,而且实现几乎没有变:
class MessageBus:
def send(...)
def read_inbox(...)
def broadcast(...)
这说明 s10 并没有重新发明一套通信机制,而是继续把邮箱文件当作唯一消息通道。也就是说,协议层并不是取代 mailbox,而是 建立在 mailbox 之上。消息仍然照样写进 JSONL inbox 文件,只不过从这一节开始,某些消息的 type 和额外字段开始有了更强的协议语义。
接下来先看 TeammateManager。整体结构和 s09 一样,仍然有 config.json 管理团队名册,仍然有 spawn() 创建持久成员,仍然通过 _teammate_loop() 在独立线程里跑每个 teammate 的 agent loop。也就是说,s10 的协议能力并没有替换掉 s09 的团队骨架,而是把协议层接到了这套团队骨架上。
这一点从 spawn() 本身几乎没变就能看出来:
def spawn(self, name: str, role: str, prompt: str) -> str:
member = self._find_member(name)
if member:
if member["status"] not in ("idle", "shutdown"):
return f"Error: '{name}' is currently {member['status']}"
member["status"] = "working"
member["role"] = role
else:
member = {"name": name, "role": role, "status": "working"}
self.config["members"].append(member)
self._save_config()
thread = threading.Thread(
target=self._teammate_loop,
args=(name, role, prompt),
daemon=True,
)
self.threads[name] = thread
thread.start()
return f"Spawned '{name}' (role: {role})"
这说明 teammate 仍然是:
- 被命名的成员
- 被持久化进
config.json - 在独立线程里运行自己的 loop
- 生命周期仍然沿用
working / idle / shutdown
真正发生变化的地方,是 _teammate_loop() 的 system prompt 和协议工具支持。
先看它的 system prompt:
sys_prompt = (
f"You are '{name}', role: {role}, at {WORKDIR}. "
f"Submit plans via plan_approval before major work. "
f"Respond to shutdown_request with shutdown_response."
)
这一句其实特别重要,因为它说明从 s10 开始,协议并不只是 harness 层偷偷支持的功能,而是明确成为 teammate 行为规则的一部分。也就是说,系统会显式告诉队友:
- 做重大工作前先提交计划审批
- 收到 shutdown_request 时要用 shutdown_response 回应
换句话说,协议不只是系统能理解,模型本身也被要求主动遵守。
再看 _teammate_loop() 主体:
should_exit = False
for _ in range(50):
inbox = BUS.read_inbox(name)
for msg in inbox:
messages.append({"role": "user", "content": json.dumps(msg)})
if should_exit:
break
...
for block in response.content:
if block.type == "tool_use":
output = self._exec(name, block.name, block.input)
...
if block.name == "shutdown_response" and block.input.get("approve"):
should_exit = True
messages.append({"role": "user", "content": results})
member = self._find_member(name)
if member:
member["status"] = "shutdown" if should_exit else "idle"
self._save_config()
前面 s09 里,teammate loop 结束之后通常只会回到 idle;但从 s10 开始,loop 内部多了一个 should_exit 标志。它的含义非常直接:
- 如果某次工具调用中,队友执行了
shutdown_response - 并且
approve=True - 那么就把
should_exit设成True - 下一轮开始前直接 break
- 最终把成员状态写成
shutdown
也就是说,shutdown protocol 的 “批准” 并不是停留在一条消息上,而是会真的改变 agent 生命周期。
然后看 _exec(),这里是 s10 最核心的代码之一。前半部分的 bash / read_file / write_file / edit_file / send_message / read_inbox 这些基础工具都延续了 s02 / s09;真正的新东西,是协议工具的处理逻辑。
先看 shutdown_response:
if tool_name == "shutdown_response":
req_id = args["request_id"]
approve = args["approve"]
with _tracker_lock:
if req_id in shutdown_requests:
shutdown_requests[req_id]["status"] = "approved" if approve else "rejected"
BUS.send(
sender, "lead", args.get("reason", ""),
"shutdown_response", {"request_id": req_id, "approve": approve},
)
return f"Shutdown {'approved' if approve else 'rejected'}"
这段代码吧 shutdown 协议拆成了两个同时发生的动作。
第一,更新 tracker 状态。
如果请求 id 在 shutdown_requests 里存在,就把它从 pending 改成:
approved- 或
rejected
也就是说,FSM 的状态转移真正落在这里,而不是仅仅靠一条自然语言消息来推断。
第二,通过 MessageBus 给 lead 回一条正式协议响应消息。
这条消息的 msg_type 是 shutdown_response,并且 extra 字段里带回了:
request_idapprove
也就是说,协议响应并没有绕过邮箱系统,而是仍然通过 inbox 文件回流给 lead。只不过从这一节开始,这种回流的消息带有明确协议语义和 request_id 关联性。
再看 plan_approval:
if tool_name == "plan_approval":
plan_text = args.get("plan", "")
req_id = str(uuid.uuid4())[:8]
with _tracker_lock:
plan_requests[req_id] = {"from": sender, "plan": plan_text, "status": "pending"}
BUS.send(
sender, "lead", plan_text, "plan_approval_response",
{"request_id": req_id, "plan": plan_text},
)
return f"Plan submitted (request_id={req_id}). Waiting for lead approval."
这里 teammate 在调用 plan_approval 时,实际上做了三件事:
第一,生成一个新的 request_id;
第二,把这次计划提交登记进 plan_requests,状态初始化为 pending;
第三,把计划内容通过 inbox 发给 lead,并带上 request_id。
也就是说,plan approval 的本质并不是 “teammate 给 lead 发一段计划文本”,而是 “teammate 正式发起一次待审批协议实例”。从这一刻起,这条计划就已经不是普通消息,而是一个有唯一 request_id、可被 approve/reject、可被 tracker 跟踪状态的正式请求。
文档里也专门强调了这一点:计划审批遵循和 shutdown 完全相同的模式,队友提交计划时生成 request_id,领导审查时引用同一个 request_id。
理解了 teammate 侧之后,再看 lead 侧的专用处理器就很顺了。
先看 handle_shutdown_request():
def handle_shutdown_request(teammate: str) -> str:
req_id = str(uuid.uuid4())[:8]
with _tracker_lock:
shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
BUS.send(
"lead", teammate, "Please shut down gracefully.",
"shutdown_request", {"request_id": req_id},
)
return f"Shutdown request {req_id} sent to '{teammate}' (status: pending)"
这一段其实就是 shutdown protocol 的发起端。
它做了三件很明确的事:
1. 生成 request_id
2. 在 shutdown_requests 中登记一条 pending 状态记录
3. 通过 inbox 给目标 teammate 发出一条 shutdown_request
然后再看 _check_shutdown_status():
def _check_shutdown_status(request_id: str) -> str:
with _tracker_lock:
return json.dumps(shutdown_requests.get(request_id, {"error": "not found"}))
这段代码说明 shutdown protocol 不只是发了请求就算了,lead 还可以通过 request_id 回头检查协议当前状态。也就是说,tracker 真正让系统获得了一种新的能力:协议状态可查询。
再看 handle_plan_review():
def handle_plan_review(request_id: str, approve: bool, feedback: str = "") -> str:
with _tracker_lock:
req = plan_requests.get(request_id)
if not req:
return f"Error: Unknown plan request_id '{request_id}'"
with _tracker_lock:
req["status"] = "approved" if approve else "rejected"
BUS.send(
"lead", req["from"], feedback, "plan_approval_response",
{"request_id": request_id, "approve": approve, "feedback": feedback},
)
return f"Plan {req['status']} for '{req['from']}'"
这一段正好和 teammate 那边的 plan_approval 构成闭环。
在 teammate 提交计划时,tracker 里会记下:
- 谁提交的
- 计划内容是什么
- 当前状态是
pending
而到了 lead 这边,一旦进行 plan review,就会:
1. 根据 request_id 找到对应计划请求
2. 把状态改成 approved 或 rejected
3. 把审批结果通过 inbox 发回给原提交者
这就意味着,plan approval 也完整走完了:
pending -> approved / rejected
所以从代码角度看,shutdown protocol 和 plan approval protocol 虽然方向相反:
- shutdown 是 lead 发请求,teammate 响应
- plan approval 是 teammate 发请求,lead 响应
但它们共享的核心骨架完全一致:
- 生成
request_id - 登记 tracker
- 通过 inbox 发请求
- 用同一
request_id回响应 - 更新 tracker 状态
这正是 s10 最漂亮的抽象所在。
最后再来看工具层。lead 侧的 dispatch map 现在变成了 12 个工具:
TOOL_HANDLERS = {
"bash": ...
"read_file": ...
"write_file": ...
"edit_file": ...
"spawn_teammate": ...
"list_teammates": ...
"send_message": ...
"read_inbox": ...
"broadcast": ...
"shutdown_request": lambda **kw: handle_shutdown_request(kw["teammate"]),
"shutdown_response": lambda **kw: _check_shutdown_status(kw.get("request_id", "")),
"plan_approval": lambda **kw: handle_plan_review(kw["request_id"], kw["approve"], kw.get("feedback", "")),
}
和 s09 相比,真正新增的协议工具正是:
shutdown_requestshutdown_response(lead 这里是 “查询请求状态”)plan_approval(lead 这里是 “审批 teammate 提交的计划”)
最后再看最外层 agent_loop(),你会发现它和 s09 基本没有变化:
def agent_loop(messages: list):
while True:
inbox = BUS.read_inbox("lead")
if inbox:
messages.append({
"role": "user",
"content": f"<inbox>{json.dumps(inbox, indent=2)}</inbox>",
})
messages.append({
"role": "assistant",
"content": "Noted inbox messages.",
})
response = client.messages.create(...)
...
表面上看,s10 一下子引入了 request_id、FSM、shutdown protocol、plan approval protocol、tracker、审批与拒绝,好像系统复杂了很多;但真正落到 agent 核心骨架上,变化依然非常克制。
底层 while True 没变,tool_use / tool_result 协议没变,messages 依然是上下文主载体,inbox 依然是通信边界注入点。变化只是:
- inbox 里的某些消息现在有了更强的协议语义
- 系统多维护了两张请求状态表
- 某些工具现在不只是 “做事”,而是在驱动一条 FSM 走向 approved 或 rejected
所以从代码角度看,s10 真正的价值并不只是 “多了三个工具”,也不只是 “能优雅关机或审批计划了”,而是第一次让这个项目在协作层面长出了非常重要的一种工程能力:
团队通信不再只是发消息,而开始变成带 request_id、可追踪状态、可复用模式的结构化协议。
到了这一节,这个项目已经不再只是一个 “多 teammate 通过邮箱交流” 的协作系统了,它开始真正拥有一种更接近组织规则的协调观:有些行为不能只靠自由文本完成,而必须通过统一协议来达成。
博主在给定下面的提示词情况下:
Spawn alice as a coder. Then request her shutdown.
想通过调试看看整个过程发生了什么,我们来具体分析下:
Lead 第一次 loop(请求开始)

Lead loop1: 模型响应结果

Lead loop1: 工具执行

Lead loop1: 工具执行结果
第一轮里,lead 先完成的是整个 shutdown protocol 之前的准备动作,也就是把 Alice 这个 teammate 正式拉起来。
先看 Lead loop1: 模型响应结果。从图里可以看到,这一轮的 response.content 仍然是前面几节我们已经非常熟悉的 “文本 block + 工具 block” 结构:前面的 TextBlock 里,模型明确表示自己要先把 Alice 创建成 coder;后面的 ToolUseBlock 则真正调用了:
spawn_teammate(name="alice", role="coder", prompt=...)
再看 Lead loop1: 工具执行。调试图里停在了 TeammateManager.spawn() 内部,可以看到这里做了两件非常关键的事情。
第一件事,是把 Alice 正式写入 .team/config.json。右侧的配置文件里已经新增了一条成员记录:
{
"name": "alice",
"role": "coder",
"status": "working"
}
这说明从这一刻开始,Alice 已经不是一次性的临时调用,而是团队名册里的一个正式成员。它的存在不再只依附于当前这轮对话,而是已经落成了 .team/config.json 中的外部状态。
第二件事,是启动了一条新的 teammate 线程。也就是说,spawn_teammate 并不只是 “写配置”,而是配置层和执行层一起建立:既有名册里的成员记录,也有运行时里的独立 _teammate_loop()。
最后看 Lead loop1: 工具执行结果。这一轮 lead 收到的 tool_result 很简单:
Spawned 'alice' (role: coder)
这说明第一轮的产物不是某个业务结果,而是一个已经存在、已经开始运行的 teammate。也就是说,从 shutdown protocol 的整体视角看,这一步对应的是:先有对象,再谈协议。
TeamMate(coder) 第一次 loop

TeamMate(coder) loop1: 模型响应结果
这一轮是 Alice 自己刚被 spawn 出来之后,在独立线程里跑的第一次 teammate loop。
从图里可以看到,此时 Alice 的 messages 里其实只有一条最初始的 user prompt,也就是 lead 在 spawn_teammate 时传给它的角色说明。随后 Alice 自己调用了一次模型,返回的 response.content 里同样是两个 block:
- 一个
TextBlock,说明它理解了自己的 coder 身份,并准备先查看当前目录结构; - 一个
ToolUseBlock,调用的是 bash,命令是ls -la
Lead 第二次 loop

Lead loop2: 模型响应结果

Lead loop2: 工具执行

Lead loop2: 工具执行结果
第二次 loop 才真正进入 shutdown protocol 本身。
先看 Lead loop2: 模型响应结果。这一轮仍然是典型的 “文本说明 + 工具调用” 结构。文本 block 里,lead 明确表示:
Now I'll request Alice's shutdown:
紧接着,工具 block 调用的是:
shutdown_request(teammate="alice")
也就是说,lead 这一次并不是再发普通消息,而是第一次正式发起一个 协议型动作。
再看 Lead loop2: 工具执行。调试图里停在了 handle_shutdown_request() 内部,这一段代码正是 s10 shutdown protocol 的起点:
req_id = str(uuid.uuid4())[:8]
shutdown_requests[req_id] = {"target": teammate, "status": "pending"}
BUS.send(
"lead", teammate, "Please shut down gracefully.",
"shutdown_request", {"request_id": req_id},
)
从图里可以非常清楚地看到,这里发生了两件事。
第一,系统生成了一个新的 request_id,比如图里的:
d08982e3
然后在 shutdown_requests 这张 tracker 表里新增了一条记录:
{
"d08982e3": {
"target": "alice",
"status": "pending"
}
}
这一步非常关键,因为它说明 shutdown protocol 从发起那一刻起,就已经不再只是 “发出一条消息”,而是正式变成了一个 可跟踪的协议实例。状态此时明确是 pending,也就是 “请求已经发出,但还没有收到响应”。
第二,MessageBus 把一条结构化消息写进了 alice.jsonl:
{
"type": "shutdown_request",
"from": "lead",
"content": "Please shut down gracefully.",
"timestamp": ...,
"request_id": "d08982e3"
}
这里最关键的就是 type="shutdown_request" 和 request_id="d08982e3"。
前者说明这已经不是普通团队消息,而是一条协议消息;后者说明后续 Alice 的响应必须引用同一个 ID,系统才能知道这是同一次 shutdown 握手。
最后看 Lead loop2: 工具执行结果。这一轮返回给 lead 的 tool_result 是:
Shutdown request d08982e3 sent to 'alice' (status: pending)
这里的返回值已经把协议状态显式说出来了:pending。也就是说,lead 发出 shutdown_request 之后,事情并没有结束,它只是进入了 FSM 的第一阶段:pending,至于后面会走向 approved 还是 rejected,要等 Alice 自己的 teammate loop 来决定。
Lead 第三次 loop(完成回答)

Lead loop3: 模型响应结果
第三次 loop 是 lead 这条主线程视角下的收束。
从图里可以看到,这一次 response.content[0] 只剩下了一个纯 TextBlock,内容大意是:
I've successfully spawned Alice as a coder and sent a shutdown request to her.
The shutdown request has been sent with request ID `d08982e3` and is currently pending.
Alice will need to respond to the shutdown request before the process is complete.
也就是说,lead 在这一轮没有继续请求任何工具,而是基于前面两轮已经获得的结果,对当前系统状态做了一次阶段性总结。由于这一次 response.stop_reason != "tool_use",所以 lead 自己的主 loop 会在这里结束。
lead 的 loop 提前结束,并不意味着整个协议结束。lead 可以先把 shutdown request 发出去,然后自己的这一轮对话先结束;但协议本身仍然活在系统里,因为:
shutdown_requests里还保留着pendingalice.jsonl里还放着一条待消费的shutdown_request- Alice 的 teammate loop 还在另一个线程里继续工作
TeamMate(coder) 第二次 loop

TeamMate(coder) loop2: 请求消息

TeamMate(coder) loop2: 模型响应结果

TeamMate(coder) loop2: 工具执行结果

TeamMate(coder) loop2: 状态变更
Alice 的第二次 loop,才真正把 shutdown protocol 这条握手链路完整闭合起来。
先看 TeamMate(coder) loop2: 请求消息。这张图非常关键,因为它把 s10 协议消息的注入过程展示得非常清楚。此时 Alice 在新一轮 _teammate_loop() 开始时执行了:
inbox = BUS.read_inbox(name)
for msg in inbox:
messages.append({"role": "user", "content": json.dumps(msg)})
所以它的 messages 里新增了一条 user 消息,内容正是从 alice.jsonl 里读出来的那条结构化协议消息:
{
"type": "shutdown_request",
"from": "lead",
"content": "Please shut down gracefully.",
"timestamp": ...,
"request_id": "d08982e3"
}
再看 TeamMate(coder) loop2: 模型响应结果。从图里可以看到,这一次 Alice 的 response.content[0] 已经不再是文本 block,而是 直接只有一个工具 block:
shutdown_response(
request_id="d08982e3",
approve=True,
reason="Shutting down gracefully as requested"
)
这说明 Alice 这一次没有把 shutdown_request 当成普通消息来随便回复一段自然语言,而是严格遵循了 system prompt 和工具协议,直接调用了 shutdown_response 这个协议工具,并且明确带上了:
- 同一个
request_id approve=Truereason=...
这其实就是前面 FSM 图第四张图的代码化体现:同一个 request_id 被带回响应,状态从 pending 准备走向 approved。
接着看 TeamMate(coder) loop2: 工具执行结果。图里可以看到,这一轮 results 中返回的就是:
Shutdown approved
与此同时,下面还可以直接看到 .team/inbox/lead.jsonl 文件已经新生成,并且里面写入了一条新的结构化响应消息:
{
"type": "shutdown_response",
"from": "alice",
"content": "Shutting down gracefully as requested",
"timestamp": ...,
"request_id": "d08982e3",
"approve": true
}
也就是说,Alice 执行 shutdown_response 工具之后,发生了两件事情:
第一,shutdown_requests["d08982e3"]["status"] 被从 pending 更新为 approved;
第二,Alice 又通过 MessageBus 给 lead 的 inbox 回写了一条 shutdown_response 消息。
这一步把协议真正完整闭环了:
- 请求端:lead 发
shutdown_request - 响应端:alice 发
shutdown_response - 相关性:同一个
request_id - 状态:从
pending变成approved
最后看 TeamMate(coder) loop2: 状态变更。调试停在 _teammate_loop() 末尾:
if member:
member["status"] = "shutdown" if should_exit else "idle"
self._save_config()
而此时右侧的 config.json 里,Alice 的状态已经从原来的 working 变成了:
"status": "shutdown"
同时图里还能看到,前面在 loop 内部由于:
if block.name == "shutdown_response" and block.input.get("approve"):
should_exit = True
所以 should_exit 已经被置成了 True。也就是说,这一次 Alice 并不是像 s09 那样正常干完活回到 idle,而是因为协议批准关闭,所以生命周期直接走向了 shutdown。
这一点其实正是 s10 最核心的价值所在:
协议不只是交换几条结构化消息,而是会真正驱动 teammate 的系统状态发生转移。
在 s09 里,teammate 的自然收束通常是:
working -> idle
而到了 s10 的 shutdown protocol 中,这条状态机会第一次出现另一条正式路径:
working -> shutdown
这次调试其实非常完整地把 s10 的 shutdown protocol 展示出来了。它最值得我们关注的,不只是 “lead 成功让 Alice 退出了”,而是整个过程中几个非常关键的工程特性都被明确验证了:
第一,shutdown 并不是粗暴 kill 线程,而是一个正式的请求-响应握手;
第二,协议的身份不靠自然语言约定,而是靠唯一的 request_id 来关联请求与响应;
第三,系统不仅发送协议消息,还会在 shutdown_requests 这张 tracker 表里显式维护其状态;
第四,teammate 收到 shutdown_request 后,不是普通聊天式回复,而是要调用 shutdown_response 这个专用协议工具;
第五,协议的最终结果并不只是 “一条消息回来了”,而是会真正驱动 teammate 的生命周期从 working 进入 shutdown。
关于 plan approval protocol 的验证大家可以自行尝试,这里博主就不演示了。
OK,以上就是 Team Protocols 工作原理的完整分析了。
大家也可以试试文档里给出的这些 prompt,感受一下协议层引入之后,这个 Agent 团队在协作方式上会有什么变化:
1. Spawn alice as a coder. Then request her shutdown.
2. List teammates to see alice's status after shutdown approval
3. Spawn bob with a risky refactoring task. Review and reject his plan.
4. Spawn charlie, have him submit a plan, then approve it.
5. 输入 /team 监控状态
6. 相对 s09 的变更
| 组件 | 之前 (s09) | 之后 (s10) |
|---|---|---|
| Tools | 9 | 12 (+shutdown_req/resp +plan) |
| 关机 | 仅自然退出 | 请求-响应握手 |
| 计划门控 | 无 | 提交/审查与审批 |
| 关联 | 无 | 每个请求一个 request_id |
| FSM | 无 | pending -> approved/rejected |
7. 小结
如果说 s09 最大的贡献,是让多 Agent 协作第一次从 “一次性 subagent 调用” 升级成了 “持久命名 teammate + 文件邮箱通信” 的团队结构;那么 s10 的贡献就是让我们进一步意识到:团队里有些动作已经不能只靠自由文本消息来完成,而必须通过统一协议来协商。
前面 s09 的 mailbox 机制已经让 lead 和 teammate 能够彼此通信,但这种通信更多还是 “消息投递”;到了 s10,系统第一次正式引入了 shutdown protocol 和 plan approval protocol,并通过 request_id 相关性模式、shutdown_requests / plan_requests 跟踪表,以及 pending -> approved | rejected 这套最小 FSM,把团队协作中的某些关键动作提升成了结构化握手。
所以,s10 真正的价值并不只是 “让队友能优雅退出”,也不只是 “多了个计划审批工具”,而是第一次把这个项目从一个 会通信的多 Agent 团队,推进成了一个开始具备 统一协作规矩 的团队。从这一节开始,Agent 之间的某些互动不再只是 “发一条消息看看对方怎么想”,而是要通过 request-response、相关 ID、状态跟踪、协议终态这些更成熟的工程方式来完成。也就是说,系统开始不只是有 “队友”,而是开始有 “组织规则”。
这一步其实非常关键,因为它为后面的自治认领、工作流控制、任务隔离等机制继续往上搭建,提供了一个真正可靠的协作基础:团队不仅要能说话,更要知道什么时候该按规矩说话。
OK,以上就是本期想要分享的全部内容了。
结语
本篇文章我们围绕 s10 Team Protocols 这一节,从问题出发,结合流程图与代码实现,完整梳理了 Agent 团队在引入协议之后,是如何将协作从 “消息交换” 升级为 “结构化协调” 的。
相比前一节的 Agent Teams,这一节最本质的变化不在于增加了新的工具,而在于引入了一种更高层的约束:团队中的某些关键行为,不再依赖自然语言理解,而必须通过统一协议来完成。无论是 shutdown 还是 plan approval,本质上都遵循同一套 request-response 模式,通过 request_id 建立关联,并在系统中以 FSM 的形式显式维护其状态流转。
从工程视角来看,这一步的关键在于:协作过程第一次被建模为 “可追踪的状态机”,而不仅仅是 “来回传递的消息”。消息负责传递信息,但真正决定系统行为的是 tracker 中的状态,以及它从 pending 走向 approved 或 rejected 的过程。这使得团队协作从 “是否收到回复” 升级为 “当前处于哪一个明确阶段”。
进一步来看,s10 实际上完成了一次非常重要的抽象升级:前面的章节已经让系统具备了执行能力、上下文管理能力以及多 Agent 协作能力,而到了这里,系统开始具备规则化协作能力。Agent 不再只是 “会沟通”,而是开始 “按规则沟通”,这也是从简单协作走向可靠系统的关键一步。
也正因为如此,Team Protocols 的意义并不只是实现了优雅关机或计划审批,而是让整个 Agent 团队第一次拥有了一种接近真实组织的运行方式:不同成员之间的某些互动必须经过明确的握手、关联与状态确认,而不是依赖隐式理解。
下篇文章我们将来学习 s11 Autonomous Agents 章节的内容,敬请期待🤗
参考
更多推荐



所有评论(0)