一次性子 Agent 与持续队友的差别不在有没有并发,而在身份、历史、通信和生命周期由谁持有。队友的汇报不是日志,而是一条持久消息,以 role=user 的结构化事件进入 Lead 历史——打印只能让人看到,进 history 才能让模型继续协作。
生命周期 一次性:完成返回 | 队友:完成进 idle 可续收
对话历史 每次独立 | 每个名字绑定独立 AgentRunner 复用
通信 返回最终结果 | 持久 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 键是 (created_at_utc, id)。processing 在重启恢复到 ready(至少一次投递);坏消息逐个移入 quarantine 并继续。
权威顺序: (created_at_utc, id)
重启: processing -> ready -> 再次 claim(至少一次)
坏消息: 校验失败 -> 原子移入 quarantine,继续找下一条
spawn 不等待完成。worker claim 后调用自己的 AgentRunner.run(content, { idempotencyKey: current.id })。完成一轮进 idle 不销毁 Runner;后续消息复用原 Runner 与历史。
spawn: running
一轮完成: idle
收到新消息: running
再次完成: idle
执行异常: failed
runtime close 完成: shutdown
队友消息从 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
创建独立 Runner,把首个 task 消息持久写入队友 Mailbox,异步启动 worker。
ready→processing,用消息 ID 作 idempotencyKey 运行。
把结果写到 lead/ready,ack 当前输入,进 idle。
claim lead 消息 → 进 history → ack → 模型决策。
向已存在队友(或 Lead)投递后续文本,复用原 Runner。