docs(design): 0013 转已接受,补进四条来自 pi 的实据

去核 reference/pi 之后补的:它同一组三份投影对不上的地方有四处不是一处(决策四);
它先发事件再落盘,反着做没问题是因为订阅者与进程同生共死,本库的出口活得比进程久
(决策五);它的回调失败处置独立收敛到同一条分界——观察型抛异常照跑、工具执行前那个
fail-closed(决策八);它一轮发十来条是被终端界面逼出来的,而它同样把实时事件流与
持久记录分成两套(决策三)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 22:43:20 -04:00
parent 8aa430c7df
commit 2385da3a97
@@ -1,6 +1,6 @@
# Design 0013 · 事件集与具名回调清单 # Design 0013 · 事件集与具名回调清单
**日期** 2026-08-10 · **状态** 确认 **日期** 2026-08-10 · **状态** 已接受(2026-08-10 项目负责人确认
**落实** `0003-public-api-shape.md` 决策五那句「观察走事件流、干预走具名回调」,并补上 **落实** `0003-public-api-shape.md` 决策五那句「观察走事件流、干预走具名回调」,并补上
`0007-seam-behaviour.md`「留给后续的」那半个契约——事件出口的「投递失败不打断循环」已经定了, `0007-seam-behaviour.md`「留给后续的」那半个契约——事件出口的「投递失败不打断循环」已经定了,
@@ -12,7 +12,7 @@
**触及** `../../src/polyloop/ports/``Event` 加字段、新增 `EventKind`)、 **触及** `../../src/polyloop/ports/``Event` 加字段、新增 `EventKind`)、
`../../src/polyloop/session/`(发事件的那一处)、`../../tests/contract/test_event_sink.py` `../../src/polyloop/session/`(发事件的那一处)、`../../tests/contract/test_event_sink.py`
**它过 `../../CLAUDE.md` §2 那道人类门**,因为改的是公共类型的字段:`Event` 从零字段变成有 **它过 `../../CLAUDE.md` §2 那道人类门**,因为改的是公共类型的字段:`Event` 从零字段变成有
字段,而 `EventKind` 的取值集合从此是一份对外承诺。 字段,而 `EventKind` 的取值集合从此是一份对外承诺。
## 读本文需要的几个名字 ## 读本文需要的几个名字
@@ -110,6 +110,12 @@ GovDoc 的两类界内事件都是**一次迭代发一条**,而一次迭代在
的动作是执行一段 Python、可能很慢,但 dissect 根本不看事件。**一个真实消费者都举不出来的 的动作是执行一段 Python、可能很慢,但 dissect 根本不看事件。**一个真实消费者都举不出来的
分辨率,按 `0001` 决策三不实现也不预留。** 分辨率,按 `0001` 决策三不实现也不预留。**
`reference/pi` 是另一个极端,一轮迭代发十来条(`agent/test/agent-loop.test.ts:1185` 把整个
序列钉死了),事件类型十种。那个粒度是被终端界面逼出来的——它要让用户看着模型一个字一个字
往外吐,所以连流式分片都发一条。本库没有这种消费者。值得注意的是 pi 也把实时事件流与持久
记录分成了两套东西:落盘那套是另外九种记录(`agent/src/harness/session/types.ts:203`),事件
流不落盘。这跟决策一是同一个分法,只是它的事件流因为要喂界面而切得极细。
一步走完时那条步记录覆盖了它两类事件载荷的大部分:`raw_output` 是修复后的文本,`parse_ok` 一步走完时那条步记录覆盖了它两类事件载荷的大部分:`raw_output` 是修复后的文本,`parse_ok`
`parse_error` 是解析结果,`tool_name` / `tool_arguments` / `observation` / `action_status` `parse_error` 是解析结果,`tool_name` / `tool_arguments` / `observation` / `action_status`
是工具那一侧。**没有覆盖的有两样,各自的去处写在这里,免得下一个人以为它们被漏了:** 是工具那一侧。**没有覆盖的有两样,各自的去处写在这里,免得下一个人以为它们被漏了:**
@@ -146,10 +152,22 @@ class Event:
数据类的这三个开关是本库所有公共数据类的统一形状,理由在 `0009`,不在本文重复。 数据类的这三个开关是本库所有公共数据类的统一形状,理由在 `0009`,不在本文重复。
**不挑几个字段拼一份摘要。** 挑出来的那份是一次投影,而投影会漂移`0003` 决策五驳回观察 **不挑几个字段拼一份摘要。** 挑出来的那份是一次投影,而投影会漂移`0003` 决策五驳回观察
投影接缝时举的正是这个——参考仓库 `reference/pi`三份投影已经漂移到同一条记录在正常回合 投影接缝时举参考仓库 `reference/pi`例子,写这一条时又去核了一遍,实际情况比那句话更糟:
可见、在压缩历史里不可见。带整条记录的话,`StepRecord` 加一个字段,事件里自动就有,两边不 它同一组三份投影对不上的地方有四处,而不是一处。
可能对不上。
| 同一条会话条目 | `harness/session/context.ts:65` | `harness/compaction/compaction.ts:68` | `harness/compaction/branch-summarization.ts:112` |
|---|---|---|---|
| `custom` 类型的条目 | 走投影器,可见 | 落到默认分支,不可见 | 显式返回空,不可见 |
| 压缩摘要后面保留的那截尾巴 | 整段带上 | 丢掉 | 丢掉 |
| 停止原因是「推迟」的助手消息 | 显式排除 | 没有这个判断,带进去 | 没有这个判断,带进去 |
| 工具结果消息 | 保留 | 保留 | 显式丢弃 |
四处没有一处是写错的——每一份投影单独看都讲得通,它们只是在不同时间被不同的需求改过。这正是
投影这种东西的失效形态:不报错、不崩溃,只是同一条记录在两个地方长得不一样,而发现它要有人
同时读三个文件。
带整条记录就没有这个问题:`StepRecord` 加一个字段,事件里自动就有,两边不可能对不上。
**`run_id` 在场,因为一个出口可以被并发的多次运行共用。** 事件出口挂在定义上(`0003` 决策 **`run_id` 在场,因为一个出口可以被并发的多次运行共用。** 事件出口挂在定义上(`0003` 决策
三),而定义可以被多次运行共用;没有运行标识,两次并发运行的事件在出口那边混成一串。 三),而定义可以被多次运行共用;没有运行标识,两次并发运行的事件在出口那边混成一串。
@@ -173,6 +191,12 @@ class Event:
存储里没有——那正是决策一那条不变量被打破的样子,而它的表现是「进度表里有第 7 步、日志里 存储里没有——那正是决策一那条不变量被打破的样子,而它的表现是「进度表里有第 7 步、日志里
只到第 6 步」,谁也说不清哪个是真的。 只到第 6 步」,谁也说不清哪个是真的。
**`reference/pi` 是反着做的**`coding-agent/src/core/agent-session.ts:633`,那一行的注释就写着
先发给扩展),而它那么做没有问题,因为它的订阅者是同一个进程里的终端界面:进程死了订阅者跟着
死,「看见了但没落盘」这件事不会留下任何痕迹。本库的出口可以是一个活得比进程久的数据库,
所以同一个顺序在这里会留下一份对不上的记录。同一件事在两个项目里答案不同,差别在订阅者的
寿命,不在哪种写法更讲究。
**发在停止判定之前,因为停止判定可能不返回。** 判定一旦决定收尾,控制流就去写结束记录、 **发在停止判定之前,因为停止判定可能不返回。** 判定一旦决定收尾,控制流就去写结束记录、
组装 `RunResult`、返回给调用方了。把发事件排在判定之后,最后一步就没有事件——而一次运行的 组装 `RunResult`、返回给调用方了。把发事件排在判定之后,最后一步就没有事件——而一次运行的
最后一步恰恰是最值得看见的那一步(它是「做完了」还是「预算烧光了」)。排在判定之前,每一步 最后一步恰恰是最值得看见的那一步(它是「做完了」还是「预算烧光了」)。排在判定之前,每一步
@@ -250,6 +274,13 @@ class Event:
一个没生效的干预和一个生效了的干预会产出两次不同的运行,而静默继续意味着事后分不出是哪一种。 一个没生效的干预和一个生效了的干预会产出两次不同的运行,而静默继续意味着事后分不出是哪一种。
事件出口按前者处置(接住、计数、继续),任何将来的回调按后者(原样抛出、终止运行)。 事件出口按前者处置(接住、计数、继续),任何将来的回调按后者(原样抛出、终止运行)。
`reference/pi` 独立走到了同一条线上:它的扩展回调抛异常会被接住、转成一条错误事件、其余回调
照跑(`coding-agent/src/core/extensions/runner.ts:809`),唯独工具执行前那一个回调抛异常会挡住
这次工具调用(`coding-agent/src/core/agent-session.ts:491`)。它的设计文档把这条写成一句
——抛异常的回调不会让运行失败,只有工具执行前那个是 fail-closed
`agent/docs/harness-v2.md:1290`)。两边都不是从原则推出来的,是各自撞到「一个悄悄没生效的
干预事后查不出来」之后收敛到的同一处。
## 代价 ## 代价
**慢出口按步拖慢运行。** 决策五不排队,所以一个每步花两百毫秒写数据库的出口,在五十步的 **慢出口按步拖慢运行。** 决策五不排队,所以一个每步花两百毫秒写数据库的出口,在五十步的