AGAgent 学习路线
第 16 / 20
CHAPTER 16 · GPT 生成学习页

协作协议与计划门控

用稳定 requestId、严格响应匹配和硬门控处理审批、关机与重试。

01 / 路线

先看它怎样跑起来

从输入到验收

这一章不是几个孤立知识点,而是一条会产生结果的因果链。

关键判断

协作协议与计划门控

亮起的是当前动作,留下的是已经满足的前置条件。点击任意一步,可以从那里继续。

  • 自由文本不能可靠表达“正在解决哪个请求”。
  • shutdown 要等待当前消息完成再关闭。
  • plan_gate 必须在 handler 前拒绝未批准的写入。
1请求登记 requestId
2pending / approved / rejected
3响应严格匹配
4状态只迁移一次
点击播放,观察动作怎样传递0 / 4
02 / 正文

顺着原文把边界看清

21 个小节16 组代码14 行表格

按原文顺序阅读。摘要只负责定位,真正的边界、例外和代码都在展开内容里。

01导读:问题背景与本章目标队友能干活之后,下一个暴露出来的问题往往不是能力,而是协调。

队友能干活之后,下一个暴露出来的问题往往不是能力,而是协调。

前一个版本里,Lead 可以创建持续队友、给他们发消息、接收结果。但普通 task/message/result 只表达内容,不表达“这条回复正在解决哪个请求”。顺利路径下这没有问题,一旦涉及审批、并发和重试,靠自由文本猜上下文就不够了。

两个场景最能说明问题。

场景一是关机。Lead 想让 Alice 停下来,但不能在她写文件时直接取消。正确顺序是:先让当前消息完成,再处理结构化 shutdown 请求,发回响应,然后结束 worker;Runner 资源仍由组合根统一关闭。

场景二是计划审批。Bob 要重构认证模块,他先提交计划,Lead 决定批准或驳回。只要计划仍是 pending 或 rejected,写入和执行类工具就必须在 handler 之前被拒绝,不能只靠 Prompt 提醒模型“先等批准”。

这两个场景的共同结构是:请求先登记,消息携带稳定 ID,响应严格匹配请求,状态只迁移一次。

第 16 章在第 15 章的持续 Teammate 和持久 Mailbox 上,只增加 protocolplan_gate 两项能力。

图片
图片

02问题的本质:消息送达不等于协议完成普通 Mailbox 已经解决了持久投递问题:每条消息拥有稳定 UUID,并经过 ready -> processing -> done/quarantine。但它不知道下面这些事实:

普通 Mailbox 已经解决了持久投递问题:每条消息拥有稳定 UUID,并经过 ready -> processing -> done/quarantine。但它不知道下面这些事实:

  • shutdown_response 是否对应某个真实存在的 shutdown 请求;
  • 响应双方是否正好是原请求的 target 和 sender;
  • 请求是否仍为 pending,是否已经过期;
  • 同一个响应因 Mailbox ack 故障重试时,是否已经消费过;
  • 一个不同 transport message 携带相同 request ID 时,是否属于重复响应。

因此 P16 没有把协议塞进普通 content 字符串,也没有让模型解析一段自由文本 JSON。它增加了一层确定性运行时:

Lead / Teammate 协议工具
    |
    v
ProtocolRuntime
  先登记 ProtocolRequest,再发送 typed protocol message
    |
    +----> JsonProtocolStore
    |      .agent_tutorial/protocol/state.json
    |
    +----> FileMailboxStore
           ready -> processing -> done/quarantine
    |
    v
TeammateRuntime
  协议消息先走确定性 router,普通消息才进入模型

相关实现集中在以下文件:

  • [协议领域模型、工具与路由](code/chapters/ch16/src/features/protocol.ts)
  • [协议单快照 store](code/chapters/ch16/src/adapters/protocol-json.ts)
  • [普通与协议 Mailbox 模型](code/chapters/ch16/src/features/mailbox.ts)
  • [联合消息文件 adapter](code/chapters/ch16/src/adapters/mailbox-json.ts)
  • [持续队友协议 host](code/chapters/ch16/src/features/teammates.ts)
  • [权限组合](code/chapters/ch16/src/core/permissions.ts)
  • [章节组合根](code/chapters/ch16/src/bootstrap.ts)
  • [CLI 组合入口](code/chapters/ch16/src/cli.ts)

协议没有创建第二套消息目录、第二个 EventInbox 或第二个 Agent Loop。


03公开工具:Lead 两个,队友一个| Lead | requestshutdown | teammate | 先登记 pending shutdown request,再发送 typed request |

P16 的公开差量只有三项:

角色工具输入可观察结果
Leadrequest_shutdownteammate先登记 pending shutdown request,再发送 typed request
Leadreview_planrequest_idapprove、可选 feedback向原计划提交者发送 typed response
Teammatesubmit_planplan先登记 pending plan request,再发送给 Lead

Lead 的 P15 工具序列保持完整前缀,只在末尾追加:

request_shutdown, review_plan

队友工具严格为:

shell, read_file, write_file, send_message, submit_plan

队友不会获得 spawn_teammaterequest_shutdownreview_plan。Lead 也不会获得 submit_plan

原文差量表曾单独列出 request_plan,但正文没有定义它的输入、状态迁移或响应方。当前实现不增加这个孤立工具,也不保留兼容别名。计划请求由队友主动调用 submit_plan 发起。


04ProtocolRequest:持久请求才是状态真相请求不是一个进程内全局字典。领域对象使用规范 UUID、明确参与方、aware UTC 和封闭状态:

请求不是一个进程内全局字典。领域对象使用规范 UUID、明确参与方、aware UTC 和封闭状态:

interface ProtocolRequest {
  readonly id: string;
  readonly kind: "shutdown" | "plan_approval";
  readonly sender: string;
  readonly target: string;
  readonly status: "pending" | "approved" | "rejected";
  readonly content: string;
  readonly createdAtUtc: Date;
  readonly expiresAtUtc: Date;
  readonly resolution: ProtocolResolution | null;
}

关键不变量如下:

  • sender 和 target 不能相同,名字必须是安全的小写 Agent slug;
  • ID 必须是规范 UUID,不接受六位随机短 ID、大小写变体或别名;
  • pending 请求不能带 resolution;approved/rejected 请求必须带匹配的 resolution;
  • resolution 时间不能早于请求创建,也不能落在或超过过期时刻;
  • 默认响应窗口是 5 分钟,区间为 [created_at_utc, expires_at_utc)

JsonProtocolStore 把全部请求保存到:

.agent_tutorial/protocol/state.json

这是一个带 version 的单一快照。创建、查询、响应校验和终态替换都由同一个跨进程 proper-lockfile 锁串行化;写入使用临时文件、fsync 和原子 rename。持久化失败时,旧字节和旧状态保持不变。

同一进程内,JsonProtocolStore 还会先通过进程内 mutex 串行化操作。它解决的是多个 store 实例在同一 Agent 进程内同时访问 state.json 的问题;跨进程竞争才由 proper-lockfile.protocol.lock 接管。两个锁解决的问题不同,因此不能互相替代。

快照数组保留锁内创建顺序。两个请求可以拥有相同 UTC 时间,但后追加的计划仍是“最新计划”;随机 UUID 不参与授权先后判断。若注入时钟真的倒退,新请求会明确失败,不会偷偷重排状态。


05第一条硬规则:先登记,再发送无论 shutdown 还是 plan approval,请求都先写入 ProtocolStore,再通过 Mailbox 发送:

无论 shutdown 还是 plan approval,请求都先写入 ProtocolStore,再通过 Mailbox 发送:

const request = await protocol.requestShutdown("alice");
// request 已先写入 JsonProtocolStore,再由 TeammateRuntime 投递 typed Mailbox

顺序不能反。如果先发送,接收方可能在请求登记前就回复,状态机将找不到 request ID。

发送失败也不能假装请求没有发生。当前实现抛出 ProtocolDeliveryError,同时保留已经持久化的 pending 请求,供诊断和恢复使用。P16 不自动创建第二个请求来掩盖失败,也不在后台无限重试。


06typed Mailbox:不把协议降级成 content JSONP15 的 MailboxMessageKind 仍然只有:

P15 的 MailboxMessageKind 仍然只有:

task, message, result

P16 新增独立的 ProtocolMessageKind

shutdown_request
shutdown_response
plan_approval_request
plan_approval_response

ProtocolMailboxMessage 除共享的 transport 字段外,还显式包含 request_idapproved

字段约束
idtransport 自己的规范 UUID,也是 event ID 和 idempotency key
sender / recipient安全 Agent 名,双方不能相同
kind必须来自独立 ProtocolMessageKind
request_id对应持久 ProtocolRequest 的规范 UUID
content非空请求正文或反馈
approvedrequest 必须为 null,response 必须为明确布尔值
created_at_utcaware UTC

普通消息 schema 没有增加可选协议字段。文件 adapter 使用严格联合类型解析两种 envelope,协议消息继续复用原来的 ready/processing/done/quarantine 原子迁移。

这使 router 可以在模型调用之前用类型分流,而不是依赖 content.startswith(...)endswith("_response") 或自由文本约定。


07第二条硬规则:响应必须完整匹配2. response kind 与 request kind 配对;

一条响应只有同时满足以下条件,才能改变请求状态:

  1. request_id 指向真实请求;
  2. response kind 与 request kind 配对;
  3. response sender 等于原 request target;
  4. response recipient 等于原 request sender;
  5. request 仍为 pending;
  6. 当前时间严格早于 expires_at_utc

错 ID、错类型、错参与方、过期响应和不同 transport 的重复响应都会明确失败,原状态不变。

这里还要区分两种“重复”:

  • 同一个 protocol message ID 因 Mailbox ack 失败而重试:幂等返回已经完成的结果;
  • 另一个 message ID 携带相同 request ID:拒绝为重复响应。

不同线程或不同 store 实例并发消费两个响应时,proper-lockfile 锁内只有一个能完成 pending -> approved/rejected 迁移。另一个重新读取快照后会看到 resolved 状态并失败。


08为什么 Lead 必须在 history 之后消费响应Lead 收到协议消息时,TeammateRuntime 分成两个阶段处理。

Lead 收到协议消息时,TeammateRuntime 分成两个阶段处理。

drain 阶段只做只读验证,然后把 typed event 交给公共 Loop。Loop 先把 event 写入 canonical history。只有进入 acknowledge_events() 后,shutdown response 才会原子消费 ProtocolRequest,并把 Mailbox 文件从 processing 移到 done。

claim protocol response
  -> validate only
  -> append typed event to Lead canonical history
  -> consume ProtocolRequest once
  -> ack Mailbox processing -> done

如果 ProtocolRequest 已消费,但 Mailbox ack 暂时失败,同一个 event 会重新进入 EventInbox。Runner 用稳定 event ID 避免重复 history;ProtocolStore 用相同 response message ID 幂等返回;下一次 ack 成功后才结束。

这仍是“至少一次 transport + 稳定 ID 幂等”,不是把任意外部副作用神奇地变成全局恰好一次。


09shutdown:完成当前消息,再走确定性路由shutdown request 进入队友的同一个持久 Mailbox。worker 每次只处理一个 processing 消息,因此正在执行的普通任务先完成,shutdown request 随后才会被 claim。

shutdown request 进入队友的同一个持久 Mailbox。worker 每次只处理一个 processing 消息,因此正在执行的普通任务先完成,shutdown request 随后才会被 claim。

协议分流发生在模型调用之前:

当前 task/message 完成并 ack
  -> claim shutdown_request
  -> 校验 request ID、类型和双方
  -> Teammate status = shutdown
  -> 发送 shutdown_response(approved=true)
  -> ack shutdown_request
  -> 不调用模型

shutdown 状态表示该 worker 不再接收新消息;队友 Runner 并没有被协议 handler 直接销毁。最终资源仍由 AgentRunner.close() 按 Teammate -> Cron -> JobSupervisor 的依赖逆序关闭。

如果 close 正好取消了“已 claim 请求、正在发送响应”的 worker,当前 request 会 release 回 ready。如果协议状态存储或响应投递失败,有效 transport 也会 release,并让 worker 进入可观察的 failed 状态;只有错 ID、错类型、错参与方、重复或过期这类消息本身无效的情况才进入 quarantine。


10计划审批:状态先变,Runner 后恢复-> create pending PLANAPPROVAL request

计划流程从队友发起:

Teammate submit_plan(plan)
  -> create pending PLAN_APPROVAL request
  -> send plan_approval_request to lead

Lead event turn
  -> typed request enters history, then Mailbox ack
  -> review_plan(request_id, approve, feedback)
  -> send plan_approval_response to original sender

Teammate worker
  -> claim typed response
  -> consume response in ProtocolStore
  -> approved/rejected state becomes visible to permission gate
  -> reuse the same Runner with a deterministic approval/rejection prompt

顺序很关键。队友收到 response 后先完成状态迁移,再调用同一个 Runner。模型即使立刻请求写工具,权限层看到的也已经是 approved 或 rejected 状态。

review_plan 只接受仍为 pending 的 plan request。它不能拿 shutdown request ID 做计划审批,也不能把响应发给与原请求无关的队友。


11plangate:Prompt 不能授予执行权限P16 没有使用“请等待批准”作为唯一防线。计划 gate 是一条结构化 PermissionRule,通过公开组合入口追加到队友已有策略:

P16 没有使用“请等待批准”作为唯一防线。计划 gate 是一条结构化 PermissionRule,通过公开组合入口追加到队友已有策略:

const teammatePolicy = basePolicy.withRules([protocol.planGateRule]);

withRules() 保留原策略的 ApprovalProvider 和 AuditSink。最终顺序仍是:参数验证 -> PreToolUse -> PermissionPolicy -> dispatcher/handler -> PostToolUse。plan gate 的 deny 与工作区 deny 一样发生在 handler 和后台提交之前。

门控规则如下:

  • 没有提交过计划时,不凭空强制所有任务走审批;
  • 最新计划为 pending 或 rejected 时,写入、执行和其他 effectful 工具被拒绝;
  • read 工具保持可用,队友仍能收集制定计划所需的信息;
  • send_messagesubmit_plan 保持可用,否则队友无法沟通或重新提交;
  • 最新计划 approved 后,允许按原权限策略执行多步计划;
  • 已批准计划之后提交的新计划会成为最新状态,并重新关闭 effectful 工具。

批准不跳过原有权限。比如 write_file 原本还需要终端 ASK,那么计划 approved 后仍需人工明确批准,并照常写入 audit。plan gate 只增加限制,不会放宽系统边界。


12idle 不等于轮询原文示例使用 while 加 time.sleep(1) 轮询 inbox。这会长期占用线程,关闭时难以管理,还把 P17 的自驱 polling 提前塞进 P16。

原文示例使用 whiletime.sleep(1) 轮询 inbox。这会长期占用线程,关闭时难以管理,还把 P17 的自驱 polling 提前塞进 P16。

当前 Teammate 在完成一批消息后进入 idle,worker task 随即结束,但独立 Runner 和 history 保留。新的普通或协议消息持久写入后,TeammateRuntime 才通过共享 JobSupervisor 启动受管 worker。没有消息时没有 daemon、没有空转 task,也没有真实 sleep。

Lead 的普通 Mailbox 事件仍沿用 P15 的显式 run_events() 边界;P16 不新增空闲 Lead 自动找活。Cron 保留 P14 已有的 wakeup,不应与 Mailbox 协议混为一谈。


13组合根:一个 Mailbox,两种持久状态CLI 只构造一个 FileMailboxStore,TeammateRuntime 和 ProtocolRuntime 必须共享同一个对象:

CLI 只构造一个 FileMailboxStore,TeammateRuntime 和 ProtocolRuntime 必须共享同一个对象:

const mailbox = new FileMailboxStore(workspace);
const teammates = new TeammateRuntime({
  store: mailbox,
  inbox: eventInbox,
  supervisor,
  cronRuntime,
});
const protocol = new ProtocolRuntime({
  store: new JsonProtocolStore(workspace),
  team: teammates,
});

BuildDependencies 在 P16 显式要求 protocolRuntime。P15 传入未使用的 protocol dependency 会失败,P16 缺少它也会失败。Bootstrap 还验证:

  • ProtocolRuntime 指向传入的同一个 TeammateRuntime;
  • 两者共享同一个 MailboxStore;
  • Teammate、Cron 和 Background 继续共享同一个 EventInbox 与 JobSupervisor。

构建通过后,Bootstrap 才为 Lead 注册两个协议工具,为队友 factory 追加 submit_plan,并把 plan gate 组合进队友权限策略。


14两个运行入口Set-Location 'F:\笔记\Agent实操\code'

固定章节入口:

Set-Location 'F:\笔记\Agent实操\code'
npm run ch16 -- --prompt "创建 writer 队友,让她先提交修改 README 的计划,批准后执行并优雅关闭"

通用入口:

Set-Location 'F:\笔记\Agent实操\code'
npm run agent-tutorial -- run --chapter 16 --prompt "创建 writer 队友,让她先提交修改 README 的计划,批准后执行并优雅关闭"

普通与协议 transport 写入 .agent_tutorial/mailboxes/;请求状态写入 .agent_tutorial/protocol/state.json


15与 Claude Code 的差异参照 learn-claude-code/s16teamprotocols/README.md 中深入 CC 源码的分析,P16 的 Protocol/Plan Gate 在教学简化上与 Claude Code 的真实团队协议存在以下差异:

参照 learn-claude-code/s16_team_protocols/README.md 中深入 CC 源码的分析,P16 的 Protocol/Plan Gate 在教学简化上与 Claude Code 的真实团队协议存在以下差异:

关机响应。 P16 用统一的 shutdown_response 携带 approved: true 完成握手,由 ProtocolStore 原子消费一次。CC 真实源码把关机拆成 shutdown_approvedshutdown_rejected 两种独立消息;确认后还会发送 teammate_terminated 通知相关方,并清理 tmux/iTerm2 窗格、解除任务认领、从团队配置中移除成员。P16 的 shutdown 只结束队友消息循环,Runner 资源仍由组合根统一关闭。

计划审批来源。 P16 由队友显式调用 submit_plan 发起审批,Lead 用 review_plan 返回批准/拒绝。CC 真实源码中,计划审批主要由 ExitPlanModeV2Tool 在 plan-mode-required 队友退出计划模式时产生;useInboxPoller 会自动回写 approval 并把请求交给 Lead 作为普通上下文,同时保留显式 approve/reject 能力。CC 审批还可以附带 permissionMode,例如“批准但以 plan mode 运行”;P16 只实现 approve/reject/feedback 三要素。

工具命名。 P16 明确拒绝原文曾单独列出的 request_plan 孤立别名,Lead 只暴露 request_shutdownreview_plan,队友只追加 submit_plan;CC 教学版 s16 的 Lead 工具集则同时出现 request_shutdownrequest_planreview_plan。这不是功能缺失,而是刻意避免让教学实现维护一个没有状态迁移、没有响应方的伪协议。

消息 schema。 P16 的 ProtocolMailboxMessage 是独立 typed envelope,字段固定为 kindrequest_idapproved,并复用 Mailbox 的稳定 message ID。CC 真实源码的协议消息同样是结构化 JSON,并通过 Zod schema 校验;但字段命名并不统一,permission 消息使用 request_id,shutdown 与 plan approval 使用 requestId。P16 用统一命名换可读性,CC 则保留了不同模块的真实演进痕迹。

执行门控。 P16 把 planGateRule 实现为 PermissionRule,pending/rejected 时 effectful 工具在 handler 和后台提交之前被 deny,属于真实硬门控。CC 教学版 s16 只演示 plan approval 消息流程,未在未批准时拦截 bash/write_file;CC 真实源码的队友有完整 permission gating。这也是 P16 向生产语义靠拢的部分。

持久化模型。 P16 用 JsonProtocolStore 把请求写入 .agent_tutorial/protocol/state.json,请求 ID 是规范 UUID,时间使用 aware UTC,并通过 .protocol.lock 保护跨进程并发。CC 教学版 s16 的 ProtocolState 存在进程内 pending_requests 字典,使用 req_xxx 短 ID 和 float 时间戳,重启即丢失。真实 CC 源码的消息是结构化 JSON,但教学版没有持久化状态真相。

投递与幂等。 P16 遵循先登记后发送,发送失败保留 pending。Mailbox 至少一次投递;ProtocolStore 用响应 message ID 保证同 transport 重试幂等,但不承诺全局 exactly-once。CC 教学版沿用 read + unlink 消费,进程在读取后处理前崩溃会丢消息,是至多一次语义。P16 的“响应严格消费一次”针对单个持久请求,不解决外部副作用去重。

idle 生命周期。 P16 的队友完成本轮后进入 idle 且 worker task 结束,新消息通过共享 JobSupervisor 启动受管 worker,没有 daemon 或 sleep。CC 教学版 s16 的队友在 LLM 返回非 tool_use 后每秒轮询 inbox,等待 shutdown_request 或新消息;CC 真实源码在 idle 时发送 idle_notification,让 Lead 能感知队友空闲。P16 的事件驱动模型不是对 CC 轮询的补丁,而是换掉了等待机制。


16从 ai-agent-book 学到什么:控制平面与优雅终止ai-agent-book 第 10 章正文与本章主题最贴近:它把不共享上下文的多 Agent 协作拆成数据平面与控制平面,用结构化信封、状态获取、优雅终止和任务生命周期解决“消息已经送达,但协作尚未完成”的问题。配套实验 10-4 的说明见 README,总线实现见 messagebus.py。

ai-agent-book 第 10 章正文与本章主题最贴近:它把不共享上下文的多 Agent 协作拆成数据平面与控制平面,用结构化信封、状态获取、优雅终止和任务生命周期解决“消息已经送达,但协作尚未完成”的问题。配套实验 10-4 的说明见 [README](ai-agent-book/chapter10/parallel-web-research/README.md),总线实现见 [message_bus.py](ai-agent-book/chapter10/parallel-web-research/message_bus.py)。

17ai-agent-book 的实验设计ai-agent-book 第 10 章指出,不共享上下文的多 Agent 系统需要两套基础设施:共享文件系统承载产物交换,形成数据平面;消息传递、状态查询、执行终止与资源调度组成控制平面。它特别强调,状态获取更自然的做法是主动发 statusupdate 或写约定进度文件,而不是让 Manager 无界轮询。执行终止应以优雅终止为首选,即 terminate -> 子 Agent 在安全点清理资源 -> ack -> 退出;强制终止只作兜底。

ai-agent-book 第 10 章指出,不共享上下文的多 Agent 系统需要两套基础设施:共享文件系统承载产物交换,形成数据平面;消息传递、状态查询、执行终止与资源调度组成控制平面。它特别强调,状态获取更自然的做法是主动发 status_update 或写约定进度文件,而不是让 Manager 无界轮询。执行终止应以优雅终止为首选,即 terminate -> 子 Agent 在安全点清理资源 -> ack -> 退出;强制终止只作兜底。

配套的 message_bus.py 把每条消息封装成 Envelope,携带 sender_idtargettypepayloadseqts,消息类型包括 task_assignedstatus_updateresultterminateackMessageBus 按订阅关系转发,支持点对点投递和 BROADCAST="*" 广播,history 保留完整消息流,short() 输出带时间戳和全局序号的紧凑日志,让实验“看得见”每条协作事件。

实验 10-4 把控制平面落到真实并行搜索上:Manager 动态启动多个同构 worker,每个 worker 使用独立 Playwright browser context。第一个 target_foundasyncio.Lock 下结算,只允许一次 terminate 广播,后续迟到命中只记录不重复结算。输家收到 terminate 后在安全点取消、发送 ack 并关闭 browser context。README 用 context 创建/关闭计数、串行基线 wall-clock 和 provenance 事件流作为验收证据。ai-agent-book 还强调,模型可以提出“完成”,但不能批准自己的“完成”;完成结论必须经过独立验证或 harness 门控。

管理者模式部分则提醒:Manager 是单点瓶颈,计划质量是整个系统的上限。A2A 协议进一步把任务生命周期标准化为 submitted、working、needs input、completed、failed,让协作双方不依赖自由文本猜测任务状态。

18P16 的对照与取舍P16 没有照搬中心 Message Bus,而是把 ai-agent-book 的控制平面原则落到固定拓扑的持久 Mailbox 和 request/response 状态机上:

P16 没有照搬中心 Message Bus,而是把 ai-agent-book 的控制平面原则落到固定拓扑的持久 Mailbox 和 request/response 状态机上:

  • 消息总线 vs 持久 Mailbox。 ai-agent-book 的中心 MessageBus 支持订阅和广播,适合动态 peer 集;P16 当前拓扑固定为 Lead 与多个平级队友,因此用持久 Mailbox 做点对点投递,不实现订阅、BROADCAST 或动态生成队友。
  • Envelope vs ProtocolMailboxMessage。 ai-agent-book 的 Envelopesender_id/target/type/payload 表达内容信封;P16 的 ProtocolMailboxMessage 增加 request_idapproved,并进入持久 request/response 状态机。模型不再从自由文本里猜“这条回复对应哪个请求、是否已批准”。
  • terminate/ack vs request_shutdown。 ai-agent-book 用 terminate 做通用并行取消;P16 的 request_shutdown 是结构化优雅关闭:先完成当前消息,再在模型调用前路由 shutdown,向 Lead 发回结构化响应,然后结束 worker;Runner 资源仍由组合根统一关闭。
  • 状态查询 vs 协议状态真相。 ai-agent-book 建议用 status_update 或进度文件代替轮询;P16 不引入轮询,而是把请求持久化为 .agent_tutorial/protocol/state.json,用 pending/approved/rejected 和 canonical history 表达协作状态,重启后仍可恢复。
  • Manager 计划质量 vs plan gate。 ai-agent-book 指出规划质量决定 Manager 模式上限;P16 不把计划审批只交给模型记忆,而是用 submit_plan/review_planPermissionRule,在 effectful handler 和后台提交之前硬性拦截未批准计划。
  • A2A 生命周期 vs P16 封闭状态机。 A2A 面向跨组织互操作,任务状态是 submitted/working/needs input/completed/failed;P16 是同一团队内的封闭协议,只实现 shutdown/plan_approval 两类请求和 pending/approved/rejected 三种状态,不引入 Agent Card、流式进度或跨组织发现。
  • 真实实验 vs 离线验证。 ai-agent-book 用真实浏览器和真实 wall-clock 证明级联终止与资源审计;P16 聚焦可重复离线测试:注入确定 UUID、UTC clock 和模型替身,断言请求先登记、响应只消费一次、过期/错误参与方被拒绝、shutdown 不进入模型、资源全部关闭。

P16 明确不做的事: 没有中心 MessageBus、订阅过滤、BROADCASTstatus_update、跨组织 A2A 或动态 peer 发现。ai-agent-book 的广播和开放协议适合 Agent 数量不固定、需要自主发现与互操作的场景;第 16 章先守住“固定团队、持久请求、严格匹配、硬门控”这条最小闭环,下一章再让队友基于共享任务板主动发现和认领工作。


19如何验证本章测试注入确定 UUID、UTC clock、模型和故障边界,不依赖真实 OpenAI、网络或 sleep:

本章测试注入确定 UUID、UTC clock、模型和故障边界,不依赖真实 OpenAI、网络或 sleep:

  • [协议 store 与 typed Mailbox 测试](code/chapters/ch16/tests/ch16-protocol.test.ts)
  • [计划硬门控测试](code/chapters/ch16/tests/ch16-plan-gate.test.ts)
  • [Teammate 协议路由测试](code/chapters/ch16/tests/ch16-runtime.test.ts)
  • [P15/P16 增量与组合根测试](code/chapters/ch16/tests/ch16-bootstrap.test.ts)
  • [双入口测试](code/chapters/ch16/tests/ch16-entry.test.ts)

运行聚焦验证:

Set-Location 'F:\笔记\Agent实操\code'
npm run test:ch16
npm run typecheck
npm run lint
npm run format:check
npm run build

关键断言都是具体业务结果:

  • P16 capability 严格等于 P15 加 protocolplan_gate
  • P15 普通消息枚举和序列化 schema 完全不变;
  • 请求先持久登记,发送失败仍保留 pending 状态;
  • ID 碰撞、坏状态和原子替换失败不覆盖旧字节;
  • request ID、类型、双方、pending 和有效期必须全部匹配;
  • 截止时刻已经过期,同 clock 请求仍按创建顺序判定最新,时钟倒退明确失败;
  • 不同响应并发时只有一个完成迁移,同 transport 重试幂等,另一个 message ID 被拒绝;
  • pending/rejected 计划让 effectful handler 零调用,approved 后仍保留原 approval/audit;
  • busy 队友完成当前消息后才 shutdown,协议请求不进入模型;
  • ack、取消、投递和协议存储故障保留可恢复 transport;无效协议消息才 quarantine;
  • P16 Lead 与 Teammate 工具集合精确,两个 CLI 入口共享同一 transport 并关闭全部资源。

20本章明确不做什么- daemon thread、每秒 idle polling 或无人持有的 task;

P16 只完成结构化协议和计划执行门控,没有提前实现:

  • 未定义的 request_plan 别名;
  • daemon thread、每秒 idle polling 或无人持有的 task;
  • 自动扫描任务板、任务窃取、SQLite claim token 或 lease;
  • Worktree 隔离、MCP 动态工具池;
  • 把 pending 发送失败静默改成另一个请求;
  • 对任意外部副作用的全局 exactly-once 承诺。

任务板扫描、并发认领和 shutdown 优先级属于第 17 章。P16 只保证协议请求可追踪、响应严格消费一次,并让计划状态在真正的工具执行边界生效。


21小结1. 请求使用规范 UUID、aware UTC 和持久单快照,先登记再发送。

协议层的价值,是把隐式约定变成可验证状态:

  1. 请求使用规范 UUID、aware UTC 和持久单快照,先登记再发送。
  2. 协议拥有独立 typed Mailbox envelope,不污染 P15 普通消息 schema。
  3. 响应同时验证 ID、类型、双方、pending 和过期时间,并只迁移一次。
  4. Lead 先把 typed event 写入 canonical history,再消费状态并 ack transport。
  5. shutdown 在当前消息之后确定性处理,不进入模型;故障保留恢复机会。
  6. plan gate 位于权限硬边界,pending/rejected 时 handler 和后台提交均为零调用。
  7. idle 生命周期仍由事件驱动,没有轮询线程,也没有提前实现下一章的自驱找活。

做到这些以后,“请关机”“计划已批准”不再是模型之间靠语气猜测的聊天,而是可以持久恢复、严格匹配、并发验证和失败重试的运行时契约。下一篇再让队友基于共享任务板主动发现并认领工作。

03 / 自测

换个场景,你还会判断吗?

答完再看理由

每题只测一个边界。先做决定,再看解释。

SCENARIO CHECK01 / 030 分

准备开始