Commit Graph

32 Commits

Author SHA1 Message Date
iomgaa 95898bccf3 docs: 事件集落地后回写 GovDoc 缺口清单与阶段清单
「审计出口与事件流的关系」那条缺口转到已回答:审计由意图日志承担,出口要包在存储接缝上
而不是接在事件出口上;三类事件里 phase_recovery 那一类在界外。新登记一条缺口:工具单独的
耗时拿不到,本库只有整步墙钟,迁过去口径会变。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:57:09 -04:00
iomgaa 8c642e7881 test(contract): 按 Codex 审查改掉五条说明型测试的名字
它们函数体是空的,名字却承诺了一个行为,读起来像「这条已经验过了」——而契约套件是新适配器
的准入标准,下游跑一遍看见绿的会以为自己那一条被验过。名字改成说清楚断言在哪一层,
函数体那段解释保持不动。

Codex 另报「测试 docstring 里出现 GovDoc 是业务词汇」,不采纳:§1.1 那道扫描挡的是领域词
(公文、超声、招标),说明一条测试被哪个消费者的需求逼出来不构成业务假设,仓库里这类引用
本来就有十几处。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:56:02 -04:00
iomgaa e71dca7c35 feat(session): 落成事件出口,五个接缝全部有调用点
Event 从零字段变成 kind + run_id + model_binding + step,新增 EventKind(只有一种取值,
但第一天就带 kind,逼每个出口分发)。事件在「一步走完」原子落地之后发,只有这次进程里
真的执行过的步才发;投递失败接住、计数进 RunResult、继续跑,CancelledError 原样穿过。

契约套件那两条 xfail 关掉:一条要断言的是库发了几次、接缝自己看不到;另一条的前提是错的
——审计纪律由意图日志承担不由事件流承担,改成在 unit 层验日志里原文与改写后的文本各有
位置。_project_observation 那段说「将来靠事件流送出去」的注释一并改对。

283 passed / 15 skipped / 2 xfailed,剩下两条 xfail 是原子写与前缀持久性,没有机器兜底。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:51:19 -04:00
iomgaa 2385da3a97 docs(design): 0013 转已接受,补进四条来自 pi 的实据
去核 reference/pi 之后补的:它同一组三份投影对不上的地方有四处不是一处(决策四);
它先发事件再落盘,反着做没问题是因为订阅者与进程同生共死,本库的出口活得比进程久
(决策五);它的回调失败处置独立收敛到同一条分界——观察型抛异常照跑、工具执行前那个
fail-closed(决策八);它一轮发十来条是被终端界面逼出来的,而它同样把实时事件流与
持久记录分成两套(决策三)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 22:43:20 -04:00
iomgaa 8aa430c7df test(contract): 0007 确认后关掉那三条 xfail
解释器不许抛异常是接缝自己的行为,改成正面断言(等实现接进来才跑)。另两条的答案
落在库这一侧不在接缝上:动作状态的触发条件是执行器自己的判断,套件面对任意实现逼
不出后两档,硬探会把 dissect 那种状态恒为 EXECUTED 的合法实现判成不合格;观察由谁
合成同理。两条留成不断言的说明,指向 tests/unit/test_session.py 里真正验它们的地方。

273 passed / 15 skipped / 4 xfailed。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 04:53:27 -04:00
iomgaa 1cd4344b16 docs(design): 落成 0013,定事件集、发事件的那一处与回调清单
事件集只有「一步走完」一种,带整条步记录而不是摘要;审计纪律由意图日志承担,
事件流保持可丢。写文档时去核 0007 决策二那笔「执行器原文丢了」的代价,发现它
并没有真的发生——「一步走完」这条记录持久化的是动作执行接缝的原样返回值,库只
替换了步记录里那一列,所以那段文本一直在日志里。

状态待确认:Event 从零字段变成有字段,按 CLAUDE.md §2 要过人类门。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 04:50:33 -04:00
iomgaa 183113f624 docs: 0011 与 0012 转已接受,README 阶段清单勾到 ⑤
④ 与 ⑤ 都勾上但各自注明了欠账:e2e 那一层还是空的(它要打真实模型网关);事件出口没有调用
点(Event 还没有字段);stores 只有逐行追加那一种形态。勾上是因为骨架与十个模块确实都落地
了,注明欠账是因为清单是进度的权威处,含糊会让人以为这两件事已经做完。
2026-08-10 04:24:01 -04:00
iomgaa 4f3f43d218 docs(design): 按两轮硕士生冷读改 0011 与 0012
0011 最要紧的一条是文档和代码对不上:Codex 那轮把坏行判据从「第一条解不开的行」改成了「有没有
被换行终结」,文档还停在旧规则上。改完顺带答掉冷读问的「末尾连着两条坏行算什么」——按新规则
第一条终结过的坏行就已经报错了。

三处确定性矛盾全部成立:标题写「fsync 在三处,其余三处不做」而正文写「那两次」、表格里 fsync=否
只有两行(改成按「一步之内四次写」重排,并把「处」的单位说清);「五个记录类」里没有「动作
结果」(它是逐步结果那条记录的一个字段,不是第六个记录类,表格行名会误导);「每步四次写」与
表格看着像五次(同一根因)。另外契约套件的状态从「24 条全跳过」改成不给会过期的数字,并把
skip 与 xfail 分开说——它们是「还没有实现」与「没有机器兜底」两回事。

还补了:一节名词解释(前缀持久性、耐久屏障、恢复判定、⑥ 都是首次出现即使用);「取消能穿过去」
那段原本自相矛盾(说线程会把写做完,又说没写完的是尾行);文件的字面约定(UTF-8、\n 结尾、
非 ASCII 不转义、目录不存在时创建)——这些恰恰是外部读取方必须知道的,而文档反复强调那份日志
要能离开这个库读懂;为什么保留键叫 record 而不是加下划线前缀;为什么用 to_thread;独占创建
只挡住一种撞车(两个进程同时续跑挡不住,登记为已知缺口);以及运行开始那次 fsync 真正的理由
是目录项而不是「读不出配置」。

最实的一条留到最后:那五维里的题目很可能带中文或空格,过不了运行标识的字符判据,而同一份
文档又规定不做转义。现在写明编码方式归下游自己选(要单射),并说清库为什么不替它选——库一旦
选了,文件名就不再等于运行标识,而它按标识去目录里找文件的用法就断了。migrations/dissect.md
同步登记。

0012:把 parent_card_id 这个笔误改成 parent_call_id(冷读的人不知道哪个对,只能问「card 是
什么」,正好把它顶出来);「每个源报四样」实际枚举了五样;补一节名词解释(scope、源、恒定采样
参数、推理开关全是首次出现即使用);补上本库这一侧的接缝签名与 ModelCall 的五个字段,并说明
中间那三个为什么一个都不往下传;补上非文本块报错、拼接不加分隔符的代价、1.1.1 那个 bug 到底
是什么、空串调用标识是防御而不是常规路径、以及为什么超时与重试次数不算模型身份。
2026-08-10 04:06:16 -04:00
iomgaa 39417a21ec feat(adapters): 落成网关适配器,十个模块全部有内容
design 0012(待确认)定六条:收一个已经装配好的客户端而不自己装配(治理参数按 §1.5 不归本库
管,而且项目常要在多个用途间共享同一个限流器和缓存);同时收那份配置只为算模型身份(客户端
把源列表与 scope 收在内部不公开);内容块按顺序拼成一个字符串不加分隔符;绑定里只有网关认得
的两个键往下传、其余留在参数快照里且不报错;网关异常原样穿出去不翻译不重试;空串的调用标识
映射成空值。

参数快照不用网关内部那个 build_model_fingerprint:它不在 __all__ 里(用它就得从子模块 import,
他们重排一次我们就断),而且它是为缓存键设计的、按模型名去重。续跑守卫怕的是「配置变了而我
没发现」,所以宁可更严——改一个源名也报出来,那意味着这次运行打的可能是另一个端点。

tests/integration/ 这一层第一次有内容:用的是网关真实的 GatewaySettings / LLMResponse /
异常类型,装配守卫也真的跑了,只把「真的发出去」那一下换成受控替身。没装 polyloop[gateway]
时整份文件跳过——一个因为可选依赖没装而常年红的套件会训练所有人忽略红。

环境:从 ~/Projects/PolyGateway(活版本 1.1.2,比 reference/ 那份 1.1.1 新)复制一份装进
conda 环境。没有配私有源,polygateway 不在任何可达的 index 上。
2026-08-10 03:57:50 -04:00
iomgaa aeb575e0f7 fix(stores): 修 Codex 对抗审查报的五条,其中两条同一根因
最实的一条:读取端只要一段能解析成 JSON 就收下,没检查它后面有没有换行。而短写完全可能
正好写完整个 JSON 对象、只差那个换行——那次写从来没被确认过(调用方的 await 还没返回),
按契约就是「没发生」,但它会被当成一条有效的动作意图读回来,恢复据此判成「状态未知」并可能
重放,而那个动作一定没执行过(调用方是在写意图返回之后才去执行的)。

判据改成「这一行有没有被换行终结」,不是「能不能解析」。同一个改动顺带修掉第三条:一行完整
终结的坏行(比如被外部追加的 {})此前会被当成撕裂尾行吞掉,读成「少了一条记录但看起来完整」
的日志;现在终结过的行解不开就是损坏,直接报错。

其余三条:
- 新建日志文件不 fsync 父目录。os.fsync(fd) 刷的是文件内容,刷不到「这个目录里多了一个
  文件」这条目录项;掉电后内容可能在而文件不存在,read_log 走「文件不存在」返回空日志,
  驱动入口判成全新运行,一次已经花过钱的运行静默没了留痕。只在新建时刷。
- 同一运行标识上的并发写会交错:一条记录可能由不止一次 os.write 写完,而 O_APPEND 只保证
  每次 write 的追加位置原子,保证不了一条逻辑行整体原子。按运行标识加锁串起来(不同运行
  照样并行),跨进程那一半仍靠独占创建挡。有一条用短写逼出那个窗口的测试。
- 往返测试的 TOTAL_WRITES 是硬编码,而且漏写结束标记它发现不了(恢复会把最后一步之后那次
  停止判定重演一遍,得出同样结果)。加一条把十次写的记录类型序列整个钉死的测试。
2026-08-10 03:52:35 -04:00
iomgaa 7f6066701e style(tests): 那个模拟崩溃的异常改名带 Error 后缀,过 ruff N818
上一个提交串在 make ci 后面,ci 挂了 commit 照样跑了——这是把两件事写进一条命令的代价。
2026-08-10 03:42:03 -04:00
iomgaa 7c52eeb33f test(round-trip): 在每一个写入边界上崩一次,断言续跑与一口气跑完等价
0002 末尾点名要这类测试,理由是恢复的 bug 天然没有失败现场——它不抛异常,只表现成「跑出来
的结果有点不一样」,而一次运行本来就次次不同,人眼分不出来。

一次两步的运行写十次(运行开始 + 每步四次 + 运行结束),十个边界逐个崩一遍,续跑的结果与
不中断跑完的逐字段相等(只抹平墙钟时间那一列)。工具与模型调用都声明可重放时十个断点全都
续得上。另外四条:恢复出来的消息历史与不中断时逐字相同(多一轮少一轮都不报错,只会让模型
从此看见不一样的东西);声明绝不重放时崩在动作意图与步记录之间正当地停在「状态未知」;模型
调用意图写了结果没写、且声明不能重来时同样停在那儿;以及续跑写下去的东西不能让下一次续跑
读不动(重放要是又写一条同种意图,恢复会判成日志被并发写过,一次成功的重放反倒把日志弄坏)。

放在 unit 而不是 integration:分层判据是「依赖什么」(CLAUDE.md §1.9),这里模型、解释器、
执行器全是替身,只多用了一个真实文件系统。等 adapters 落地真的连上网关,那两层才有内容。
2026-08-10 03:41:27 -04:00
iomgaa 6af289d283 feat(stores): 落成逐行追加的日志存储,契约套件第一次真的在跑
design 0011(待确认)定了六条:一次运行一个文件且文件名就是运行标识(不转义不哈希,按标识
去目录里找文件是最自然的用法;标识必须是安全文件名,否则 ../ 会把文件写到目录外面);一行
一条记录加一个 record 类型标签(serialization 编出来的载荷没有元信息键,标签是存储这层加的,
record 从此是保留键);第一条解不开的行就是日志结尾、它后面还有内容就是损坏;fsync 只在
运行开始、两条意图、运行结束四处(其余两处靠前缀持久性兜);写入走 to_thread;运行开始记录
用 O_EXCL 兜住跨进程撞车。

契约套件里那条 test_step_without_an_action_is_still_recorded 转成真断言——它标着 xfail 的
理由是「StepCompleted.result_id 在 0006 里是必填字符串」,而 0006 决策七早就把它改成可为空
并加了不变量。xfail 8→7,跳过 24→14。

原子写「一起不可见」那一半按契约套件的点名在这一层补上了:给实现留一个可注入的故障点
(一个可替换的「把这些字节写进去」),测试把它换成写一半就抛异常,断言那条记录整条不可见。
前缀持久性仍然验不了(掉电才看得出来),继续登记为已知缺口。

调研三条实据写进了 0011:两个下游 fsync 全仓零处(一个的 SQLite 还开着 synchronous=NORMAL),
所以这条比它们都严、代价是每步两次 fsync;一个下游的轨迹检查器同样是「碰到第一条坏行就放弃
整个文件」;另一个下游踩过「文件名少一维导致两个阶段静默互相覆盖」,O_EXCL 把那类静默覆盖
变成显式失败。这几条我自己逐条核过——那份调研的 subagent 承认它编过一句「我抽查过了」。

migrations/dissect.md 登记两条:运行标识要带齐现在文件名里那五维,以及这份意图日志和它那份
逐步轨迹是两样东西不要混。
2026-08-10 03:38:31 -04:00
iomgaa c5c1e68706 docs(design): 按两轮硕士生冷读改 0009 与 0010
0009 最实的一条是文档与代码对不上:文档说那条扫描测试断言 __dataclass_params__.kw_only,
而代码实际查的是构造签名。冷读的人正确地指出前者守的范围更窄——哨兵写法 KW_ONLY 达成了同样
的效果但那个标志是假(会误杀),而 kw_only=True 的类里用 field(kw_only=False) 开口子那个
标志仍然是真(会漏掉)。文档改成描述实际做法并写清楚这条理由。另修:「长六个字符」实际是
五个;补上两种写法的区别、Pydantic AI 那条「超过一个位置参数」与本文「一个都不许」的差别
来自处境不同、规则作用域只管数据类(NamedTuple 与 pydantic 绕得过,是已知边界)、
以及 §1.3「新增字段必带默认值」为什么不受影响。

0010 最实的一条是论据自相矛盾:决策二拿「跑通了的下游逐字一致」当段序依据,决策三又说那个
槽位在那边至今没实现、收到非空注入直接抛错。改成说准——它证明的是有人设计装配顺序时把槽位
放在了这里,不是这个位置在真实提示词里验证过。另补一节「读本文需要的几个名字」(注入/通道/
解释器/合成观察/四者同源/pi 全是首次出现即使用,而且「Skill 条目」「工件」「Injection」三个
说法指同一样东西却没有一处对上);承认库实际还定了消息边界的粒度,超出了「只管顺序槽位规模」
那句话,并给出这条线画在哪的判据;说明「变化频率」指的是跨运行而不是一次运行内部;补上规模
量出来给谁用、模板为什么在构造请求时就校验、以及为什么不回填解释出来的动作。
2026-08-10 03:15:46 -04:00
iomgaa 76495d9e39 fix(session,_recovery,types): 修 Codex 对抗审查报的五条
逐条核实全部成立。最重的是第一条:停止判定的结果只存在于结束记录里,而步记录与结束记录是
两次写。崩在两者之间那次判定就丢了,恢复照常回到预算准入——一次「恰好用满预算完成」被改写
成「预算耗尽」(两条轨迹长度一模一样),一次已经达成目标的运行接着往下跑,一次该以连续解析
失败收尾的运行再花一次模型调用。修法是续跑时把那次判定重演一遍:判定要的东西全在日志里
(动作结果在步记录那条原子写里、工具名在步记录上、模型回复在模型调用结果里)。最终回答那
一档更麻烦,答案文本只存在于结束记录里,靠重新解释那条已存下来的回复找回来——那时副作用还
没发生,重新解释是安全的。五种收尾各一条测试。

其余四条:
- 模型调用失败步在续跑时补写,prompt_chars 填了 0。重新装配出来的和被打断时是同一份,
  照它算。填 0 是把轨迹里那一列改写成假值。
- 取消正好落在写运行开始记录那一下时不留取消标记,留下一份只有开始记录的日志:run 因标识
  已存在而拒绝、resume 当成可以从第 0 步续跑。用 shield 让那条记录一定落地,取消结束记录
  才有地方挂(反过来先写结束记录会拼出结构上说不通的日志)。
- ModelCallResult 能同时带 reply 和 failure,而恢复只看 reply is None,于是把一次失败的
  调用当成成功、接着解释那段回复执行动作。加构造期不变量。
- call_id 空串一路能进持久化记录。docstring 里那句「绝不能是空串」原本没有任何东西守着。

顺带把主循环里重复的一次上下文装配去掉,并把只在续跑第一次迭代成立的那个分支挪出循环。
2026-08-10 03:12:42 -04:00
iomgaa 0132887cb9 feat(session): 落成主循环、两个装配对象与两条入口
A 到 H 八档由这里唯一执行,每档的判定住在 _stopping。三处容易写错的地方都有测试钉着:
恰好用满预算完成记成目标达成而不是预算耗尽(结算在下一次迭代开头);规模超限不写步也不写
意图(唯一一种真的一步都没走的终止);未执行与环境故障两档的观察取库合成的那段,执行器
给的不进历史。

写入序列断言成一条流水:模型意图 → 模型结果 → 动作意图 → 步记录,模型调用失败也落一条
结果记录(不落的话恢复会把一次已知的失败判成状态未知走重放)。结果 ID 从运行标识与序号
推出来而不是随机数——重放同一步拿到同一个 ID,轨迹里也少一列每次都不同的值。

写这块时修掉的两个自己的坑:
- 重放时会重复写意图,而同一步两条同种意图被 _recovery 判成「日志被并发写过」,一次成功的
  重放反倒把日志弄坏。加了两个一次性开关跳过已经落过盘的那条。
- _recovery 数连续解析失败时把模型调用失败那一步也算进去了。它压根没走到解释器,判据改成
  「解释器给了一段回喂文本」——解析失败必定带着那段说明,模型调用失败没有。

取消:CancelledError 原样重抛、run 不返回结果,但结束标记要在宽限期内尽力写下去,写不完
只记日志不再抛(再抛会把取消这件事本身盖掉)。并发隔离:全部可变状态住在每次运行一个的
_Driver 里,定义与请求都是 frozen 的,有一条并发跑两次的测试。

Budget 加了构造期校验(四项都必须为正):零或负数会产出一次「零步、预算耗尽」的运行,
那和一次真的跑满上限的运行在停止原因上完全一样,混进统计里分不出来。

事件出口收下了但没有调用点——Event 还没有字段,发一条内容为空的事件既没用又会变成一份
要兼容的形状。
2026-08-10 02:59:34 -04:00
iomgaa 2367d3afbc feat(_assembly): 落成段序与注入槽;design 0010 删掉工具段模板
0010 取代 0006 决策三里 tool_section_template 那一行。三条理由:两个真实消费者都是项目侧
自己把工具清单拼进上下文的(一个塞在 run 级模板里伪装成示例演示的一次执行输出,另一个的
render_tool_docs 全仓零生产调用方);留着它的话迁移的人会去填,填完提示词里有两份工具清单,
而那个下游的验收标准是轨迹逐字段可比、变了还不报错;参考框架 pi 的内核同样不渲染,它的应用层
虽有 Available tools 段,但每行来自与 description 分开的 promptSnippet 字段——说明就算要渲染,
那段文字也不该是从校验用的 schema 生成的。四者同源不受影响,schema_for_model() 还在。

段序照唯一跑通了的下游:run 级片段 → 注入槽 → 目标级片段 → 逐步的模型输出/观察交替,
按变化频率从低到高排(供应商按前缀缓存计费)。库不合成任何 system 消息——有一个下游全程
没有 system 消息,加一条它的提示词会凭空多出一段。

注入槽每条一条 USER 消息、正文原样、不加任何分隔符(分隔符是渲染格式,归项目侧),通道按
名字排序(映射的迭代顺序取决于调用方怎么构造,用集合建出来的每进程都不同)。空注入等同有
测试守着。观察模板必须含占位符,缺了就报错——不校验的话每一步的观察会整个消失,而模型收到
的是一段看起来完全正常的固定文本。规模度量逐块问,认不得的块类型直接失败而不是当成 0。

零业务假设扫描第三次抓到我自己(模块 docstring 里写了「实验因子」),已改成中性说法。
migrations/govdoc-saas.md 登记了「模型原生工具调用没有路」这个缺口。
2026-08-10 02:48:56 -04:00
iomgaa 7da5e07726 docs(design): 给 0008 与 0009 补上外部先例,两个决定都是印证不是改动
0008:主流框架没有一家把截断量做成工具返回值上的字段(Anthropic 的 tool_result 只有三个
字段,OpenAI/LlamaIndex/Pydantic AI 的结果类型都没有),唯一结构化记下来的 LangChain 走的
是通用 artifact 通道而不是专设字段。另补「将来要加时怎么加」的正反两例:Pydantic AI 在允许
返回的类型集合里加一个新类型、旧路径不动,零兼容问题;OpenAI Agents SDK 改成运行期形状
嗅探,一个返回 {"msg": ...} 的普通工具被当成图片输出转掉、线上 400,三天后回落。

0009:Pydantic AI 2026-07 加过一条几乎同形的 meta-test(扫描包、新增公共数据类超过一个位置
参数就失败),PR 正文两句正好对上本文两条论证——「一条老在评审里被提却没有机器执行的意见」,
以及「不给已有的数据类补,那会打断按位置构造的调用方;白名单只减不增,清空它得发新 major」。
它已经付了本文想避开的那笔账。另记一条连带影响:kw_only 字段不进 __match_args__。
2026-08-10 01:21:12 -04:00
iomgaa 6fafd95d6c feat(tools): 落成 executor() 与派生分发器;公共数据类一律只收关键字参数
两个自行调研后决定的问题,各自的证据写进了 design doc:

一、handler 返回 str 而不是带截断计数的小结构(0008 决策三,文末新增一节)。三条实据:
两个真实消费者的执行函数今天就返回纯字符串(GovDoc 的 handler 是 Coroutine[..., str],
dissect 的环境 execute 是 -> str);dissect 的 observation_truncated_chars 唯一的生产写入点
硬编码 0 且全仓零读取点,存在的是名字不是需求;reference/pi 是唯一把截断做完整的,它记的是
totalBytes/outputBytes/maxBytes 这组绝对量而不是一个差值——现在补 truncated_chars 补的
大概率是错形状,正是 scope.md 说的「猜出来的接缝比没有接缝更难拆」。

二、新增 design 0009:src/polyloop/ 下每个数据类都加 kw_only=True,另加一条扫描测试守它。
实验室七个仓库 223 个 dataclass 里 kw_only 出现零次,但那是默认行为不是选择。真正的证据是
PolyGateway:它的 LLMResponse 前 11 个字段顺序被三个下游的测试替身按位置构造锁死,模块
docstring 写着「字段顺序即公共承诺」,还得专门写一条 test_eleven_legacy_fields_positional
守着,从此再也插不进字段。那个约束不是它选的是它继承的,而本库还没有下游装上。
扫描测试查的是构造签名不是那个装饰器参数——要守的承诺是「按位置构造不了」。

executor() 在派生那一刻全查一遍实现,缺一个就报错,不拖到分发时才炸。RegistryExecutor 是
具体类而不是闭包,因为 RunRequest 要用 isinstance 认它。CancelledError 不被那个
except Exception 接住(它继承 BaseException),有测试守着。
2026-08-10 01:19:15 -04:00
iomgaa fa09e873c5 feat(_recovery): 落成四态判定与四个断点各自的续跑路径
一步之内写四次(模型意图 → 模型结果 → 动作意图 → 步记录原子写),所以断点有四个,
这个模块的全部工作就是把断点认出来:

- 模型意图有、结果无 → 状态未知,按请求声明的模型重放策略决定重调还是停下来报告
- 结果条目在但记的是失败 → 状态一点都不未知,补上那一步以模型调用失败收尾
  (判成未知走重放的话,一次已知的失败会被当成可能成功过)
- 回复已存、动作意图未写 → 副作用还没发生,重新解释那条回复,不再花一次调用的钱
- 动作意图有、步记录无 → 按意图记录里存下的那个策略决定;重放沿用原来那个预分配 ID,
  另分配一个的话原意图永远配不上结果,下次恢复读到的还是「状态未知」

四态表最后一行(结果有、意图无)判为日志损坏拒绝续跑,另加几种撞号与缺号:同一步两条
同种意图(并发写)、步序号不连续(中间某一步的原子写整个丢了)、意图跑到步记录前面、
有动作意图却没有模型调用意图、有记录却没有运行开始记录。

模块 docstring 里写明了一条此前只是隐含的对齐:步序号与模型调用序号是同一个数。没有它,
result_id 为空的步记录(解析失败、模型调用失败、最终回答三档)认不出属于哪一步。
2026-08-10 01:10:09 -04:00
iomgaa 4bf74bf5ac feat(_stopping): 落成停止判定四档,0008 转已接受
四个纯函数各管一档,顺序本身由主循环走:预算准入(A)、提示词规模(B)、连续解析失败(D)、
完成判定(G)。加一个不可变的三计数结构。

三处刻意钉死的边界,都写了测试:
- 两个预算上界同时耗尽报步数那一个。理由是兼容性不是原理(某下游按步数耗尽的占比告警),
  正因为推不出来才必须被测试钉死。
- 预算「达到即拦」用 >=,提示词规模「超过才拦」用 >。两处不是同一件事:一个数已经用掉
  几个额度,一个量一个东西有多大。
- 环境故障排在完成判定前面;未执行不做完成判定;完成信号恒为假不是故障(初稿在这里
  写错过,会让某个下游的每一次运行都在第一步终止)。

不 import polyloop.tools,五个逻辑层模块互不 import,所以「这次执行的工具被标了完成标记吗」
由调用方查好注册表传一个布尔进来。
2026-08-10 01:04:38 -04:00
iomgaa 956d98652d fix(tools): 修掉两个假阳性与一个改得动的内部状态,按两轮独立评审
代码审查(新鲜上下文,只给 diff 与验收标准)报了五条影响正确性的,逐条核实全部成立:

1. spec_for() 交出去的 parameters 就是注册表内部那份真字典。docstring 承诺的快照只挡住了
   「调用方改自己那份」,没挡住「从注册表取出来往里伸一层改」——而后者一下同时改掉模型
   看见的 schema 和校验用的 schema。改成逐层冻成只读视图,schema_for_model 出口再化回
   普通字典与列表。
2. {type: integer} 拒掉 3.0。JSON Schema draft-06 起小数部分为零的浮点数是合法整数,
   模型写 1e2 时 json.loads 给的就是 float。这是我自己在注释里点名最怕的那种假阳性。
3. additionalProperties: false 撞上 patternProperties 时拒掉一切匹配 pattern 的键。
   那些正是这份 schema 专门要收的键,模型改名也绕不过去。patternProperties 在场就跳过。
4. 工具名不校验类型、纯空白名放行。名字要落进发给模型的 schema,不是字符串会让整个请求
   被网关拒掉,报错指向请求体不指向注册表。
5. _by_name 是可变 dict,两条查询路径能被就地改到分岔。换成只读视图。

不可哈希那条不修,改在 docstring 里写明(参数 schema 是映射,注册表放不进 set)。
scope.md 的行号引用换成条目名——行号是最容易漂的一种参数,插一行就静默指错。

0008 按一轮硕士生冷读重写:字段位置那段原来自相矛盾(一边说位置是公共承诺、插在中间会
静默改掉后面字段的含义,一边就插在中间,且没讨论追加在末尾这个同一判据下的显然选项),
改成追加在末尾并说明规则;补上四个名字的就地解释(重放策略、两条完成通路、动作结果五个
字段、restrict_to);决策四那张表原来只有三列却被正文说成填五个字段,恒定的两个单列出来
并各自给了理由;补上 validate 不通过为什么算未执行、executor() 为什么全查、为什么必须是
具体类而不是闭包、异常栈去哪了。
2026-08-10 00:57:36 -04:00
iomgaa 6abe13abb1 feat(serialization): 落成记录的编解码与 schema 版本校验
一个 encode 对七个 decode_*。编码不需要知道目标类型(对象自己知道),解码需要
(一个字典什么都不知道),不对称是这个原因。

载荷就是记录类的字段,没有任何元信息键——没有类型标签、没有时间戳。哪一行是哪种记录由
存储实现自己解决,库不替它定文件布局;多塞一个键会让步记录的载荷不再和迁移前那份逐行
轨迹同形,而那边的验收标准是逐字段可比。

解码三条:带版本的三种记录(运行开始、步记录、运行结果)版本必须在场且认得,缺了或者
认不得都失败;记录类上的每个字段都必须在载荷里,有默认值的也一样(默认值补齐会把
「这件事没发生过」改写成「发生了但值为空」);多余键忽略(存储常要在同一个字典里塞
自己的东西,把它整个递回来解码是最自然的写法)。

第二条的直接后果写进了模块 docstring:往持久化结构里加字段必须同时抬 schema 版本。
CLAUDE.md §1.3 那条「新增字段必带默认值」管的是 Python 构造器,持久化这一侧由 §1.4 管。

SchemaVersionError 与 DecodeError 分开:一个说去升级库,一个说去查数据。压成一个的话,
一次例行升级漏做会被读成数据损坏,然后有人去修数据。
2026-08-10 00:52:01 -04:00
iomgaa 30895cd306 feat(tools): 落成工具规格与注册表,四者同源由同一个实例驱动
ToolSpec 五个字段加构造期校验(空名字、非映射 parameters),parameters 深拷贝一份存下来
——不拷贝的话调用方在别处改那个字典会连带改掉模型看见的 schema,而那次修改没有任何地方
记录得到。

ToolRegistry 是不可变值对象:注册顺序保留(工具清单要贴进提示词,而 restrict_to 收到的
常常是 set,照集合顺序输出会让同一份配置在不同进程里渲染出不同提示词);相等按内容判
不按身份判,供 RunRequest 那条一致性校验用;restrict_to 遇到不认识的名字直接报错,
不静默丢弃。

validate 校验的是 JSON Schema 的一个子集(必填键、additionalProperties: false 时的多余键、
顶层 type),子集边界写在 docstring 里。完整校验只能靠第三方库,而公共签名上不许出现
第三方类型。剩下那部分由工具自己报错,那是一条正常观察。

executor() 没写,缺口见 design/0008(待确认,要过人类门)。
2026-08-10 00:45:53 -04:00
iomgaa 77ffad9dc4 docs(design): 落成 0008,补上工具实现挂在哪里这个洞
0006 决策六定的 ToolSpec 五个字段里没有一处装工具本身的实现,注册表派生的执行器
拿到一次合法调用之后无处分发。handler 这个词只在 0003 的正文里出现过,从没写成字段。

本文定四件事:实现挂在 ToolSpec 上(不另开清单,防漂移)、可以为空但 executor()
那一刻就查、签名是收参数返回观察文本的协程、派生执行器怎么填 ActionOutcome 五个字段。
状态待确认,要过 CLAUDE.md §2 人类门。
2026-08-10 00:45:40 -04:00
iomgaa c9f10973ff feat(types,ports): 落成公共值类型与五个接缝的 Protocol
第 ⑤ 阶段第一块,按 0006 与 0007 落地。从依赖图的底部开始写:
这两个模块定错了上面全得跟着改。

types 装 22 个公共值类型、枚举与持久化记录。StepCompleted 带一条构造期校验——
result_id 为空当且仅当 action_outcome 也为空,两者一空一有值是结构上说不通的,
放行它等于把「有动作结果却没有意图」这档日志损坏当成合法完整步接受。
校验用显式异常不用 assert,因为 -O 会把断言整条剥掉,下游用 -O 跑的那天守卫就静默消失。

ports 装五个 Protocol 与四个只在调用往返之间存在的壳。每个接缝多一个同步
parameters(),参数快照靠它聚合——原先没有这个方法,那个快照根本调不动。
RunLog 五个字段全带默认值,所以「读一个从没写过的运行标识返回空日志」就是 RunLog()。

零业务假设那条测试第二次抓到我:StopReason 的 docstring 里写了下游的实验词汇,
另有四处直接点名下游项目。库源码点名下游本身就是 §1.1 禁的——三个下游领域互不相交,
一个业务词进来就是替其中一个做了另外两个不需要的假设。已改成中性说法加 design doc 指向。

import-linter 从此不再是平凡的绿:Analyzed 15 files, 7 dependencies,
ports → types 那几条边真的被检查了一遍,之前是 0 dependencies。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:01:24 -04:00
iomgaa f8e02290f4 docs(design): 落成 0006 与 0007,公共 API 的名字、签名与接缝行为
0006 定「叫什么、什么形状」:五个接缝的 Protocol 名与签名、公共类型的英文名与
字段清单、类型分到 types / ports / tools 三个模块的判据。
0007 定「同一个签名下什么算对」:三个动作状态的触发条件、动作被拒绝时观察由库
合成而不取执行器那段、解释器不许抛异常、read_log 读不存在的运行返回空日志。
两份拆开是因为后者的权威处按 §0 是 tests/contract/,design doc 只记当初为什么这么定。

这两份改动了 0003 四处,全部在文首登记:记录集合是六种东西不是五类;
参数视图是方法不是字段;预算是四项不是两个计数;ports 装「Protocol 与它们的
入参/返回结构体」那半句写不出来——照它写 types 会反向依赖 ports。
四处全是「把字段类型逐个写出来」这个动作本身逼出来的,纯读文档看不见。

四轮评审:两轮硕士生冷读报了约 45 条,两轮 Codex 对抗审查报了 13 条,
逐条核实后基本全部成立并修完。最后一轮是唯一一次契约测试与文档互相抓到对方的错——
文档改了方法名测试没跟,测试把 dissect 的动作语言写死成输入会误杀 GovDoc 的实现。

结论回写 architecture.md:第七节补类型归属判据,第八节改 ports 那一行,
第九节补五个 Protocol 的英文名,第十四节把「英文名还没定」那条缺口换成指向;
决策索引加两行。字段表刻意不回写——按 §0 那是代码的权威。
CLAUDE.md 与 README.md 开头的「一次 Agent Session」是术语漂移,改成「一次运行」。

CLAUDE.md §7 加两条工作方式:能压成一段结论的活尽量交给 subagent、
委托出去的活交证据不交判断;以及持续往下做,只在人类门和真判断不了的岔路停。
§8 那句「讲完停下来等回应」与后者打架,收窄到只管说话方式。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 23:56:38 -04:00
iomgaa a14bf8d288 test(contract): 落成五个接缝的契约骨架,用它逼出七个设计洞
第 ④ 阶段的核心交付物。这套测试不针对任何具体实现,写的是「不管你怎么实现
都必须满足这些行为」,下游写完自己的实现接到 fixture 上跑一遍即可,
是任何新适配器的准入标准(CLAUDE.md §0)。

现在全部跳过,因为公共类型与 Protocol 还没落地。价值不在跑,在写:
写一条契约要求把每次调用逐字写出来——方法叫什么、参数填什么、返回值怎么取,
而散文里读着通顺的地方,落到这一步就露出来了。

三轮文档评审没报出的七个洞,写这套测试时全部撞了出来:
没有动作的步 result_id 填什么;三个 ActionStatus 取值的触发条件;
动作被拒绝时观察的来源(执行器与 SyntheticObservations 两处都有);
解释器能不能抛异常;Event 没有字段所以事件出口的契约只写得出一半;
read_log 读不存在的运行必须返回空日志而不是抛异常;
以及原子性与前缀持久性这两条 0005 的承诺根本没有机器兜底——
写这套测试之前我们默认它们会被契约测试接住。

洞标成 xfail(strict=True) 而不是常驻 fail:一个永远红的套件会训练所有人忽略红。
它们都不带 fixture,否则会被「实现还没有」那个跳过挡住,
于是「答不上来」就伪装成了「还没轮到」。补上之后 XPASS 会报错,逼人回来删标记。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 23:37:13 -04:00
iomgaa e017160c45 build(repo): 落成工具链、依赖契约与十个模块的空骨架
第 ③ 阶段剩下的那半:架构文档之外,import-linter 契约也落地了。

pyproject.toml 把九条依赖规则里的七条写成五条 import-linter 契约。
分层用一条 layers 契约表达规则 1、2、3、8,`|` 表示同层互不 import;
另外四条 forbidden 分别管下游反向 import、polygateway 的唯一入口、
以及三个纯逻辑模块不碰 asyncio 与 pathlib。
契约不是平凡的绿:故意注入两处违规验证过,都被点名到行号。

先建十个模块的空包,是为了避开 PolyGateway bootstrap 期那段 Makefile 门控——
它当时没有包,lint-imports 报 module not found 而红,只好加一段跳过逻辑。
空包让契约从第一天就真的在跑。

剩下两条规则落不进契约,写成了 tests/unit 下的测试:
规则 6「types 与 ports 不许 import 任何第三方」判据要反过来写(只许标准库和自己),
规则 9「import polyloop 之后 sys.modules 里没有 polygateway」是运行时事实。
另加硬约束 §1.1 零业务假设的黑名单扫描——它第一次跑就抓到 _assembly 的 docstring
里写了 dissect 的业务词,已改。三个扫描类测试都带 fail-closed 守卫,
防止目录搬走之后扫到空列表安静地绿。

工程约定取自实验室已有项目:setuptools + src layout、ruff 十一项 select、
line-length 100 来自 PolyGateway;dev 工具链版本钉死、--strict-markers 与
--import-mode=importlib 来自 CHSAnalyzer 与 dissect 踩过的坑,各自的理由写在配置注释里。
e2e 默认不跑,它打真实网关要花钱。

CLAUDE.md §0 那句「契约还没写,要等 src/ 落地」已过期,改掉;
README 勾掉第 ②③ 阶段,补上本地检查命令与 GovDoc-Editor 那一行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:28:59 -04:00
iomgaa 766f7a0290 docs(design): 落成 0005,修订存储接缝的原子写与步记录三处
第二轮 Codex 对抗审查报的五条全部核实成立,0003/0004 已冻结,修订新写一份。
五条都不改任何决策的结论,改的是结论到形状之间那一段的落实。

决策一:步记录加一个布尔的完成信号列。0004 决策二承诺靠它区分两条完成通路,
而决策四的字段表里没有这个字段。它自己就够区分,不必查注册表——
事后分析手上常常只有轨迹文件。
决策二:存储接缝六个方法重切一刀,把「按预分配 ID 写一条结果」拆成
「写模型调用结果」与「写动作结果与步记录」。原子性从散文变成签名里不可表达其他形态。
决策三:观察那一列的口径改成「回填进历史的那段文本」,不加字段。
dissect 的 render_messages 把 step.observation 原样套模板发出,
raw_output 也是解析器截断后的版本——两侧同构,都是一段文本加一个数字。
决策四:作废 0004 词表里「一个可能取不到的完成信号」,那是上一轮 fatal bug 的原话。
决策五:存储接缝加前缀持久性。少了它,一次明明成功的模型调用会被恢复
记成「状态未知」,而它成功的证据就在同一份日志里。

结论已回写 architecture.md 第九节与决策索引;migrations/dissect.md 需求七
补上「分开记的是文本加数字,不是两段文本」,堵掉这次审查里出现过的误读。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:02:38 -04:00
iomgaa 4f8812fa82 docs(design): 落成边界、续跑、公共 API 形状与停止语义四份决策
第 ② 阶段需求对齐与第 ③ 阶段架构的产出,代码尚未开始。

design/0001 定边界判据:三道测试(时机 / 信息 / 性质)全过才在界内,
外加「只认接缝、不认接缝后面是什么」与不夺走下游实验因子的排除条款。
design/0002 定步级续跑:不承诺原子性,承诺绝不静默丢失与不替工具猜幂等性;
先写意图再执行、结果 ID 预分配、重放策略由工具声明且默认绝不重放。
design/0003 定公共 API 形状:单一入口两个动词、五个接缝、三个伪接缝的排除理由、
分层与九条依赖规则。design/0004 定停止判定顺序、十个停止原因取值与步记录字段表。
0003 与 0004 需过 CLAUDE.md §2 人类门,已由项目负责人确认,状态转为已接受。

explanation/scope.md 与 explanation/architecture.md 是这四份决策的常青回写,
分层与模块边界的权威在 architecture.md,将来由 import-linter 契约机器断言。
migrations/ 下 dissect 是唯一的硬迁移验收,govdoc-saas 只做设计级对齐。

三道闸都过了:14 agent 对抗辩论定骨架,两轮硕士生阅读报的 30 余条已修完,
Codex 对抗审查抓出的两条致命问题(提交型完成被误判成环境故障、
崩溃恢复漏一个状态)已修,修完的形状还没送 Codex 复审。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:48:33 -04:00
iomgaa a2e94318b9 chore(repo): 建仓,落成协作规范与文档骨架
协作方式以 CHSAnalyzer 为蓝本,按「库」这个身份改写:

- CLAUDE.md §0 的权威表换成公共 API 契约、公共类型、下游迁移三条主线,
  数据库 schema / HTTP 契约 / alembic 迁移在本项目不存在,整体删去。
- §1 新增四条库特有的硬约束:字段只增不删不改名、持久化 schema 走显式版本、
  不反向 import 下游、发布必须走完整流程(PolyGateway 有两个版本只 bump
  没上传,registry 长期停在旧版且无人发现)。
- §3 的四类 Codex 对抗审查按同一判据重定:公共签名、停止判定与预算结算、
  取消传播与 Session 隔离、schema 演进。共同点是错了不会当场炸。
- research-wiki 在常青层加第四类 migrations/,因为本项目的核心验收标准就是
  能否搬回 dissect、能否替代 GovDoc-SaaS 的 docagent-core/,塞进 guides/
  会让它看起来像可选工序。

reference/ 不入库:五个仓库各带一个 .git、合计约 96MB,提交进来会变成一堆
不可用的嵌套仓库。它也不是任何事实的权威,agent-core.md 同理。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:02:09 -04:00