From 2385da3a976ff564fc7ec270898e7c40e0b2e8c6 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 10 Aug 2026 22:43:20 -0400 Subject: [PATCH] =?UTF-8?q?docs(design):=200013=20=E8=BD=AC=E5=B7=B2?= =?UTF-8?q?=E6=8E=A5=E5=8F=97=EF=BC=8C=E8=A1=A5=E8=BF=9B=E5=9B=9B=E6=9D=A1?= =?UTF-8?q?=E6=9D=A5=E8=87=AA=20pi=20=E7=9A=84=E5=AE=9E=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去核 reference/pi 之后补的:它同一组三份投影对不上的地方有四处不是一处(决策四); 它先发事件再落盘,反着做没问题是因为订阅者与进程同生共死,本库的出口活得比进程久 (决策五);它的回调失败处置独立收敛到同一条分界——观察型抛异常照跑、工具执行前那个 fail-closed(决策八);它一轮发十来条是被终端界面逼出来的,而它同样把实时事件流与 持久记录分成两套(决策三)。 Co-Authored-By: Claude Opus 5 (1M context) --- .../design/0013-event-set-and-callbacks.md | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/research-wiki/design/0013-event-set-and-callbacks.md b/research-wiki/design/0013-event-set-and-callbacks.md index a1e1826..24b0cc6 100644 --- a/research-wiki/design/0013-event-set-and-callbacks.md +++ b/research-wiki/design/0013-event-set-and-callbacks.md @@ -1,6 +1,6 @@ # Design 0013 · 事件集与具名回调清单 -**日期** 2026-08-10 · **状态** 待确认 +**日期** 2026-08-10 · **状态** 已接受(2026-08-10 项目负责人确认) **落实** `0003-public-api-shape.md` 决策五那句「观察走事件流、干预走具名回调」,并补上 `0007-seam-behaviour.md`「留给后续的」那半个契约——事件出口的「投递失败不打断循环」已经定了, @@ -12,7 +12,7 @@ **触及** `../../src/polyloop/ports/`(`Event` 加字段、新增 `EventKind`)、 `../../src/polyloop/session/`(发事件的那一处)、`../../tests/contract/test_event_sink.py`。 -**它要过 `../../CLAUDE.md` §2 那道人类门**,因为改的是公共类型的字段:`Event` 从零字段变成有 +**它过了 `../../CLAUDE.md` §2 那道人类门**,因为改的是公共类型的字段:`Event` 从零字段变成有 字段,而 `EventKind` 的取值集合从此是一份对外承诺。 ## 读本文需要的几个名字 @@ -110,6 +110,12 @@ GovDoc 的两类界内事件都是**一次迭代发一条**,而一次迭代在 的动作是执行一段 Python、可能很慢,但 dissect 根本不看事件。**一个真实消费者都举不出来的 分辨率,按 `0001` 决策三不实现也不预留。** +`reference/pi` 是另一个极端,一轮迭代发十来条(`agent/test/agent-loop.test.ts:1185` 把整个 +序列钉死了),事件类型十种。那个粒度是被终端界面逼出来的——它要让用户看着模型一个字一个字 +往外吐,所以连流式分片都发一条。本库没有这种消费者。值得注意的是 pi 也把实时事件流与持久 +记录分成了两套东西:落盘那套是另外九种记录(`agent/src/harness/session/types.ts:203`),事件 +流不落盘。这跟决策一是同一个分法,只是它的事件流因为要喂界面而切得极细。 + 一步走完时那条步记录覆盖了它两类事件载荷的大部分:`raw_output` 是修复后的文本,`parse_ok` 与 `parse_error` 是解析结果,`tool_name` / `tool_arguments` / `observation` / `action_status` 是工具那一侧。**没有覆盖的有两样,各自的去处写在这里,免得下一个人以为它们被漏了:** @@ -146,10 +152,22 @@ class Event: 数据类的这三个开关是本库所有公共数据类的统一形状,理由在 `0009`,不在本文重复。 -**不挑几个字段拼一份摘要。** 挑出来的那份是一次投影,而投影会漂移:`0003` 决策五驳回观察 -投影接缝时举的正是这个——参考仓库 `reference/pi` 的三份投影已经漂移到同一条记录在正常回合 -可见、在压缩历史里不可见。带整条记录的话,`StepRecord` 加一个字段,事件里自动就有,两边不 -可能对不上。 +**不挑几个字段拼一份摘要。** 挑出来的那份是一次投影,而投影会漂移。`0003` 决策五驳回观察 +投影接缝时举过参考仓库 `reference/pi` 的例子,写这一条时又去核了一遍,实际情况比那句话更糟: +它同一组三份投影对不上的地方有四处,而不是一处。 + +| 同一条会话条目 | `harness/session/context.ts:65` | `harness/compaction/compaction.ts:68` | `harness/compaction/branch-summarization.ts:112` | +|---|---|---|---| +| `custom` 类型的条目 | 走投影器,可见 | 落到默认分支,不可见 | 显式返回空,不可见 | +| 压缩摘要后面保留的那截尾巴 | 整段带上 | 丢掉 | 丢掉 | +| 停止原因是「推迟」的助手消息 | 显式排除 | 没有这个判断,带进去 | 没有这个判断,带进去 | +| 工具结果消息 | 保留 | 保留 | 显式丢弃 | + +四处没有一处是写错的——每一份投影单独看都讲得通,它们只是在不同时间被不同的需求改过。这正是 +投影这种东西的失效形态:不报错、不崩溃,只是同一条记录在两个地方长得不一样,而发现它要有人 +同时读三个文件。 + +带整条记录就没有这个问题:`StepRecord` 加一个字段,事件里自动就有,两边不可能对不上。 **`run_id` 在场,因为一个出口可以被并发的多次运行共用。** 事件出口挂在定义上(`0003` 决策 三),而定义可以被多次运行共用;没有运行标识,两次并发运行的事件在出口那边混成一串。 @@ -173,6 +191,12 @@ class Event: 存储里没有——那正是决策一那条不变量被打破的样子,而它的表现是「进度表里有第 7 步、日志里 只到第 6 步」,谁也说不清哪个是真的。 +**`reference/pi` 是反着做的**(`coding-agent/src/core/agent-session.ts:633`,那一行的注释就写着 +先发给扩展),而它那么做没有问题,因为它的订阅者是同一个进程里的终端界面:进程死了订阅者跟着 +死,「看见了但没落盘」这件事不会留下任何痕迹。本库的出口可以是一个活得比进程久的数据库, +所以同一个顺序在这里会留下一份对不上的记录。同一件事在两个项目里答案不同,差别在订阅者的 +寿命,不在哪种写法更讲究。 + **发在停止判定之前,因为停止判定可能不返回。** 判定一旦决定收尾,控制流就去写结束记录、 组装 `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`)。两边都不是从原则推出来的,是各自撞到「一个悄悄没生效的 +干预事后查不出来」之后收敛到的同一处。 + ## 代价 **慢出口按步拖慢运行。** 决策五不排队,所以一个每步花两百毫秒写数据库的出口,在五十步的