第 十五 章 Agent 架构实操 深入学习 · 交互式 含 QA 测试

解密 Claude Code 协作机制:如何通过 Inbox 注入让 AI 队友真正实现“异步通信”

💡 持续 Teammate + 持久 Mailbox + typed event 进入唯一 Loop。
有些工作不是问答:队友需要保留历史、接收补充、异步汇报、完成后继续存在。本章实现持续 Teammate、持久 Mailbox,以及把 Mailbox 消息送入公共 Loop 的 typed event 通道。
本章进度
0%
1 本章要掌握的目标
  • Lead 只增加 spawn_teammate / send_message 两个工具,无 check_inbox;本章只做文本消息的持久双向投递。
  • MailboxMessage kind:task / message / result,同时实现 RuntimeEvent;sender 来自 ToolContext 而非输入。
  • 每消息一个文件,目录表达状态 ready/processing/done/quarantine;原子 rename 迁移、双锁保护。
  • 至少一次投递;先写 canonical history 再 ack(mailbox 是唯一保留半状态的事件类型)。
  • 队友每名持有独立可复用的 Runner,状态 running/idle/failed/shutdown;idle 是被动的,不会自己找活。
2 核心知识点
协作不是多开模型调用

一次性子 Agent 与持续队友的差别不在有没有并发,而在身份、历史、通信和生命周期由谁持有。队友的汇报不是日志,而是一条持久消息,以 role=user 的结构化事件进入 Lead 历史——打印只能让人看到,进 history 才能让模型继续协作。

生命周期  一次性:完成返回 | 队友:完成进 idle 可续收
对话历史  每次独立      | 每个名字绑定独立 AgentRunner 复用
通信     返回最终结果   | 持久 Mailbox 双向文本
Mailbox:每消息一文件,状态由目录表达

「读文件+删文件」的收件箱是至多一次投递:读完与写入 history 之间崩溃,消息永久消失且无痕迹。本章 send 先写临时文件并 fsync,再 os.replace 提交到 ready;claim 用双锁(进程内 promise 队列 + 跨进程 proper-lockfile)原子 rename ready→processing;ack 用 processing→done;quarantine 隔离坏消息——换来至少一次投递。

.agent_tutorial/mailboxes/
  lead/   ready/ processing/ done/ quarantine/
  alice/  ready/ processing/ done/ quarantine/
ready/{id}.json --os.replace--> processing/{id}.json
processing/{id}.json --os.replace--> done/{id}.json
FIFO、恢复与坏消息隔离

权威 FIFO 键是 (created_at_utc, id)。processing 在重启恢复到 ready(至少一次投递);坏消息逐个移入 quarantine 并继续。

权威顺序: (created_at_utc, id)
重启:  processing -> ready -> 再次 claim(至少一次)
坏消息: 校验失败 -> 原子移入 quarantine,继续找下一条
TeammateRuntime:一个名字一个 Runner

spawn 不等待完成。worker claim 后调用自己的 AgentRunner.run(content, { idempotencyKey: current.id })。完成一轮进 idle 不销毁 Runner;后续消息复用原 Runner 与历史。

spawn: running
一轮完成: idle
收到新消息: running
再次完成: idle
执行异常: failed
runtime close 完成: shutdown
Inbox 注入:先进 history 再 ack

队友消息从 ready claim 到 processing → 发布到共享 EventInbox(与 cron/background 同一个,不新建第二通道)→ wakeup 唤醒宿主 runEvents() → 先追加 canonical history → 再 ack(processing→done)。ack 失败重试不重复 history 或模型调用;跨 runtime 相同 done 消息可幂等 ack,内容冲突明确失败。mailbox 是唯一保留半状态(已进 history、ack 未完成)的事件类型,逆序崩溃会永久丢工作。

EventInbox drain
  → 校验稳定 event_id
  → 追加到 Lead canonical history
  → TeammateRuntime.acknowledge_events()
  → Mailbox processing -> done
  → 模型执行这一条 event turn
3 机制流程
1
spawn_teammate

创建独立 Runner,把首个 task 消息持久写入队友 Mailbox,异步启动 worker。

2
worker claim

ready→processing,用消息 ID 作 idempotencyKey 运行。

3
完成后发 result

把结果写到 lead/ready,ack 当前输入,进 idle。

4
Lead event turn

claim lead 消息 → 进 history → ack → 模型决策。

5
send_message

向已存在队友(或 Lead)投递后续文本,复用原 Runner。

4 术语表
MailboxMessageKindtask / message / result 三种;发送时由 ToolContext 注入可信 sender。
processing → ready重启恢复路径与 release 归还,保证至少一次投递。
quarantine坏消息或失败输入隔离留证据,不阻塞合法消息;队友失败向 Lead 发布可观察的 result。
idempotencyKey消息稳定 UUID,作为本轮工具上下文用于去重。
至少一次可重放但不可静默丢失;读+删的替代方案是至多一次,有永久丢失窗口。
wakeup新消息到达时请求宿主跑 runEvents 的回调;关闭顺序 Teammate→Cron→supervisor→model。
刻意不做无 request/response、计划审批/权限冒泡、shutdown 握手(P16);无工作窃取(P17);无广播/队友生队友;idle 只等显式消息。
5 QA 测试环节(自测题)
已完成 0 / 6 · 答对 0
Q1. P15 的 Lead 是否提供 check_inbox?
Q2. 消息的权威 FIFO 顺序是?
Q3. ack 的含义是?
Q4. cron / mailbox / background 的事件进入哪里?
Q5. close 时正在执行的队友会?
Q6. 队友工具集是?
6 验证与实验
  • npm run test:ch15:44 个测试文件 / 336 个用例,注入确定 UUID/UTC clock/模型替身,全程离线;P15 主 Agent 工具序列 18 个(原 16 + spawn_teammate/send_message),队友严格四工具 shell/read_file/write_file/send_message。
  • 验证 FIFO、同名 spawn 只一次、先 history 后 ack、ack 失败重试不重复;关闭顺序 Teammate → Cron → supervisor → model。
  • npm run ch15 -- --prompt "创建一名 writer 队友,让她阅读 README 并汇报缺口"