@migor/agentia 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +688 -4
- package/README.md +1 -1
- package/dist/AGENTS.md +313 -33
- package/dist/container/container.js +15 -0
- package/dist/container/container.js.map +1 -1
- package/dist/core/limits.d.ts +93 -13
- package/dist/core/limits.js +103 -13
- package/dist/core/limits.js.map +1 -1
- package/dist/core/line-framing.d.ts +36 -0
- package/dist/core/line-framing.js +39 -0
- package/dist/core/line-framing.js.map +1 -0
- package/dist/core/message.d.ts +11 -0
- package/dist/core/run.d.ts +68 -4
- package/dist/core/run.js +27 -1
- package/dist/core/run.js.map +1 -1
- package/dist/core/schema.js +59 -8
- package/dist/core/schema.js.map +1 -1
- package/dist/core/timeout.d.ts +43 -1
- package/dist/core/timeout.js +76 -3
- package/dist/core/timeout.js.map +1 -1
- package/dist/core/tool.d.ts +25 -1
- package/dist/core/trace.d.ts +40 -1
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -1
- package/dist/engine/defer.d.ts +49 -0
- package/dist/engine/defer.js +72 -0
- package/dist/engine/defer.js.map +1 -0
- package/dist/engine/forwarded.d.ts +14 -5
- package/dist/engine/forwarded.js +4 -1
- package/dist/engine/forwarded.js.map +1 -1
- package/dist/engine/loop-result.d.ts +65 -7
- package/dist/engine/loop-result.js +24 -5
- package/dist/engine/loop-result.js.map +1 -1
- package/dist/engine/loop.d.ts +2 -0
- package/dist/engine/loop.js +157 -17
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/mcp-server-http.d.ts +2 -0
- package/dist/engine/mcp-server-http.js +215 -0
- package/dist/engine/mcp-server-http.js.map +1 -0
- package/dist/engine/mcp-server-stdio.d.ts +15 -0
- package/dist/engine/mcp-server-stdio.js +106 -0
- package/dist/engine/mcp-server-stdio.js.map +1 -0
- package/dist/engine/mcp-server.d.ts +134 -0
- package/dist/engine/mcp-server.js +232 -0
- package/dist/engine/mcp-server.js.map +1 -0
- package/dist/engine/menu-drift.d.ts +34 -0
- package/dist/engine/menu-drift.js +81 -0
- package/dist/engine/menu-drift.js.map +1 -0
- package/dist/engine/policy.d.ts +2 -1
- package/dist/engine/policy.js +7 -1
- package/dist/engine/policy.js.map +1 -1
- package/dist/engine/replay.d.ts +2 -1
- package/dist/engine/replay.js +3 -2
- package/dist/engine/replay.js.map +1 -1
- package/dist/engine/resume-input.d.ts +10 -0
- package/dist/engine/resume-input.js +11 -0
- package/dist/engine/resume-input.js.map +1 -1
- package/dist/engine/run-config.d.ts +26 -2
- package/dist/engine/run-config.js +60 -0
- package/dist/engine/run-config.js.map +1 -1
- package/dist/engine/spec.d.ts +33 -4
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/tool-context.d.ts +10 -5
- package/dist/engine/tool-context.js +5 -1
- package/dist/engine/tool-context.js.map +1 -1
- package/dist/engine/tracer.d.ts +43 -5
- package/dist/engine/tracer.js +73 -10
- package/dist/engine/tracer.js.map +1 -1
- package/dist/engine/trimming.js +28 -2
- package/dist/engine/trimming.js.map +1 -1
- package/dist/engine/turn.d.ts +60 -3
- package/dist/engine/turn.js +180 -71
- package/dist/engine/turn.js.map +1 -1
- package/dist/engine/types.d.ts +89 -3
- package/dist/engine/types.js +38 -2
- package/dist/engine/types.js.map +1 -1
- package/dist/eval/export.d.ts +48 -0
- package/dist/eval/export.js +146 -0
- package/dist/eval/export.js.map +1 -0
- package/dist/eval/harvest.js +7 -2
- package/dist/eval/harvest.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/adapter-options.d.ts +22 -0
- package/dist/integrations/adapter-options.js +51 -0
- package/dist/integrations/adapter-options.js.map +1 -1
- package/dist/integrations/anthropic.d.ts +4 -2
- package/dist/integrations/anthropic.js +27 -27
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/file-sink.d.ts +20 -0
- package/dist/integrations/file-sink.js +27 -0
- package/dist/integrations/file-sink.js.map +1 -0
- package/dist/integrations/mcp-http.d.ts +3 -3
- package/dist/integrations/mcp-http.js +1 -1
- package/dist/integrations/mcp-http.js.map +1 -1
- package/dist/integrations/mcp-protocol.d.ts +115 -0
- package/dist/integrations/mcp-protocol.js +69 -0
- package/dist/integrations/mcp-protocol.js.map +1 -0
- package/dist/integrations/mcp-stdio.d.ts +3 -3
- package/dist/integrations/mcp-stdio.js +17 -3
- package/dist/integrations/mcp-stdio.js.map +1 -1
- package/dist/integrations/mcp.d.ts +6 -95
- package/dist/integrations/mcp.js +24 -66
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/metrics-otlp.js +52 -7
- package/dist/integrations/metrics-otlp.js.map +1 -1
- package/dist/integrations/metrics-render.d.ts +15 -0
- package/dist/integrations/metrics-render.js +79 -2
- package/dist/integrations/metrics-render.js.map +1 -1
- package/dist/integrations/metrics-state.d.ts +99 -1
- package/dist/integrations/metrics-state.js +128 -4
- package/dist/integrations/metrics-state.js.map +1 -1
- package/dist/integrations/metrics.d.ts +62 -6
- package/dist/integrations/metrics.js +49 -8
- package/dist/integrations/metrics.js.map +1 -1
- package/dist/integrations/openai.js +9 -31
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/otlp.d.ts +3 -3
- package/dist/integrations/report.d.ts +7 -0
- package/dist/integrations/report.js +20 -1
- package/dist/integrations/report.js.map +1 -1
- package/dist/runtime/memory.d.ts +61 -1
- package/dist/runtime/memory.js +47 -7
- package/dist/runtime/memory.js.map +1 -1
- package/dist/runtime/run.d.ts +12 -2
- package/dist/runtime/run.js +112 -15
- package/dist/runtime/run.js.map +1 -1
- package/dist/store/fsStore.js +10 -4
- package/dist/store/fsStore.js.map +1 -1
- package/dist/store/record.d.ts +41 -0
- package/dist/store/record.js +54 -0
- package/dist/store/record.js.map +1 -0
- package/dist/store/redisStore.d.ts +3 -1
- package/dist/store/redisStore.js +21 -7
- package/dist/store/redisStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +23 -0
- package/dist/store/sqliteStore.js +103 -8
- package/dist/store/sqliteStore.js.map +1 -1
- package/dist/store/store.d.ts +65 -2
- package/dist/store/store.js +5 -2
- package/dist/store/store.js.map +1 -1
- package/dist/toolkit/capability-cycles.d.ts +50 -0
- package/dist/toolkit/capability-cycles.js +88 -0
- package/dist/toolkit/capability-cycles.js.map +1 -0
- package/dist/toolkit/capability-slice.d.ts +86 -0
- package/dist/toolkit/capability-slice.js +98 -0
- package/dist/toolkit/capability-slice.js.map +1 -0
- package/dist/toolkit/module.d.ts +7 -1
- package/dist/toolkit/module.js +65 -32
- package/dist/toolkit/module.js.map +1 -1
- package/dist/transport/approval-policy.d.ts +1 -1
- package/dist/transport/approval-policy.js +8 -4
- package/dist/transport/approval-policy.js.map +1 -1
- package/dist/transport/async.d.ts +197 -18
- package/dist/transport/async.js +871 -85
- package/dist/transport/async.js.map +1 -1
- package/dist/transport/drain-gate.d.ts +6 -2
- package/dist/transport/drain-gate.js +14 -2
- package/dist/transport/drain-gate.js.map +1 -1
- package/dist/transport/http-endpoints.d.ts +151 -0
- package/dist/transport/http-endpoints.js +480 -0
- package/dist/transport/http-endpoints.js.map +1 -0
- package/dist/transport/http-io.d.ts +66 -0
- package/dist/transport/http-io.js +145 -0
- package/dist/transport/http-io.js.map +1 -0
- package/dist/transport/http-route.d.ts +10 -3
- package/dist/transport/http-route.js +18 -3
- package/dist/transport/http-route.js.map +1 -1
- package/dist/transport/http-shapes.d.ts +11 -0
- package/dist/transport/http-shapes.js +31 -1
- package/dist/transport/http-shapes.js.map +1 -1
- package/dist/transport/http.d.ts +22 -35
- package/dist/transport/http.js +98 -476
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/owner-id.d.ts +29 -0
- package/dist/transport/owner-id.js +46 -0
- package/dist/transport/owner-id.js.map +1 -0
- package/dist/transport/owner-liveness.d.ts +8 -0
- package/dist/transport/owner-liveness.js +56 -0
- package/dist/transport/owner-liveness.js.map +1 -0
- package/dist/transport/resume-policy.d.ts +48 -10
- package/dist/transport/resume-policy.js +25 -10
- package/dist/transport/resume-policy.js.map +1 -1
- package/dist/transport/scheduler.js +9 -23
- package/dist/transport/scheduler.js.map +1 -1
- package/dist/transport/slot-pool.d.ts +9 -1
- package/dist/transport/slot-pool.js +9 -1
- package/dist/transport/slot-pool.js.map +1 -1
- package/dist/transport/task-events.d.ts +34 -3
- package/dist/transport/task-events.js +86 -5
- package/dist/transport/task-events.js.map +1 -1
- package/dist/transport/wake-policy.d.ts +50 -0
- package/dist/transport/wake-policy.js +43 -0
- package/dist/transport/wake-policy.js.map +1 -0
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,638 @@
|
|
|
5
5
|
(0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
|
|
6
6
|
决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.9.5] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
> 本版主题(窗口 `0.9.4 → 0.9.5`):**外部深评 P2 表的最后三条收口(K5 / T4 / K2)+ 两处「报告的判据要订正」**。
|
|
13
|
+
> 三块的共同点都是**把静默换成有声**:① 记忆回写从此**能发现**并发丢写(可选 CAS:冲突时**不写 + 出声**,
|
|
14
|
+
> 不替你合并);② 挂着的任务不再各占一份 ≈1 MB 的事件缓冲(新旋钮按**条数**配额,回收的是**缓冲**、
|
|
15
|
+
> 留表项与序号 ⇒ 读者先收一帧 `stream.truncated` 再转实时,而不是被谎报成「别的进程」);
|
|
16
|
+
> ③ 加第五类能力从「改五处按类分支、漏一处不报错」变成「给注册表加一行,不表态就 `tsc` 报 `TS2741`」。
|
|
17
|
+
> **无破坏性变更**:新加的两个能力都是**可选**的(不实现 ⇒ 行为与 0.9.4 逐字相同),
|
|
18
|
+
> `AsyncRunner.streamNonTerminalBuffers` 的缺省值只影响「回放看不全」那一档(且看得见)。
|
|
19
|
+
> 既有使用者**不需要任何动作**。
|
|
20
|
+
|
|
21
|
+
### 重构 · 四类能力的装配差异收进一张注册表(外部深评 K2)(2026-09-29 ③)
|
|
22
|
+
|
|
23
|
+
来源:`DEEP-AUDIT-VERIFIED-2026-09-28.md` 的 P2 表 `K2`。性质不是「代码不好看」:加第五类能力
|
|
24
|
+
此前要在 `toolkit/module.ts` 改**五处按类分支**(收集 / 可用名单 / `buildSlice` 展开 /
|
|
25
|
+
`@Prompt` 版本表 / 孤儿能力计数),漏一处只表现为「那类能力静默不进菜单 / 不进版本表 /
|
|
26
|
+
不计入孤儿告警」—— 构建与全部用例照样全绿。
|
|
27
|
+
|
|
28
|
+
- **新增 `src/toolkit/capability-slice.ts`**:`CapabilityPayloads` 定义「一类能力」是什么,
|
|
29
|
+
`KIND_SPEC` 的类型是映射类型 `{[K in CapabilityKind]: CapabilityKindSpec<K>}` ⇒
|
|
30
|
+
**加了 payload 成员却不给表项,`tsc` 报 `TS2741`**(与 `SUCCESS_STOP_REASON` /
|
|
31
|
+
`TERMINAL_STATUS` 同款护栏);`CAPABILITY_KINDS` **从表的键派生** ⇒ 五处遍历自动覆盖。
|
|
32
|
+
- **`module.ts` 的五处按类分支换成对表的遍历**(`collectCapabilities` / `capabilityToolNames` /
|
|
33
|
+
`buildCapabilitySlice` / `capabilityCount` / `capabilityVersions`)—— 行为零变更
|
|
34
|
+
(既有 toolkit 用例 142 条 + 全套件 1597 / 195 / 27 全绿即证据)。
|
|
35
|
+
- **两处刻意留在表外并写明理由**:菜单重名校验(跑在合并后的工具列表上,与类别无关)、
|
|
36
|
+
能力引用图(只有 `@SubAgent` / `@Skill` 有 `tools` 引用 —— 语义不对称,不是漏项)。
|
|
37
|
+
- 唯一的类型擦除集中在 `specOf()`(相关联合,TS#30581 的已知限制),安全性由「每行在定义处
|
|
38
|
+
逐个收窄 + 键集从表派生 + 逐类驱动用例」共同兜住 —— 不是靠这一句断言。
|
|
39
|
+
|
|
40
|
+
### 修复 · 非终态流的缓冲配额(外部深评 T4)(2026-09-29 ②)
|
|
41
|
+
|
|
42
|
+
来源:`DEEP-AUDIT-VERIFIED-2026-09-28.md` 的 P2 表 `T4`。报告当日判据不完整(单流**条数**上限
|
|
43
|
+
从 #115 起就在),**仍然成立**的是那半:**非终态流的缓冲只增不减** —— 每任务 ≤`streamBufferEvents`
|
|
44
|
+
(500) 条 ≈ 1 MB,而等审批 / 等事件 / 等定时器的任务**永远走不到终态** ⇒ 挂着的任务越多内存越大。
|
|
45
|
+
|
|
46
|
+
- **`TaskEventStreams` 新增 `nonTerminalBuffers`(缺省 32 条流)**,并经
|
|
47
|
+
**`AsyncRunner.streamNonTerminalBuffers`** 暴露给宿主(`0` = 不为没人读的非终态流留回放缓冲)。
|
|
48
|
+
超出配额即从**最旧且无订阅者**的非终态流开始**回收缓冲**。
|
|
49
|
+
- **回收的是缓冲,不是表项**(这是本次对报告判据的订正):表项留着当**桩**(`has()` 仍为真、
|
|
50
|
+
`nextIndex` 接着走),只把回放历史丢掉并用 `firstAvailable` 明示缺口 ⇒ 连上来的读者先收到
|
|
51
|
+
一帧 `stream.truncated` 再转实时,**不会**被谎报成 `stream.unavailable`(那是「别的进程」的台词,
|
|
52
|
+
而任务就在本进程跑着)。删表项会**静默破坏**「序号是流自己的」那条设计约束:恢复段重开
|
|
53
|
+
`nextIndex = 1` 的流,拿 `Last-Event-ID` 续订的读者从此永远筛不出东西。
|
|
54
|
+
- **代价如实**:① 正在被订阅的流**不受配额约束**(不抽走正在读的数据)⇒ 同时被读的流很多时
|
|
55
|
+
缓冲总量仍会超配额;② 每条非终态任务留一个桩(约百字节),桩本身不设上限(同因);
|
|
56
|
+
③ 「回放看不全」是**看得见**的降级(`stream.truncated` + `droppedBefore`),不是静默丢事件。
|
|
57
|
+
- `0` 语义登记进 `core/limits.ts`(`disabled`),探针在 `tests/limits.test.ts`(走公共旋钮,
|
|
58
|
+
两处接线一起驱动)。
|
|
59
|
+
|
|
60
|
+
### 变更 · 记忆回写的 CAS(可选、成对、冲突出声)(2026-09-29 ①)
|
|
61
|
+
|
|
62
|
+
来源:`DEEP-AUDIT-VERIFIED-2026-09-28.md` 的 P2 表 `K5`(**未做**那三条之一)。
|
|
63
|
+
病灶:`flushMemory` 是无条件**全量读改写** —— 同 keys 的并发 run 之间是 last-write-wins,
|
|
64
|
+
后写的那条把先写的改动整片覆盖,**双方都看不出异常**。
|
|
65
|
+
|
|
66
|
+
- **`MemoryStore` 新增两个可选成员**(`runtime/memory.ts`):`loadWithRev(keys) → { values, rev }`
|
|
67
|
+
与 `saveIfRev(entries, rev) → { committed, reason? }`(`rev` 是**不透明**版本句柄,框架不解释、
|
|
68
|
+
只在本 run 回写时原样递回)。**成对实现**:只实现一个是装配错误 ⇒ `executeRun` **入口**抛
|
|
69
|
+
`TypeError`(半个 CAS 与「不支持」等效,却让「有没有被覆盖」看起来有据可查)。类型级守卫在
|
|
70
|
+
`tests/types/memory-surface.types.ts`(两个成员必须在**接口上**且**可选**)。
|
|
71
|
+
- **冲突时:不写 + 出声**(`runtime/run.ts`):`saveIfRev` 回 `committed !== true`(含忘返回
|
|
72
|
+
`undefined` —— 不知道写没写就不许记成写成功)⇒ 该 key 集**一个字都不写**并打一条
|
|
73
|
+
`console.warn`(含 keys 与 `conflict`)。**框架不合并、不重试** —— 「后写赢 / 逐键赢 / 按时间戳赢」
|
|
74
|
+
都是策略,属 store 与宿主;框架只负责把「丢了一条写」变成有声。回写被拒**不改 run 结局**
|
|
75
|
+
(仍 `succeeded`,与 sink 抛错同款)。代价如实:这一轮记忆**没落库**,重试配方见 `usage-guide` §6.5。
|
|
76
|
+
- **不支持 CAS 的 store:出声降级**。只有 `load` / `save` 时行为**一字不改**(照旧 last-write-wins),
|
|
77
|
+
但**第一次**用这个 store 实例时打一条**一次性**提示(按实例去重,不是每轮刷屏)。
|
|
78
|
+
框架**检测不了**没有版本号的覆盖 —— 版本号是唯一依据,写进 `usage-guide` §7 边界表。
|
|
79
|
+
- `InMemoryMemoryStore` 顺带实现这一对:版本是**整店一个单调计数**(粒度粗 ⇒ 宁可多报冲突,
|
|
80
|
+
也不漏报真实覆盖;`loadWithRev` 的返回形状 = `MemorySnapshot`)。
|
|
81
|
+
|
|
82
|
+
### 变更 · store 的可选能力进接口(S5)+ 两处如实口径(K1 / T6)(2026-09-28 ㉓)
|
|
83
|
+
|
|
84
|
+
- **`TaskStore` 增加两个可选成员**(`store/store.ts`):`compact?()` 与 `close?()`。
|
|
85
|
+
`FileTaskStore.compact()` / `SqliteTaskStore.close()` 此前只活在具体类上,宿主拿到接口类型时
|
|
86
|
+
只能 `as` 强转(换 store 后运行期才炸)。现在调用点写 `store.compact?.()` 即可。
|
|
87
|
+
**框架不替你调**:`drain()` 不关 store(可能是共享的,关掉是宿主的生命周期决定),压实同理归宿主。
|
|
88
|
+
类型级守卫在 `tests/types/store-surface.types.ts`(`npm run typecheck:types`)。
|
|
89
|
+
- **文档写明「嵌套能力共用一块黑板、不隔离」**(`usage-guide` §5.1,零代码改动):
|
|
90
|
+
`@SubAgent` / `@Skill` 与父 run 是同一份 `RunContext`(`withRunContext` 全仓单点调用),
|
|
91
|
+
子 agent 写的键父 run 看得见(有意);并发子 agent 写同一键是 last-wins(与 `flushMemory` 同款取舍),
|
|
92
|
+
要隔离请自己命名空间。「按子树隔离」保留为 `spec.md` §11 开放项。
|
|
93
|
+
- **初始 `save` 不 await:定案不改**(外部深评 T6,报告已自行降级 P3):
|
|
94
|
+
`submit` 是**同步门面**(await 会把它变成 async = 破坏性变更,只换来「多等一个网络往返」);
|
|
95
|
+
「静默吞错」这一半已有处置(迟到的 reject 转成任务 failed,且只在任务未被推进时改判)并用例守着
|
|
96
|
+
(`tests/transport/async.test.ts`)。判据与承重理由写进 `spec.md` §10 ㉓。
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
### 修复 · 四条「静默失效」收口(2026-09-28 ㉒)
|
|
100
|
+
|
|
101
|
+
来源:`DEEP-AUDIT-VERIFIED-2026-09-28.md` 的 P2 表(`S1` / `S2` / `S3` / `S6`),
|
|
102
|
+
逐条在当前 `main` 上复核后动刀。四条同一族:**错了 / 算不出 / 超限了,却没有任何信号**。
|
|
103
|
+
|
|
104
|
+
- **redis store 的 TTL 不再吃掉非终态记录**(`store/redisStore.ts`,**行为变更**):
|
|
105
|
+
此前 `applyTtl` 不看状态,一条**等审批中**(`suspended`)的任务会在 TTL 到点时从 redis 里消失 ——
|
|
106
|
+
「批了它」的决定无家可归(`approve` 404);而内存 store 的淘汰逻辑里明写着 `suspended` 不可淘汰
|
|
107
|
+
(同一事实的两处读数方向相反)。现在「终态」判定单源到 `core/run.ts` 的 `isTerminalStatus`
|
|
108
|
+
(穷尽分类表:加成员不表态 ⇒ `tsc` 报缺属性),只有**终态**记录才拿到查询窗口(非终态覆写靠不带选项的
|
|
109
|
+
`SET` 清除既有 TTL),幂等索引与记录同生共死。**代价如实**:永远等不到审批的挂起记录会永久驻留
|
|
110
|
+
(批不批是人的事,store 无从判死)—— 与内存 store 同款取舍,清理属部署层职责。
|
|
111
|
+
- **上游没回报 usage 时不再静默填 0**(`integrations/anthropic.ts` / `integrations/openai.ts` /
|
|
112
|
+
`engine/turn.ts`):`?? 0` 让「端点不回 usage」(或忽略 `stream_options.include_usage`)与
|
|
113
|
+
「真的只花了 0」在数值上无法区分,于是 `maxCostUsd` 这条成本护栏**静默失效**。现在四条响应路径
|
|
114
|
+
(流式 / 非流式 × 两条适配器)都标 `MessageUsage.unreported`,引擎在 llm.turn span 上记
|
|
115
|
+
`usage.missing` 事件,报告出 `usageMissingTurns` / `usageMissingModels`、指标出
|
|
116
|
+
`agentia_model_usage_missing_turns_total`(render + OTLP 两出口)。与 `usage.unpriced`(模型不在
|
|
117
|
+
价格表里)**分开计数**:后果都是「成本看起来是 0」,但一个要换模型、一个要查端点/网关。
|
|
118
|
+
- **`statusOfStreamError` 收成一份**(`integrations/adapter-options.ts`):原本两条适配器各写一份、
|
|
119
|
+
靠注释互相喊话对齐 —— 合并时发现它们**早就不一致**(`not_found_error` 一侧 400 另一侧 500,
|
|
120
|
+
等于把「模型名错了」记成 `server`)。新增 `tests/integrations/stream-error-parity.test.ts`:
|
|
121
|
+
一张表跑两条真适配器 + 真 SSE,两行 `wasDivergent` 标出分歧点。
|
|
122
|
+
- **两条 MCP 桥的分帧缓冲加上限**(`core/line-framing.ts`,`MAX_FRAME_CHARS` = 8M 码元):
|
|
123
|
+
`buf += chunk` 遇到「不带换行的巨型行」就是无界增长(先 OOM 再谈协议;MCP server 往 stdout
|
|
124
|
+
混日志本来就常见)。连接器侧超限即拒绝全部在途请求并终止子进程;反向桥侧回一条协议错、
|
|
125
|
+
丢弃到下一个换行**重新对齐**(其后的帧照常派发)。
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
### 文档 · 外部深评的 5 份报告入库 + 逐条落地盘点(2026-09-28 ⑲)
|
|
129
|
+
|
|
130
|
+
**改了「复核报告不随仓提交」这个惯例**:那轮外部深评产出的 5 份报告**原文逐字入库**到
|
|
131
|
+
`docs/reviews/2026-09-28/`(入库前后逐份 `shasum -a 256` 比对,**逐字节一致**),
|
|
132
|
+
配一份索引 `docs/reviews/2026-09-28/README.md`,里面是**逐条落地状态表**。
|
|
133
|
+
|
|
134
|
+
- **为什么改**:① 剩余条目是真实的待办来源(P2 表还有 **14 条**未做 + 1 条「有意为之」),
|
|
135
|
+
不入库 ⇒ 只有当天在场的人知道还剩什么;② 本仓文档有 **7 处**引用这些报告,
|
|
136
|
+
而它们此前**指向一个不在仓里的文件** —— 「指向一个读不到的东西」本身就是坏引用。
|
|
137
|
+
- **代价如实**(写进索引 §0):入库即意味着这些文字会随代码**漂移**。所以正文逐字保留、
|
|
138
|
+
「现在还剩什么」一律以索引表为准、**每落地一条回来更新那张表**(本目录唯一要维护的地方)。
|
|
139
|
+
- **盘点结论**(逐条按其自报位置在当前 `main` 上重跑定点 `grep` / 读源码,不采信报告自述):
|
|
140
|
+
P1 表 9 条 **8 条已落地**(`C1`/`C2`/`T1` → #169,`E1`/`E2`/`T2`/`T3`/`J1` → #170);
|
|
141
|
+
P2 表 26 条 **11 条已落地**(`C6` → #171,`C3`/`C4`/`C5` → #172,`E3` → #170,`E8` → #174,
|
|
142
|
+
`T5` → #173,`S4` → #176),**14 条未做**,`T7` 为「有意为之」;
|
|
143
|
+
`DEEP-REVIEW` / `PR-164-REVIEW` / `SRC-STRUCTURE` 三份的残留项除两条「定案不做」外全部已落地。
|
|
144
|
+
- **顺带校正报告一处判据**(`T4`):报告说「非终态流的缓冲永不淘汰」,而**单流条数上限**
|
|
145
|
+
(500、丢最旧)在审计当时就已存在(#115 起)—— 报告漏看;仍成立的是「非终态流的**表项**
|
|
146
|
+
不被回收」。**判据不同、修法就不同**,所以这条保留为「未做」但把判据写准。
|
|
147
|
+
- **7 处引用**:`spec.md:4224`/`:4256`/`:4438`/`:4489` 与 `CHANGELOG.md:12` 原文里的
|
|
148
|
+
「(**未随本仓提交**)」自本条起不再成立 —— 按 spec 的**只增不改**纪律原文一字未动,
|
|
149
|
+
权威更正是 `docs/spec.md` §10 ⑲,逐条对照在索引 §3。
|
|
150
|
+
另一处(`CHANGELOG.md:315` 指向「**仓库根** `SRC-STRUCTURE-2026-09-28.md`」)是**坏引用**
|
|
151
|
+
(那个路径从来没存在过),已**就地修成真实路径** —— 修的是坏引用,不是历史事实。
|
|
152
|
+
|
|
153
|
+
### 修复 · 外部深评五条 P1 收口(2026-09-28 ⑫)
|
|
154
|
+
|
|
155
|
+
来源同上:`DEEP-AUDIT-VERIFIED-2026-09-28.md`(**未随本仓提交**)里「本轮复核方未覆盖、保留原判」
|
|
156
|
+
的其余五条 P1,**逐条实证后**才动刀(含一条被自己的诊断否掉的原判,见 spec §10 ⑫)。
|
|
157
|
+
|
|
158
|
+
- **上下文策略抛错不再判死整条 run**(`engine/turn.ts`):`ContextPolicy.beforeTurn` 里可能**走网络**
|
|
159
|
+
(compaction 的 `summarize` 是模型调用),一次 429 / 超时就会让整条 run 以「请求失败」收尾 ——
|
|
160
|
+
而上下文压缩对一条 run 是**尽力而为**的优化,不是前置条件。现在降级成「本回合原样放行」,
|
|
161
|
+
**且出声**:run 根上记一条 `context.policy_failed { iteration, message }` + 一条 `console.warn`
|
|
162
|
+
——否则「历史一直很长、token 慢慢涨」没人知道为什么。
|
|
163
|
+
- **压缩失败的滞回额度不再被烧掉**(`engine/policy.ts`):`lastCompactAt` 原先在
|
|
164
|
+
`compactMessages(...)` **之前**就写 —— 摘要器失败时额度已经用掉,于是「这一回合没压成」
|
|
165
|
+
还连带让后面 `compactEvery - 1` 个回合都不再尝试。现在记的是「上次**成功**压缩的回合」,
|
|
166
|
+
失败不记账、下一回合自然重试。⚠️ 代价说明:摘要器**持续**失败时每回合都会再调一次
|
|
167
|
+
(每次多一次失败的模型往返)—— 要抑制请在 `summarize` 里自己退避,别指望滞回额度兜。
|
|
168
|
+
- **MCP HTTP 传输的意外不再带走宿主**(`engine/mcp-server-http.ts`):请求处理那层 `try` 此前
|
|
169
|
+
**只有 `finally`** ⇒ 协议面之外的异常(例如某条 `inputSchema` 是带 getter 的对象,
|
|
170
|
+
`validateJsonSchema` 一读就抛)冒泡出 async handler = unhandled rejection = Node ≥15 默认
|
|
171
|
+
**终止进程**。同文件的鉴权钩子、`JSON.parse`、stdio 那条传输都有兜底,只有这里漏了 ⇒
|
|
172
|
+
「同一份协议面在两条传输上一条活着、一条把宿主带走」。现在回 **200 + JSON-RPC `-32603`**
|
|
173
|
+
(协议面的错误是 200 里的错误对象,不是进程级 5xx),响应头已发时只收口。
|
|
174
|
+
- **`drain()` 不再可能等一个永远不结束的等待**(`transport/http.ts` + `http-endpoints.ts`):
|
|
175
|
+
唯一能结束一条「自己不结束的 SSE」的机制(收口)原先排在等待**之后**,而缺省 `timeoutMs = 0`
|
|
176
|
+
是**不限** ⇒ 死锁。现在**活连接不享受无限窗口**:没有 deadline 时先收口再等,有 deadline 时
|
|
177
|
+
保持既有的优雅等待。返回值也**如实分两种收口**:切过在飞的 run ⇒ `false`(工作是被 abort 收尾的,
|
|
178
|
+
回 `true` 等于把「切了」说成「等干净了」);只关掉旁观者长连(`/tasks/<id>/stream`)
|
|
179
|
+
⇒ 不损失任何工作,不参与这个判断(把它算成没排干净是另一种说错话)。
|
|
180
|
+
- **能力引用成环在装配期即拒**(新增 `toolkit/capability-cycles.ts` + `module.ts` 接线):
|
|
181
|
+
`subagentToTool` 的第二参是**延迟求值 thunk**、装配期把引用全建满 ⇒ 环是**纯运行期**递归,
|
|
182
|
+
而 `maxIterations` 只限**宽度**、深度没有任何闸 ⇒ 模型一次自我调用就是无限递归
|
|
183
|
+
(树按 `宽度^深度` 炸开)。现在装配期 DFS 找环并抛错,错误信息给出**环上的节点序列**。
|
|
184
|
+
全仓 grep 确认**没有任何既有用例**写自引用 ⇒ 不误伤既有装配。
|
|
185
|
+
- **续跑认领从「新鲜度启发式」升级成「租约」**(新增 `transport/owner-id.ts`、`owner-liveness.ts`,
|
|
186
|
+
改 `resume-policy.ts` / `async.ts`):`staleAfterMs` 只回答「记录起跑多久了」,不回答
|
|
187
|
+
「主人还在不在」,两个方向都会错 —— ① 单段 run 跑超窗口 ⇒ 他进程当孤儿抢走 ⇒
|
|
188
|
+
**同一任务跑两遍**(真·重复执行);② 崩溃进程留下的记录**仍带 ownerId**(`submit` 恒写)
|
|
189
|
+
⇒ 落在保鲜期内没人捡 ⇒ **饿死**(扫描是**惰性**的,扫完就没人再扫)。
|
|
190
|
+
现在 `ownerId` 形状为 `p<pid>@<host>-<8 位十六进制>`,`staleAfterMs > 0` 时**同主机直接问
|
|
191
|
+
「那个 pid 还在不在」**(`process.kill(pid, 0)`):**在的绝不抢**(不管记录多老)、
|
|
192
|
+
**不在的立刻可抢**(= 崩溃孤儿,不等保鲜期);**判不了**(异主机 / 升级前写下的旧格式 /
|
|
193
|
+
自定义标识)才退回新鲜度启发式 ≡ 升级前行为。
|
|
194
|
+
- ⚠️ **`ownerId` 是内部标识,宿主别解析它**(格式本次变了)。混版本部署安全:旧进程写下的
|
|
195
|
+
无 `@host` 记录一律走「判不了 ⇒ 新鲜度」,行为与升级前一致。
|
|
196
|
+
- 残留风险如实记(两种都只会「**晚一点**才捡」,不会「抢错」):**PID 复用**与**僵尸进程**
|
|
197
|
+
(父进程未收尸)都会被判成「主人在」,要等那个 pid 真的消失才可捡。刻意**不加时间硬上限**
|
|
198
|
+
兜它们 —— 加上限 = 把租约换回新鲜度启发式,「长跑被抢」那个 bug 原样回来。
|
|
199
|
+
|
|
200
|
+
### 修复 · 定时器上限单源(2026-09-28 ⑪)
|
|
201
|
+
|
|
202
|
+
- **超过 `2^31-1`ms(≈24.86 天)的定时器延迟不再是「配置合法、行为相反」**:Node 不会遵守它
|
|
203
|
+
(只在 stderr 留一行 `TimeoutOverflowWarning`,随后把延迟钳到 **1ms**),于是「配 30 天超时」
|
|
204
|
+
会变成「每个调用立即超时」、「退避到限流窗口之后」会变成「热重试打风暴」。此前防线只在
|
|
205
|
+
`Scheduler.every` / `at`,而**派生出来的等待**(`withTimeout` ← 引擎 `toolTimeoutMs`;
|
|
206
|
+
`interruptibleSleep` ← 重试退避;`AsyncRunner.runTimeoutMs`;`createAnthropicClient.timeout`;
|
|
207
|
+
`DrainGate.waitForIdle` ← `drain` 的等待预算)一律只查「非负 / 有限」。现在上限单源在
|
|
208
|
+
`core/timeout.ts`(`MAX_TIMER_DELAY_MS`),五处站点在**构造期 / 入口**一并拒绝。
|
|
209
|
+
- **`NaN` 也拒**(同一处校验):`withTimeout(p, NaN)` 此前被那句 `!(x > 0)` 静默读成
|
|
210
|
+
「不设超时」(与「我设了个预算」正好相反,桥还会因此起自己的 60s 兜底 ⇒ 双计时器、双账本);
|
|
211
|
+
`interruptibleSleep(NaN)` 此前穿透「非正」判直落 `setTimeout(NaN)`(同样被钳到 1ms)。
|
|
212
|
+
- **上游的 `Retry-After` 走「夹」不走「拒」**:`Retry-After: 99999999` 不是使用者的错,
|
|
213
|
+
为它把一次 429 升级成硬失败是错的;现在被夹到上限,不再静默变成「1ms 后立刻重试」。
|
|
214
|
+
- **`cancel` 的 409 文案分成两种说法**(`AsyncRunner`):原先一律说「正在运行但**不在本进程**」,
|
|
215
|
+
而同一判据还覆盖「**已在本进程受理、正在等并发槽位**」(四条恢复路径先置 `running` 再派发,
|
|
216
|
+
句柄要到拿到槽位才登记)—— 对它说反话。现在两支分开措辞。**闸不放开**(放开会让
|
|
217
|
+
`onFinished` 双发)。
|
|
218
|
+
- ⚠️ **`limits` 表的 `withTimeout.ms` / `interruptibleSleep.ms` 的 `badValue` 由 `none` 改 `throws`**:
|
|
219
|
+
传给 `RunAgentOptions.toolTimeoutMs` 的 `NaN`、或任何超过上限的超时值,现在会**抛错**。
|
|
220
|
+
这两种值此前的行为都与配置意图相反(一个静默变「立即超时」,一个静默变「不设超时」)。
|
|
221
|
+
|
|
222
|
+
### 修复 · 复审收口(外部深评 2026-09-28)
|
|
223
|
+
|
|
224
|
+
- **schema 的环与深度护栏**(C6):`validateJsonSchema` 按 properties / items 递归,
|
|
225
|
+
**环 + 深值**会钻到 V8 的 `RangeError: Maximum call stack size exceeded` —— 症状没有辨识度
|
|
226
|
+
(看着像「校验不了」,而不是「schema 有环」)。现在两道闸:`assertNoSchemaCycle()` **前置**抓环
|
|
227
|
+
(与值深度无关,点名路径;用祖先集,**共享子树不算环**)+ `MAX_SCHEMA_DEPTH = 256` 兜
|
|
228
|
+
「合法但深到失控」。⚠️ 只加深度闸是**不够的**:递归深度 = min(schema 深度, 值深度),
|
|
229
|
+
所以「环 + 浅值」根本不会爆栈(我也正是这么写出第一版红用例的)。
|
|
230
|
+
- **limits 表的对账收口**(C4 + C5 家族):`LIMIT_SEMANTICS` 里声明「坏值抛错」的旋钮有 18 个,
|
|
231
|
+
而「报错文案真的取自同一张表」的对账 case 只覆盖 10 个,且那个清单是个 `Array` ——
|
|
232
|
+
新增一个 throws 旋钮却忘了对账,**不会有任何东西红**(「表说它有构造期校验」与「文案真同源」
|
|
233
|
+
之间没有机器守着)。现在:**A4 单向穷尽断言**(每个 throws 旋钮都必须有 case)+ 补齐 8 条
|
|
234
|
+
case + 把实现侧的文案接上 `zeroClauseOf`(7 处此前是手写或被通用文案盖掉)。
|
|
235
|
+
另补登 **5 个**从未进表的旋钮:`BudgetGuardOptions.maxTotalTokens` / `maxCostUsd`
|
|
236
|
+
(v0.5.0 起就存在)与 `metricsSink.maxCapabilities` / `maxModels` / `maxScores`。
|
|
237
|
+
顺带修 8 条 `zeroClause` 的**双层括号**(表里自带「必须为正数(…)」而实现又加一层),
|
|
238
|
+
并把 `withTimeout` 的 **async 站点**(坏值表现为 rejection 而非同步 throw)纳入对账。
|
|
239
|
+
- **排队段的上限 `AsyncRunnerOptions.maxQueued`**(T5,缺省 0 = 不限):`concurrency` 只约束
|
|
240
|
+
「同时在跑几个」,而「跑不上、排在后面」那一段**此前没有任何上限** —— `submit` 永远收单、
|
|
241
|
+
`POST /tasks` 永远 202,排队段随调用方灌入无界增长(每条排队任务 = 一条记录 + 一棵悬挂的
|
|
242
|
+
`#execute` promise + 一个槽位等待者,全在内存)。现在超限 `submit` 抛 `TaskQueueFullError`
|
|
243
|
+
(带 `status: 503` 自陈字段),HTTP 宿主回 **503 + `Retry-After`**(与 `maxConcurrentRuns`
|
|
244
|
+
同款,**不是 400** —— 排队满不是调用方的错)。
|
|
245
|
+
① **判据不是「已受理未持槽的条数」**,而是「**真正在排队的深度** = `max(0, 已受理未持槽数
|
|
246
|
+
− 空槽位数)`」:前者把「池子还空着、马上就能跑」的也算成排队的,`concurrency: 1` +
|
|
247
|
+
`maxQueued: 1` 下会把第 2 条**合法**提交误拒(第一版就是这么写的,被用例当场抓出来)。
|
|
248
|
+
② 为此把「出排队段」挪到 `await acquire()` **之前**做一次快照判断(`inUse < concurrency`
|
|
249
|
+
就当场出集合):只走 acquire 的连续体时,「占槽」(`running++`)是同步的、「出排队段」却要
|
|
250
|
+
等一个微任务,同一条任务在那一瞬被算两遍 ⇒ 深度虚高一格。
|
|
251
|
+
⚠️ 刻意**不**把它做成「同步快路径替代 acquire」:那会顺带少掉一次微任务,把「置 running」
|
|
252
|
+
相对引擎推进的时刻**提前**,踩到既有用例里「等到 `running` 就当工具已挂在飞」的隐含假设
|
|
253
|
+
(`cancel.test.ts` 的前置断言当场红,实测踩到过)—— **记账改原子,时序一个字不改**。
|
|
254
|
+
③ **只在 `submit` 判**:恢复路径(approve / 到期唤醒 / 崩溃重投)推进的是**已受理**的任务,
|
|
255
|
+
拦下来等于把它搁死在 store 里。④ 幂等键命中的重复提交**不吃** 503(闸在去重之后)。
|
|
256
|
+
⑤ `get queued` 与闸**同用** `#queueDepth`:读数即判据,不会各说各话。
|
|
257
|
+
顺带把 `SlotPool.inUse` 的「测试用」自述改成真话(它从此是闸的一半)。
|
|
258
|
+
- **工具 I/O 事件名单源到 `core/trace.ts`**(E8):`tool.input` / `tool.output` 跨
|
|
259
|
+
**engine → eval → integrations** 三层被手写(**12 处**取值 —— 报告写「7 处」偏小,我逐条点数是 12)。
|
|
260
|
+
漏一处的后果不是「少记一条事件」,是**静默归零**:`integrations/report.ts` 与
|
|
261
|
+
`metrics-state.ts` 都按名字过滤,改名漏一处 ⇒ 报表说「没有工具调用」而 trace 里明明有。
|
|
262
|
+
现在常量 + `ToolIoEventName` 类型落在 **`core/trace.ts`**(⚠️ 报告建议的落点
|
|
263
|
+
`engine/tool-events.ts` **不可行**:`integrations` 只许依赖 `core`,够不着 engine —— 那样
|
|
264
|
+
integrations 侧只能再手写一份,等于把单源补成两处);11 处取值 + 6 处 import 改引常量。
|
|
265
|
+
`eval/harvest.ts` 里**生成出去的用例源码文本**保留字面量:生成的脚本不 import 本仓常量,
|
|
266
|
+
改了会得到一个引用不存在标识符的脚本(跑起来才炸)。
|
|
267
|
+
配**源码级守卫** `tests/architecture/tool-event-names.test.ts`(自带遮蔽器,与
|
|
268
|
+
`lib/source-scan.ts` 相反 —— 那个按设计遮蔽字符串):三条自证样本 + 白名单按「文件 + 恰好
|
|
269
|
+
条数」+ 射程钉 + 走查下限;变异三条全部具名复红(含「守卫自身的守卫」:遮蔽器退化)。
|
|
270
|
+
- **删掉内部死方法 `TaskEventStreams.forget`**(外部深评「forget(taskId) 未处理」那条的收口):
|
|
271
|
+
全仓**零调用点**(`grep -rn 'forget(' src/ tests/ packages/ examples/ scripts/` 命中的
|
|
272
|
+
`scheduler.ts` 那 4 处是它自己的**局部函数** `const forget = (taskId) => inFlight.delete(taskId)`,
|
|
273
|
+
操作的是 `inFlight`,与事件流无关)。§10 上一轮已如实登记它「不该为它写测试 —— 那等于锁死
|
|
274
|
+
死代码」,本轮直接把它**删掉**:淘汰这件事唯一的真实机制就是 `#evictTerminal`(按 LRU 丢终态流、
|
|
275
|
+
有订阅者的不丢)。原地留一段注释说明「它曾经在、为什么删、真要再加回来该怎么设计」。
|
|
276
|
+
⚠️ 这一条**不是**「补文档」:删掉公开方法会让它的名字从 API 面消失,对使用者是**无感**的
|
|
277
|
+
(它从未从 `src/index.ts` 导出),但少一个「代码里有、文档里承诺、实际永不发生」的错位。
|
|
278
|
+
- **OTLP 导出不再每次 flush 白算分位**(S4):`metrics-otlp.ts` 原来每次 flush 调一次
|
|
279
|
+
`state.snapshot()`,却只从结果里读 `runs` / `failed` / `costUsd` **三个标量** —— 而这三个
|
|
280
|
+
**本来就是 `MetricsState` 的公开字段**;它对 capabilities / models / runLabels 全是**直接遍历公开 map**
|
|
281
|
+
(`grep latencyP50\|latencyP95 src/integrations/metrics-otlp.ts` → 空),一个分位字段都不读。
|
|
282
|
+
而 `snapshot()` 会为**每个**组合各算 p50 + p95,`percentile()` 的实现是 `[...ring].sort()`。
|
|
283
|
+
实测(`npm run bench:otlp`,满 1024 窗口 / 30 个组合):单个 p50+p95 **233.8 µs**、
|
|
284
|
+
`snapshot()` 整体 **5929.6 µs**、而 `buildOtlpPayload()` 只要 **41.6 µs** ⇒ 白算 **99.3%**,
|
|
285
|
+
且**每个 flush 都付一次**(拉取式 Prometheus 侧无此问题:那里确实要分位)。
|
|
286
|
+
修法**不是**报告建议的「让 `snapshot()` 的分位可选 / 另给一个不算分位的视图」—— 那是给一个
|
|
287
|
+
本不该存在的调用加开关。OTLP 只需要三个公开字段,**删掉这次调用**即可,依赖面同时变诚实:
|
|
288
|
+
它不再依赖 `MetricsSnapshot`(Prometheus 侧渲染的产物)这个形状。**无行为变更**(payload 逐字段不变,
|
|
289
|
+
1322 条框架用例全绿)。配**行为级**守卫 `tests/architecture/otlp-no-snapshot.test.ts`:
|
|
290
|
+
把 `state.snapshot` 换成会抛的桩再跑导出路径(⚠️ 不用源码扫描 —— 被守的源码注释里**本来就写着
|
|
291
|
+
`state.snapshot()`**,靠遮蔽器区分注释与代码等于把守卫的成败押在遮蔽器上);含阳性对照
|
|
292
|
+
「这枚雷真的会炸」防真空 + 「改字段 payload 跟着变」防读副本。变异两条具名复红:
|
|
293
|
+
① 把 `snapshot()` 加回去;② 三个标量改读错来源。
|
|
294
|
+
新增 `npm run bench:otlp`(`scripts/bench-otlp-snapshot.ts`,与 `bench:trace` 同档、不进 CI)。
|
|
295
|
+
- **engine 层的四处「如实性」收口**(外部深评 E4 / E5 / E6 / E7):
|
|
296
|
+
- **E4 · 请求失败出口不再硬写 `eventsDelivered: false`**:那条出口在 `loop.ts` 的 catch 里,
|
|
297
|
+
而 `eventsDelivered` 原先只挂在**内层** `LoopContext` 上 —— 外层够不到,于是只能硬写。
|
|
298
|
+
现在它挪到**跨段共享**的 `progress` 对象上(`turn.ts:233` 的注释本来就写着「抛错时调用方仍读得到」),
|
|
299
|
+
由 catch 按事实传入。**零行为变更**(`pendingEvents` 在终态分支本来就无条件清),
|
|
300
|
+
但**对外字段不再说谎**:它随 `rec.result` 落库、在导出面上,报错方向会让宿主以为该重投。
|
|
301
|
+
配回归用例(事件已注入 → 续跑段请求失败 ⇒ `eventsDelivered === true`):**变异撤回传参即红**。
|
|
302
|
+
- **E5 · `stop reason` 的成败分类改成穷尽表**:`isSuccessStopReason` 的
|
|
303
|
+
`reason === 'end_turn' || …` 换成 `Record<AgentStopReason, boolean>` 表 ——
|
|
304
|
+
「往联合里加一个成员而没在表里表态」**在类型上写不出来**(实测 `TS2741: Property 'brand_new_reason'
|
|
305
|
+
is missing`),三处消费者(run 状态 / trace 状态 / 子 agent 交回判定)因此不可能各说各话。
|
|
306
|
+
⚠️ 比原判要求的 `never` 断言更强:**编译期**就拦,且新增成员时逼你补一行语义。
|
|
307
|
+
配 `tests/types/stop-reason.types.ts`(类型级):`@ts-expect-error` 是**正控** ——
|
|
308
|
+
谁把表放宽成 `Partial<Record<…>>` / 加 `?? false` 兜底,那条指令就变成未使用 ⇒ 类型检查红。
|
|
309
|
+
- **E6 · `budgetTokens` 的口径文案改准**:它**只估 `messages`** —— system prompt 与 tools schema
|
|
310
|
+
不在这个数里(`policy.ts` 只把 `messages` 喂给 `countTokens`),所以它是「对话历史多大」的尺子、
|
|
311
|
+
不是「这次请求多大」的尺子(真实 input tokens 恒 ≥ 它)。`types.ts` 与该行 `usage-guide` 同改。
|
|
312
|
+
- **E7 · `setAttribute` 的闸口径改准(只改口径,代码未动)**:报告写「无闸」,逐行复核后更准的说法是
|
|
313
|
+
「**不过 `maxEvents` 那道数量闸**、值**不截断**」;有界的部分写清楚(正文受 `maxEventChars` 截断、
|
|
314
|
+
条数受调用点上限约束)。⚠️ 并写明**为什么不顺手加截断**:属性是交付 trace 与增量流**共用的同一份载荷**,
|
|
315
|
+
而 `core/trace.ts` 那条折叠契约(按 `seq` 折回**逐字等于** `snapshot()`)的两端就是它们 ——
|
|
316
|
+
只截一侧就当场毁约(`tests/engine/trace-events.test.ts` 钉着它)。要收就收 run 入口那几条的入参大小。
|
|
317
|
+
⚠️ **本批顺带订正上一批(#177 / ⑲)的两处计数错**:⑲ 写「P2 表 26 条:11 已落地 / 14 未做」是
|
|
318
|
+
**我的算术错**(逐行数出来是 8 / 17+1),且 ⑲ 的索引表**漏了整行 `K6`**(三态 `system` 的差异)。
|
|
319
|
+
索引表已补齐并逐行改成可数的状态,详见 `spec.md` §10 ⑳ 的订正段。**本批之后的 P2 账:
|
|
320
|
+
26 行 = 12 已落地 / 13 未做 / 1 有意为之。**
|
|
321
|
+
- **runtime / container / 文档三处「静默给错东西」收口**(外部深评 K3 / K4 / K6,⛔ **行为变更**):
|
|
322
|
+
- **K3 · `Run.finish()` 补守卫**:`start()` / `suspend()` 都有守卫,`finish()` 没有 ⇒
|
|
323
|
+
对一条**挂起中**的 run 调它会**静默翻成终态**(挂起带着 `suspendedMessages` 等审批/唤醒,
|
|
324
|
+
翻掉之后宿主的恢复路径再也接不上,零信号)。`Run` 是公开导出 ⇒ 这是对外承诺。
|
|
325
|
+
顺带把 `cancelled` 的**可达性**写进类型注:进程内 `Run` 到不了它(取消表现为
|
|
326
|
+
`stopReason: 'aborted'` ⇒ 记 `failed`),它由宿主(`TaskRecord.status`)落 ——
|
|
327
|
+
同一份联合类型两个消费者、可达集不同,是有意的,但此前没写在类型上。
|
|
328
|
+
- **K4 · async 工厂不再被静默当成值**:容器**同步**解析,`useFactory` 写成 `async` ⇒
|
|
329
|
+
返回的 Promise 被**原样缓存成「值」**,下游注入到 Promise 本身(首次属性访问全 `undefined`、
|
|
330
|
+
零报错、`tsc` 看不出)。现在在 `cache.set` **之前**检测 thenable 并抛 `TypeError`,
|
|
331
|
+
文案给出两条出路(装配前 await 好再用 `useValue`;或包一层 `useValue: { promise }`)。
|
|
332
|
+
检测器**就地写**(`container` 是纯叶子,`layering.test.ts` 里 `container: []`)、
|
|
333
|
+
**覆盖三个分支**(只查工厂等于把同类错留在另两处)。逃逸口配了测试。
|
|
334
|
+
- **K6 · 三态 `system` 的差异进 `usage-guide`**(纯文档):`string` / `SystemPrompt` 两形态
|
|
335
|
+
会自动追加 `REPORT_HINT`,**函数形态不追加**(有意 —— 返回值由使用者全权决定),
|
|
336
|
+
但使用者此前读不到这个差异。
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
- **停机窗口里的派发收成唯一入口**(P1-1 / P2-1):所有「先落库再派发」的路径(`submit` /
|
|
340
|
+
`approve` / `signalTask` / 到期唤醒 / **审批超时兜底** / **`resumePending` 的认领**)形状相同,
|
|
341
|
+
而停机闸原先散在各支里 ⇒ 「覆盖几条路径」成了**靠人记**的清单:上一批修了三支,第四支
|
|
342
|
+
(审批超时兜底,由 `GET /tasks/:id` 的惰性闸驱动,而「停机中照常可轮询」是宿主的明确承诺)
|
|
343
|
+
与第五支(认领循环)漏了 —— 两处都在 `drain()` 返回 `true`(「排空干净」是 SIGTERM 决策依据)
|
|
344
|
+
之后**又起了新 run**。现在闸只在 `#dispatch()` 里判一次;配**源码级穷尽守卫**
|
|
345
|
+
(`this.#execute(` 与 `this.#executeInner(` 各只许出现在自己的那一个家里)⇒ 新增恢复路径而
|
|
346
|
+
绕过派发口时构建红,不靠记性。实证:`tests/transport/drain-race.test.ts`(五条路径各一条
|
|
347
|
+
用例 + 阳性对照)。
|
|
348
|
+
- **`approve` 只许批本任务待决的 id**(P1-2):原先 `decisions` 里多出来的键照样写进
|
|
349
|
+
`rec.approvals`(随每次 `save` 全文重写、并随任务**永久保留** —— 终态也不清),一次调用就能把
|
|
350
|
+
记录从 246 字节撑到 419 KB(实证),反复调用可无限叠加。现在多出任何一个 id ⇒
|
|
351
|
+
**400 + 整批拒 + 记录一字不动**(与 `parseApproveBody`「全有或全无」、`parseEventBody`
|
|
352
|
+
「多一个字段即拒」同一纪律)。**不加常量上限**:单次 approve 能写的键被钉在当前
|
|
353
|
+
`pendingApprovals` 里,跨轮累计也只随真实挂起轮次增长 —— 单次输入无法放大体积。
|
|
354
|
+
- 缓冲满的 409 文案按挂起原因分两种说法(`approval`:唯一触发源是 approve、不在投递方手里;
|
|
355
|
+
`timer`:到点自己醒来,approve 对它不适用)。
|
|
356
|
+
- **trace 截断在两条缝上都看得见**(P3-1):`traceLimits.maxEvents` 超限后,run 根上会留**两笔**
|
|
357
|
+
`trace.truncated` —— **起点标记** `{ limit }`(第一笔丢弃那一刻,标「从这里起有缺」)与
|
|
358
|
+
**收尾摘要** `{ droppedEvents, limit }`(交付时的最终计数)。两笔**同时**进交付的 trace 与
|
|
359
|
+
`onTraceEvent` / `subscribe` 增量流 ⇒ 「按 `seq` 折回**逐字等于** `snapshot()`」那条折叠契约
|
|
360
|
+
在截断下**无例外地**成立(此前它在设了上限且真截断时是假的:摘要从不出现在增量流里,
|
|
361
|
+
只是默认不设上限所以没人碰到)。两笔簿记**不占** `maxEvents` 配额(截断时事件数上限是
|
|
362
|
+
`maxEvents + 2`)。⚠️ 起点标记会进交付的 trace —— 有意的:让制品自己标出「从这里断的」。
|
|
363
|
+
- **`task.event` 留痕的 `delivered` 改名 `injected`**(P3-2,**可观测面变更**):口径修正为
|
|
364
|
+
「注入进本段消息流」—— 原名超前于事实(该段若在首个模型请求前中止/失败,事件不进
|
|
365
|
+
持久化历史,但 trace 上已写着 delivered)。消费这条 trace 事件的下游请同步改字段名。
|
|
366
|
+
|
|
367
|
+
### 修复 · PR #164 复核收口(2026-09-28 ⑨)
|
|
368
|
+
|
|
369
|
+
- **停机窗口的拒绝不再静默**:`#dispatch()` 在停机中拒掉一条派发时,会打**一条**
|
|
370
|
+
`[agentia] 停机中:…` 的 `console.warn`(说清「谁、什么状态、谁来认领、宿主该做什么」;
|
|
371
|
+
只报一条 —— 窗口里所有记录的原因完全相同)。此前是一条裸 `return`,运维只看到一个停在
|
|
372
|
+
`running` 的任务,无从解释。
|
|
373
|
+
- **`drain()` 的契约写全了**:它是**单向闩**(置位后没有复位路径)⇒ 返回 `false`(排空超时)
|
|
374
|
+
同样意味着**宿主必须退出**。否则停机窗口里已落库的记录(`running`/`queued` + 本进程 ownerId)
|
|
375
|
+
在本进程内**没有自愈路径**:`resumePending` 按 `own-process` 跳过、`submit` 已关闭 ⇒
|
|
376
|
+
只能等下一次启动认领(探针实证:认领数 0、`awaitTask` 不返回)。订正 `drain-gate.ts` 里
|
|
377
|
+
「false 不改任何共享状态」那句(`draining` 恰恰是保持置位的),并给 usage-guide §7 加一行
|
|
378
|
+
「`drain()` 之后有些任务停在 `running` 不动」。
|
|
379
|
+
- **守卫补上 `#executeInner` 这个旁路**:只钉 `this.#execute(` 拦不住它 —— 绕过它会**同时**
|
|
380
|
+
绕过停机闸与 `active++` / `#streams.open`(`drain()` 会在一条 run 真在跑时返回 `true`)。
|
|
381
|
+
- **文案订正**:`submit` 的「进闸与派发之间刚开始停机的窄窗口」**不存在**(`submit` 全同步,
|
|
382
|
+
没有插入点 —— 走 `#dispatch` 是结构一致性,不是行为依赖);`sse-frames.test.ts` 把
|
|
383
|
+
`trace.truncated` 说成「run 根的 attribute」(它是**事件**);`api.html` 的
|
|
384
|
+
`createHttpHandler` 行补上 ⑦ 新增的语义级 400。
|
|
385
|
+
|
|
386
|
+
### 新增
|
|
387
|
+
|
|
388
|
+
- **run 事件投入口(spec §10 2026-09-28 ⑥,定案 A3+B1)**:外部系统可以把一条**事件**
|
|
389
|
+
投给一条**挂起**的 run —— `AsyncRunner.signalTask(taskId, event)` 与
|
|
390
|
+
`POST /tasks/:id/events`(与 approve/cancel 对称),醒来时事件进消息历史。
|
|
391
|
+
- 🔒 **投毒面焊成窄的**:事件体是白名单 `{ eventId?, type, payload }`(全是字符串,
|
|
392
|
+
多一个字段 → 400;`payload` 上限与 `maxBodyBytes` 同口径 → 413)—— 外部永远不能
|
|
393
|
+
构造消息块,引擎把它渲染成**一条 user 文本消息**,在未决 tool_use 解决之后注入
|
|
394
|
+
(直接追加到历史末尾会破坏续跑判定:`tailToolUses` 只认末尾一条)。
|
|
395
|
+
- **只对挂起生效**:不存在 → 404;非 `suspended`(含已终态)→ 409;先落库再派发
|
|
396
|
+
(崩在窗口里不丢事件);timer 挂起收到事件**提前醒**且不沿旧 `wakeAt`。
|
|
397
|
+
- **幂等**:给 `eventId` 就按它去重(`TaskRecord.deliveredEventIds`,随记录落库、
|
|
398
|
+
有界 FIFO)—— 重复投递 → 409;不给则重复投递 = 重复进历史(如实,不假装恰好一次)。
|
|
399
|
+
- **待注入缓冲也有上限(64 条)**:满了 → 409,且**不收下**(文案说清「本次事件没有被
|
|
400
|
+
记录」)—— 定案 B「说出来,不静默」。**不选「丢最旧」**:丢缓冲若不同时摘
|
|
401
|
+
`deliveredEventIds`,发件方重投会拿到「已投递」的 409 而事件其实已经没了(静默丢事件)。
|
|
402
|
+
- **留痕**:续跑段 run 根记 `task.event { injected, event_type, event_id? }`(口径是
|
|
403
|
+
「注入进本段消息流」—— 本轮改名,见上方 [Unreleased] 的 P3-2 条目);
|
|
404
|
+
离开挂起态时挂起读数照常除名。
|
|
405
|
+
- 新增导出:`TaskEvent`(类型);`RunInvocationOptions.events` / `TaskRecord.pendingEvents`
|
|
406
|
+
/ `TaskRecord.deliveredEventIds` / `AgentRunResult.eventsDelivered`(全部加法,无破坏性)。
|
|
407
|
+
`eventsDelivered` 是引擎对「本段把事件真注入了没有」的如实报告 —— 宿主据此清簿记,
|
|
408
|
+
保证注入后再次挂起也不会在下次续跑**重复注入**(复审探针抓出的缝,已闭合)。
|
|
409
|
+
- **sqlite 到期索引(spec §10 2026-09-28 ⑤ 落地)**:`TaskStore` 新增**可选**方法
|
|
410
|
+
`listDue?(before)`(接口加法,不破自定义 store),`SqliteTaskStore` 实现它
|
|
411
|
+
(派生列 `wake_at` + `(status, wake_at)` 索引;存量库构造期就地迁移并回填,
|
|
412
|
+
列漂移读时自愈)。`AsyncRunner` 的「到期唤醒」那一半扫描有索引走索引
|
|
413
|
+
(10k 记录:全表 list ~38ms → listDue ~0.1ms),没有回退全表、语义不变。
|
|
414
|
+
InMemory / File store **不实现**(它们的 list 本来就在内存里)。
|
|
415
|
+
⚠️ `resumePending` 的其余三条职责(挂起读数重建 / 审批超时 / 孤儿认领)仍以全表
|
|
416
|
+
`list()` 为输入 —— listDue 只替代到期唤醒那一半。
|
|
417
|
+
|
|
418
|
+
- **时间挂起(durable timer,spec §10 2026-09-28 ②)**:工具可以在**执行期**调
|
|
419
|
+
`ctx.deferUntil(at)` 说「现在还不是时候,T 之后再问我」—— 引擎把该回合收尾成挂起
|
|
420
|
+
(`stopReason: 'suspended'` + `suspendedReason: 'timer'` + 目标时刻 `wakeAt`),
|
|
421
|
+
到点由宿主续跑并**重跑这一批**工具。用途:等批处理作业、等限流窗口、等外部系统回填。
|
|
422
|
+
- ⏱️ **时刻必须是将来**:非有限数或 `at <= now` 当场抛 `TypeError`(该条 `tool_result`
|
|
423
|
+
记 is_error、run 照常往前走,**不**挂起)—— 允许过去时刻会让「醒来 → 再请求同一个过去
|
|
424
|
+
时刻」自己打转。同回合多条请求取**最早**的那个。
|
|
425
|
+
- ⚠️ **整批语义**:挂起是回合级的(协议要求每个 `tool_use` 都有配对 `tool_result`),
|
|
426
|
+
所以同回合**已经执行完**的其他工具会在醒来后**重跑**(副作用重复)。框架把它变成看得见的:
|
|
427
|
+
trace 记 `defer.requested { wake_at, tool_use_ids, discarded }`,真有兄弟工具被作废时
|
|
428
|
+
落一条 `console.warn`。要精确控制就让模型单独调它,或把它做成幂等读。
|
|
429
|
+
- 🩺 **可见性**:`GET /healthz` 新增 `suspended: { approval, timer, nextWakeAt }`
|
|
430
|
+
(本进程口径,与 `inFlight` 同一张表;无时间挂起时 `nextWakeAt` 是 `null` 不是 `0`),
|
|
431
|
+
另有 `AsyncRunner.suspendedSummary`。
|
|
432
|
+
- 🛑 **停机**:`drain()` 之后**不再唤醒**睡着的 run(停机 = 不再往前推);重启后由新进程的
|
|
433
|
+
首次 `resumePending` 唤醒。取消用 `AsyncRunner.cancel` / `POST /tasks/<id>/cancel`
|
|
434
|
+
(见下「取消 API」)—— 对睡着的 run 它就是「不再醒」的合法出路。
|
|
435
|
+
⚠️ 这道闸(2026-09-28 复审第二轮补齐)覆盖**三条**把挂起任务推进起来的路径:
|
|
436
|
+
到期唤醒 / `approve` / `signalTask` —— 三者形状相同(先落库成 `running`、再派发),
|
|
437
|
+
落库与派发之间隔着 store 往返的窗口,`drain()` 若在窗口里完成,那条任务会在
|
|
438
|
+
**停机完成之后**才开跑。窗口过后若已在停机:不派发,记录留给下次启动的
|
|
439
|
+
`resumePending` 认领(决定 / 事件都在记录里,不丢)。此前只有到期唤醒那一支有闸。
|
|
440
|
+
- ⛔ **abort 优先于延后**:同回合里工具请求了延后、而 run 又被中止(`runTimeoutMs` 到点 /
|
|
441
|
+
`cancel`),run 以 `aborted` 收尾、**不**挂成 timer —— 否则会到点自己醒来接着跑,
|
|
442
|
+
等于把取消/超时吃掉。
|
|
443
|
+
- 新增字段(**全部加法,无破坏性**):`ToolRunContext.deferUntil`、`AgentRunResult.wakeAt`、
|
|
444
|
+
`RunMeta.wakeAt`、`TaskRecord.wakeAt`、`HealthResponse.suspended`。另顺手修两处:
|
|
445
|
+
`resumePending` 跳过挂起记录时的原因不再是 `'terminal'` 而是 `'suspended'`(诊断不说错话);
|
|
446
|
+
时间挂起醒来时的续跑段**不再**重复注入会话历史、且 trace 会 link 上一段 run。
|
|
447
|
+
- **旧任务记录的读时归一(迁移垫片,spec §10 2026-09-28 ③)**:store 读回记录时会把 0.9.5 之前
|
|
448
|
+
落库的旧形状归一 —— 状态值 `awaiting_approval` → `suspended` 并补 `suspendedReason: 'approval'`,
|
|
449
|
+
挂起时刻 `approvalPendingSince` → `suspendedSince`(旧键删掉)。**升级不再需要宿主手工改库**:
|
|
450
|
+
此前那种记录读回来会被 `isTerminalTask` 判成终态、`resumePending` 按 `'terminal'` 跳过 ⇒
|
|
451
|
+
一条在等审批的 run 成孤儿(既不续跑、也无法再被审批)。
|
|
452
|
+
- 六个「bytes → 记录」点(`FileTaskStore` 全量扫 + 残行探测、`SqliteTaskStore` 的
|
|
453
|
+
get / byIdempotency / list、`RedisTaskStore` 单键读)收成**唯一入口** `src/store/record.ts`;
|
|
454
|
+
静态守卫:`src/store/*.ts` 里除它之外不得直接 `JSON.parse`。
|
|
455
|
+
- `SqliteTaskStore` 额外把派生列 `status` 拉回与 json 一致 —— 否则外部/DBA 的
|
|
456
|
+
`SELECT status, count(*) FROM tasks GROUP BY status` 会**继续**报旧值。
|
|
457
|
+
- 归一失败/坏 JSON 的取舍与各 store 既有口径一致;本改动**无公共 API 变化**。
|
|
458
|
+
- **取消 API(spec §10 2026-09-28 ④)**:`AsyncRunner.cancel(taskId)` 与 `POST /tasks/<id>/cancel`
|
|
459
|
+
—— 在跑的**真中断**、在睡的**不再醒**、在排队的**绝不起跑**,落库 `status: 'cancelled'`
|
|
460
|
+
(**新状态**,与 `failed` 分开:取消不是失败)。
|
|
461
|
+
- 机制是同一条 abort signal,差别在**意图**:`runTimeoutMs` 超时仍然落 `failed`
|
|
462
|
+
(`error.type === 'timeout'`);取消带 `error.type === 'aborted'`。
|
|
463
|
+
- **不假装**:在跑的 run 不在本进程、或宿主不认 `signal`(2s 宽限内没收尾)⇒
|
|
464
|
+
`TaskCancelError(409)`,记录一个字节不动。
|
|
465
|
+
- 顺带补一个洞:排队期间被取消的任务原先**照样会跑**(认领处不重判状态)。
|
|
466
|
+
- 复审再补两格:abort 后 **reject** 的宿主(包 fetch 类客户端的常见写法)也按意图落
|
|
467
|
+
`cancelled`(不落 `failed`);同幂等键重提一条被取消的任务会**真跑**
|
|
468
|
+
(去重不再把 `cancelled` 当「已有」)。
|
|
469
|
+
- 另:`awaitTask` 的终态集合改成 `isTerminalTask`(原先手写两值 ⇒ 新终态被漏掉,
|
|
470
|
+
症状是「取消后一直等到超时」)。
|
|
471
|
+
- **菜单漂移不再静默(R8 候选 3,spec §10 2026-09-27 ⑧)**:挂起段之后**续跑**时,未决
|
|
472
|
+
tool_use 引用的工具若已不在当前菜单(删了 / 改名了),框架把这件事记成**三处信号** ——
|
|
473
|
+
`menu.drift` 事件(`{ missing, tool_use_ids, menu_size }`;时间线与
|
|
474
|
+
`GET /tasks/:id/stream` 都看得到)、父 span 的 `menu.drift` attribute(如 `missing:danger`)、
|
|
475
|
+
一条 `console.warn`。⚠️ **run 照常收尾**(不判失败:挂起是合法态、改代码是发布常态 ——
|
|
476
|
+
理由与「严格模式为什么不给」见 spec §10 2026-09-27 ⑧)。另新增 run 根 attribute
|
|
477
|
+
`tools.names` / `tools.menuHash`(装配后菜单的名字清单 + 名字与**输入 schema** 的摘要,
|
|
478
|
+
与 `prompts.versions` 同动机:质量回归能定位到具体菜单版本)。口径:判据只看名字;签名与
|
|
479
|
+
菜单顺序、schema 键序无关,`description` **不参与**。**公共 API 零变化**。
|
|
480
|
+
- **MCP 反向桥(`createMcpServer`,R8-P5)**:把 app 的能力菜单(装配后、过中间件的那份)
|
|
481
|
+
暴露成 MCP server —— Claude Code / Cursor / 任何 MCP 宿主能直接调你的 `@Tool`。
|
|
482
|
+
传输二选一(都只用标准库):`stdio`(换行分隔 JSON-RPC,日志只去 stderr)与
|
|
483
|
+
StreamableHTTP(POST 收报文回 `application/json`;`initialize` 铸 `mcp-session-id`
|
|
484
|
+
头但**不校验**(无状态 server,宽容是有意的);GET → 405、DELETE → 200;客户端断连
|
|
485
|
+
中止该次调用的 signal;`auth` 钩子只给缝 —— 读 body 之前、抛错即 401,与
|
|
486
|
+
`createHttpHandler` 同纪律)。协议范围只到 tools(initialize / tools/list / tools/call
|
|
487
|
+
+ ping)。**trace 叙事不破**:每次 tools/call 造一棵 trace(run 根 `mcp.tools/call` +
|
|
488
|
+
capability span + 与引擎同形状的 `tool.input`/`tool.output` 事件)投递 `opts.sinks`;
|
|
489
|
+
结果映射与正向桥方向对称(抛错 → 协议层成功 + `isError: true`)。公共面新增
|
|
490
|
+
`createMcpServer` 与 `McpServerApp` / `McpServerOptions` / `McpServer` 类型。
|
|
491
|
+
决策见 spec §10 2026-09-27 ⑦。
|
|
492
|
+
- **租户归因 labels(`labels: Record<string, string>`,R8-P4)**:`createApp` 缺省 +
|
|
493
|
+
`app.run` 单次覆盖(**整体替换**不合并)+ `runAgent` 直连三层同语义。落 run 根的
|
|
494
|
+
`labels.<key>` 属性(trace 侧无基数问题;与框架自写的 `source` 触发来源审计正交);
|
|
495
|
+
可序列化,异步任务随 TaskRecord 落库、续跑不丢;键空 / 值非字符串在 run 入口抛
|
|
496
|
+
TypeError。进 metrics 是**另一个开关**:`metricsSink({ labelKeys, maxLabelValues? })`
|
|
497
|
+
显式点名哪些键上指标标签(缺省一个都不上),每键相异值数封顶(缺省 100、
|
|
498
|
+
必须为正数),超出折叠进 `__other__`(只丢粒度不丢量),被折叠数见
|
|
499
|
+
`snapshot().droppedLabelValues` 与 `dropped_keys{kind="label:<key>"}`;四个 run 级
|
|
500
|
+
家族(`runs_total` / `runs_failed_total` / `tokens_total` / `cost_usd_total`)在全局
|
|
501
|
+
样本外追加带标签样本(⚠️ 开了以后 `sum(agentia_runs_total)` 会重复计数,总量用
|
|
502
|
+
不带标签的序列)。公共面新增 `RunLabelMetrics` 类型。决策见 spec §10 2026-09-27 ⑥。
|
|
503
|
+
- **CLI `agentia export`:trace 落盘文件 → 训练数据集**(JSONL,一行一份
|
|
504
|
+
`{ messages, meta }`;R8-P3b)—— harvest(产回归用例)的孪生。开了
|
|
505
|
+
`traceContent: 'full'` 的 run 导出带真 assistant 文本的完整对话;没开的导出工具
|
|
506
|
+
轨迹,缺文本**不造占位**(占位文本进训练数据是投毒),缺口进 `meta.incomplete`。
|
|
507
|
+
过滤:`--ok-only` / `--min-score n`(没带分数的 run 被排除:没判过 ≠ 及格);
|
|
508
|
+
`--out` 落盘(stdout 是产物的纪律同 harvest)。框架侧 `src/eval/export.ts` 的
|
|
509
|
+
`exportRun` 是 module 级(不进公共面,同 harvestEvalCase 纪律),CLI 侧为去类型
|
|
510
|
+
移植副本 + 逐字对拍守护。决策见 spec §10 2026-09-27 ⑤。
|
|
511
|
+
- **opt-in 记录 assistant 文本进 trace(`traceContent: 'full'`)**:每回合的模型文本落
|
|
512
|
+
该 llm.turn span 的 `output.text` 属性(多块 `\n` 连接;过 `maxEventChars` 同一道
|
|
513
|
+
截断闸 —— 它管「多长」,`traceContent` 管「记不记」)。缺省不记,现状逐字不变;
|
|
514
|
+
纯 tool_use 回合不记。透传子 agent / skill 子循环(`forwarded.ts` 同树同口径)。
|
|
515
|
+
run 根快照记 `config.traceContent`。⚠️ 实测代价:3 回合、每回合约 1600 字符输出的
|
|
516
|
+
run,trace 体积 5 564 → 11 010 字节(约 2×);且模型输出从此进入要脱敏的面 ——
|
|
517
|
+
出库前走 `docs/observability.md` 配方 2.4。这是「trace → 训练数据导出」(R8-P3b)
|
|
518
|
+
的引擎侧前提。决策见 spec §10 2026-09-27 ④。
|
|
519
|
+
- **模型 fallback 链(引擎级,`fallbacks: [{ model, client? }]`)**:`createApp` 缺省 +
|
|
520
|
+
`app.run` 单次覆盖 + `runAgent` 直连三层同语义。主模型本回合最终失败(含其
|
|
521
|
+
`maxRetries` 用尽)且错误可换(`classifyError` 的 retryable 类:rate_limit / server /
|
|
522
|
+
timeout / connection)时按序换环重试本回合 —— **每一环开自己的 llm.turn span**
|
|
523
|
+
(model 名正确 ⇒ 成本归因与 `usage.unpriced` 探测天然对),切换在新 span 记
|
|
524
|
+
`llm.fallback { from, to, errorType }` 事件,run 根快照记 `config.fallbacks`。
|
|
525
|
+
护栏:`aborted` 永不换(用户取消不是故障)、本回合吐过字不换(与 retry 的
|
|
526
|
+
`!emitted` 同一护栏)、每回合从主环重新起;子 agent / skill 子循环不继承。
|
|
527
|
+
链环 `client` 缺省复用本次 run 的 client(同端点换模型是主用例);run 入口校验
|
|
528
|
+
坏环/死 client(持久化反序列化空壳)响亮抛 TypeError。决策见 spec §10 2026-09-27 ③。
|
|
529
|
+
- **`jsonlTraceSink({ path })`:JSONL 文件 sink 进框架** —— CLI 三件套(`agentia report` /
|
|
530
|
+
`diff` / `harvest`)消费 `trace.jsonl`,而产出侧此前要用户手写 `appendFileSync`(usage-guide
|
|
531
|
+
曾这么教)。现在一行接入:`createApp({ sinks: [jsonlTraceSink({ path: 'trace.jsonl' })] })`。
|
|
532
|
+
- **metrics exemplars(指标 ↔ trace 互跳)**:`metricsSink` 记账时跟踪两个代表性现场 ——
|
|
533
|
+
`runs_failed_total` 挂**最近一次失败** run、`run_duration_ms` histogram 挂**迄今最慢** run
|
|
534
|
+
的 traceId/spanId(`snapshot().exemplars` 可见;`export: 'openmetrics'` 与 `'otlp'`
|
|
535
|
+
两个出口会挂上,缺省 `prometheus` 输出逐字节不变)。Grafana 配好 exemplar 跳转后,
|
|
536
|
+
指标尖峰可以一键跳到那条 trace(配置见 `docs/observability.md` §4)。
|
|
537
|
+
内置 `GET /metrics` 路由的 Content-Type 跟 sink 的 `contentType` 走 —— openmetrics 模式
|
|
538
|
+
直接喂给 `createHttpHandler({ metrics })` 即可,不用自己挂路由。
|
|
539
|
+
- **摘要渲染折叠纯载荷长串**(#147):长上下文摘要器此前有三条封顶(图片占位 / 未知块 /
|
|
540
|
+
参数截断),但工具把 base64 这类载荷当**字符串**返回时整段进摘要器。现在连续 ≥4000
|
|
541
|
+
字符的载荷串折成 `⟨载荷 N 字符已折叠⟩` —— 只影响摘要渲染,trace 本体与 token 估算不变。
|
|
542
|
+
(PEM 折行载荷够不着下限,已知边界已在文档声明。)
|
|
543
|
+
|
|
544
|
+
### 变更
|
|
545
|
+
|
|
546
|
+
- **挂起改成「一个状态 + 一个原因」:`awaiting_approval` → `suspended` + `suspendedReason`**
|
|
547
|
+
(spec §10 2026-09-28 ①;为 durable timer 铺路):`RunStatus` 与 `AgentStopReason` 的成员名
|
|
548
|
+
都改成 `'suspended'`,「为什么挂起」由新字段承载 —— `'approval'`(等人工决定)/
|
|
549
|
+
`'timer'`(等一个时刻)。两条判据因此落在**原因**上:`approvalTimeoutMs` 只对 `approval`
|
|
550
|
+
成立、`approve` 对 `timer` 挂起一律 409 —— 否则一条等时刻的 run 会被「审批超时」提前叫醒
|
|
551
|
+
并重派。`TaskRecord.approvalPendingSince` 改名 `suspendedSince`(它本来就是挂起时刻)。
|
|
552
|
+
新增导出 `SuspendedReason`(`api.html` 计数 224 → 225)。**破坏性** —— 迁移见下。
|
|
553
|
+
- **脱敏配方 2.4 升级**(`examples/observability` 的 `redactSink`,框架 `src/` 零改动):
|
|
554
|
+
新增内置正则预设(Bearer / JWT / AWS access key / LLM `sk-` key / 邮箱 / 手机号),
|
|
555
|
+
**缺省全开**(拷走即用),`presets` 可开子集或 `false` 全关;预设命中的替换文案带类别
|
|
556
|
+
标签(`[REDACTED:email]`),自定义 `patterns` 与 `keys` 命中的仍是裸 `[REDACTED]`。
|
|
557
|
+
脱敏不内建进框架是已锁定决策(spec §9.3 / §10 2026-09-14 ⑥),这是配方层的升级。
|
|
558
|
+
- **sink 投递失败不再完全静默**:`flushSinks` 吞掉 sink 异常的纪律不变(观测不击穿业务),
|
|
559
|
+
但吞之前现在会落一条 `console.warn`(文案含「trace sink」,可 grep)——「观测的观测」
|
|
560
|
+
此前是零信号:sink 天天挂、面板一切如常。决策见 `docs/spec.md` §10 2026-09-27 ②。
|
|
561
|
+
|
|
562
|
+
### 重构(纯结构,零行为变化)· 结构收口(2026-09-28,底盘见 `docs/reviews/2026-09-28/SRC-STRUCTURE-2026-09-28.md` 的落地顺序 1–4)
|
|
563
|
+
|
|
564
|
+
- **HTTP 宿主拆成三件 —— `http.ts` 拆分第三步**:`src/transport/http.ts` **840 → 306 行**,
|
|
565
|
+
最大单函数 `createHttpHandler` **489 → 123 行**(拆分前这一个函数占全文 58%)。
|
|
566
|
+
同系列前两步(#99 外移形状口径 → `http-shapes.ts`、#100 外移路由判定 → `http-route.ts`)
|
|
567
|
+
移走的都是**纯件**,占大头的那一半(九条路由的**体**)原地不动 —— 于是文件从 533 行又长回
|
|
568
|
+
840。这一步把它搬走:
|
|
569
|
+
- `http-endpoints.ts`(新)= **端点体**:派发表 `handleRoute` + 九条 `handleXxx`
|
|
570
|
+
(读 body → 调 runner → 写响应 → 选状态码)。免鉴权组(`/healthz` / `/metrics`)连同
|
|
571
|
+
它们自己的 405 也在这张表里 —— 表**不认识鉴权**,闸在宿主侧(`handler` 里那两行否定
|
|
572
|
+
条件:「不是免鉴权组才过闸」)。
|
|
573
|
+
- `http-io.ts`(新)= **收发原语**:`sendJson` / `readBody` / `parseJsonBody` / 405 / 413 /
|
|
574
|
+
503 / 500 的机械动作。**零内部依赖**(只 import `node:http` 的类型)⇒ 宿主与端点都引它
|
|
575
|
+
而**不可能**成环 —— 无环是构造性的,不是碰巧。
|
|
576
|
+
- `http.ts` = 宿主契约 + 宿主状态 + 准入/停机。原先散在闭包里的三个 `let`(在飞 run 计数 /
|
|
577
|
+
停机态 / SSE 收口表)收成一个**有名字的对象** `HttpState`,按引用交给每个请求 ——
|
|
578
|
+
「健康检查与停机判断看的是同一个数」这条承诺从此有个明主。
|
|
579
|
+
- **行为零变化**:既有 HTTP 套件 146/146 未改一字全过;公开面(`src/index.ts`)一行未改
|
|
580
|
+
(`HealthResponse` 随实现搬走并在 `http.ts` 转出,与 #99 转出两个形状类型同款)。
|
|
581
|
+
- **配源码级守卫** `tests/transport/http-boundary-guard.test.ts`:端点体绕不开的五个记号
|
|
582
|
+
(`route.kind` / `sse.event(` / 三个形状解析器)必须在端点文件、必须不在宿主文件;准入
|
|
583
|
+
判定只许留在宿主;原语层不许长内部依赖;派发表的穷尽断言必须在场。反向验证过三条
|
|
584
|
+
(放回拆分前的文件 / 删掉穷尽断言 / 给原语层加内部依赖 —— 各恰好一条红)。
|
|
585
|
+
- 顺带修好一条**盯着实现排布而非契约**的守卫:`tests/docs/sse-frames.test.ts` 原先写死扫
|
|
586
|
+
`src/transport/http.ts` 的 `sse.event('…')`,帧一搬家就误红(帧一条没少)—— 改成扫整个
|
|
587
|
+
`src/transport/` 目录。断言方向是「文档提到的 ⊆ 实现 emit 的」,所以扫宽不会假绿。
|
|
588
|
+
|
|
589
|
+
- **MCP 反向桥拆成三件 —— `createMcpServer` 441 → 47 行**:`src/engine/mcp-server.ts`
|
|
590
|
+
**564 → 409 行**,最大单函数 `createMcpServer` **441 → 47 行**(拆分前占全文 78%)。
|
|
591
|
+
先例是正向桥按传输分文件,于是:
|
|
592
|
+
- `mcp-server.ts` = **协议 + 执行 + 装配**:顶层的 `rpcError` / `rpcResult` / `dispatch`
|
|
593
|
+
(报文分派,两传输共用)/ `callTool`(执行一次 `tools/call`、造一棵 trace)—— 拆前它们是
|
|
594
|
+
441 行闭包里的无名块。`createMcpServer` 只余「读选项 → 造 core → 按 `opts.transport` 挑传输」。
|
|
595
|
+
- `mcp-server-stdio.ts`(新,72 行)= stdio 传输:stdin/stdout 换行分隔 JSON-RPC,
|
|
596
|
+
`process.stdout` 的 EPIPE 必须吞(对端走了的次生现象)。
|
|
597
|
+
- `mcp-server-http.ts`(新,199 行)= StreamableHTTP 传输:POST 收 JSON-RPC、GET→405、
|
|
598
|
+
DELETE→200、`initialize` 发 `mcp-session-id`、客户端断连中止本次工具调用的 `signal`。
|
|
599
|
+
- **无环是构造性的(不照抄正向桥)**:协议与执行经 `McpCore` **注入**给传输,传输只从
|
|
600
|
+
`mcp-server.ts` 取**类型**。正向桥的 `mcp.ts ↔ mcp-stdio.ts` 是**真实的值环**(靠 ESM 函数
|
|
601
|
+
提升侥幸无恙);反向桥刻意做成注入式单向。
|
|
602
|
+
- **「为什么留在 engine」复核仍成立**:本文件需 engine 侧 4 个值 + integrations 侧 2 个值,
|
|
603
|
+
而 integrations 只许依赖 core ⇒ engine 是唯一能同时够到两边的层(搬走会造出越权边与环)。
|
|
604
|
+
- **不再往下拆传输**(反例):`startHttpTransport` 172 行 / `startStdioTransport` 57 行属
|
|
605
|
+
「一个宿主的生命周期」,该是一个协作者,且比正向桥同位函数(265 / 235 行)更瘦。
|
|
606
|
+
- **行为零变化**:MCP 反向桥单测 22/22 全过,`npm run e2e:mcp:server` 全绿。
|
|
607
|
+
|
|
608
|
+
- **`#executeInner`(247 行)的编排归属 = 定案「不拆」**(**零代码改动**,产出是一份有依据的裁定):
|
|
609
|
+
`async.ts` 最大可调用体 247 ÷ 总行 1690 = **14.6%**、可调用体 **40** 个 ⇒ 按判据属「**大类**」病
|
|
610
|
+
(抽协作者,**别切方法**),而纯协作者已抽过六轮(`slot-pool` / `approval-policy` / `drain-gate` /
|
|
611
|
+
`resume-policy` / `task-waiters` / `task-events`)。它触及 **14 个 runner 内部成员**,外移编排
|
|
612
|
+
等于重建 `AsyncRunner`;且调用结构**早已被** `tests/transport/dispatch-guard.test.ts` **钉死**
|
|
613
|
+
(`#dispatch` = 派发口 / `#execute` = 计数与开流包裹层 / `#executeInner` = 状态机主体)。
|
|
614
|
+
唯一成块的纯件 `callOpts` 装配(~42 行)因「单调用者 + 不产生可守卫边界」也判为不外移。
|
|
615
|
+
|
|
616
|
+
- **文件级无环守卫 + 断开 MCP 正向桥的**真实值环** —— 第 4 件**:
|
|
617
|
+
- 新增 `tests/architecture/file-cycles.test.ts`:`layering.test.ts` 的 `layerOf()` 取路径第一段,
|
|
618
|
+
**层内环是盲区**;本守卫建**文件级值边图**(Tarjan SCC)断言**运行期无环**。口径只算**值边** ——
|
|
619
|
+
`import type` 运行期擦除、不构成环(`runtime/context ↔ run`、`engine/mcp-server ↔
|
|
620
|
+
mcp-server-{stdio,http}` 两条 type-only 环**留着**)。两条变异反向验证过(注入值边 / 把真实
|
|
621
|
+
`import type` 改成值导入 ⇒ 各恰好 1 条红);另加「注释里的导入字面量不算边」自证。
|
|
622
|
+
- 断环:桥(`integrations/mcp.ts`)与两个连接器**共用**的协议面(7 helper + 4 结构类型)
|
|
623
|
+
抽到新文件 `src/integrations/mcp-protocol.ts` —— 此前 `mcp.ts` re-export 连接器(值)+
|
|
624
|
+
连接器反向取 helper(值)是一条**真实的值环**(靠 ESM 函数提升侥幸无恙)。抽后成单向 DAG,
|
|
625
|
+
**公共面不变**(`src/index.ts` 未改;`mcp.ts` 继续转出这些符号)。
|
|
626
|
+
- ⚠️ 盘点报告初稿写「两条环的回边都是 `import type`」**是错的** —— 实测 `mcp.ts:350/352` 是
|
|
627
|
+
**re-export 值**。结论与修法见 `docs/spec.md` §10 2026-09-28 ⑩ 第 4 条。
|
|
628
|
+
|
|
629
|
+
### 迁移
|
|
630
|
+
|
|
631
|
+
- **升级前停在 `awaiting_approval` 的任务记录**(`FileTaskStore` / `SqliteTaskStore` /
|
|
632
|
+
`RedisTaskStore` 里已写好的 JSON):**本版内置兼容读,宿主不需要做任何动作** —— store 读回记录
|
|
633
|
+
时会把旧形状归一(`awaiting_approval` → `suspended` 并补 `suspendedReason: 'approval'`;
|
|
634
|
+
`approvalPendingSince` → `suspendedSince`,旧键删掉),`SqliteTaskStore` 还会把派生列 `status`
|
|
635
|
+
一并拉正。细节与门禁见 `docs/spec.md` §10 2026-09-28 ③。
|
|
636
|
+
⚠️ 本小节此前写的是「框架**不**内置兼容读、请宿主在升级前手工改库」—— 垫片落地后那段要求
|
|
637
|
+
**已作废**(手工处置仍可行,但不再是必须;在等审批的 run 不会再被判成终态)。
|
|
638
|
+
`SuspendedReason` 仍然是**类型**而不是别名:不为旧值引入第二个运行时名字。
|
|
639
|
+
|
|
8
640
|
## [0.9.4] - 2026-09-26
|
|
9
641
|
|
|
10
642
|
> 本版主题(窗口 `0.9.3 → 0.9.4`):**成本与限制的账本对齐 + 「能不能发」有了判据**。
|
|
@@ -27,6 +659,33 @@
|
|
|
27
659
|
- **`tool_use` 的参数同样有界**:同一类「大载荷灌进摘要器」的洞,两处各配回归用例
|
|
28
660
|
(含「小参数不许被截断」的阳性对照),变异验证 4/4 咬人。
|
|
29
661
|
|
|
662
|
+
### 变更 · 复审收口(2026-09-27,九条「测试没覆盖的缝」)
|
|
663
|
+
|
|
664
|
+
> 来源:对 R8 五项实现(P1–P5)的逐条复审 —— 单测全绿、全链 8/8,问题全在用例之外。
|
|
665
|
+
> 每条的取证与复现读数记在各自提交里;下面按模块列**行为变化**。
|
|
666
|
+
|
|
667
|
+
- **MCP 反向桥三处**:① `tools/call` 现在走引擎**同一份** `inputSchema` 校验器,入参不合法
|
|
668
|
+
回 `-32602` 且方法体零调用(此前缺必填项被当成功调用,实测返回 `你好,undefined`);
|
|
669
|
+
② `auth` 钩子提到方法/路径判定**之前**(此前未鉴权能拿到 `405 allow: POST, DELETE` 与
|
|
670
|
+
`DELETE → 200`,与 `createHttpHandler` 的「其余先鉴权」纪律不符);③ stdio 的 stdout
|
|
671
|
+
挂了 `error` 守卫吞 EPIPE(此前宿主先关读端会把 server 打成栈回溯 + exit 1;正向连接器
|
|
672
|
+
对子进程 stdin 一直是这么吞的)。
|
|
673
|
+
- **`metricsSink` 归因标签补上第四道基数上限 `maxLabelCombos`**(缺省 200,进 limits 真源表):
|
|
674
|
+
`maxLabelValues` 只封每个键的**值域**,而进内存的是键的**组合**(叉乘)—— 缺省 100 值域
|
|
675
|
+
配 3 个键 = 1,030,301 条常驻,且 `droppedLabelValues` 看不见它。超限的**新组合**折进一个
|
|
676
|
+
全 `__other__` 的桶(量不丢),折叠数经 `snapshot().droppedLabelCombos` 与
|
|
677
|
+
`dropped_keys{kind="label:combos"}` 可见。**单键配置(最常见)组合数 ≈ 值数 ⇒ 行为不变。**
|
|
678
|
+
- **标签 combo 身份不再裸拼接**:值里出现 `,`/`=`/`\` 时会给 `\` 前缀转义 —— 此前两个不同
|
|
679
|
+
标签集能拼出同一个键(实测 `a="x,b=y",b="z"` 与 `a="x",b="y,b=z"` 并成一本账、且按前者
|
|
680
|
+
的标签渲染,后者的量被错配)。正常值(不含这三个字符)的键与展示形**逐字不变**。
|
|
681
|
+
- **`agentia export` / `exportRun` 三处**:① 汇总现在**也**在 stdout 模式打(走 stderr)——
|
|
682
|
+
此前 `agentia export x.jsonl > dataset.jsonl` 静默丢坏行、而 `--out` 形态与 `report` 都会报;
|
|
683
|
+
② `incomplete` 新增 `assistant-text` 的**整棵 trace 无正文**判据(混合回合的真文本此前静默
|
|
684
|
+
缺席却不当缺口)与 `no-final-assistant`(末条 user ⇒ 无 loss 目标);③ 侧产物与 CLI 移植
|
|
685
|
+
副本在「trace 缺 `status` / span 缺 `attributes`」的裸 trace 上不再分叉(框架侧此前会抛)。
|
|
686
|
+
**⚠️ 迁移注意**:`meta.incomplete` 会多出 `no-final-assistant` 这个词 —— 按等值断言
|
|
687
|
+
`incomplete` 的消费方要放宽成「包含」或补上这个词。
|
|
688
|
+
|
|
30
689
|
### 变更 · 三个旋钮的 `0` 从「静默失效」改成构造期报错
|
|
31
690
|
|
|
32
691
|
- **`Scheduler.every.maxInFlight: 0`**:闸门判据是 `inFlight.size >= maxInFlight`,`0` / 负数时
|
|
@@ -41,9 +700,10 @@
|
|
|
41
700
|
|
|
42
701
|
- **`ModelPricing` 新增 `cacheRead` / `cacheWrite`**(缺省读 0.1×、写 1.25×;**1h 档写 2×** 由
|
|
43
702
|
`priceOverrides` 表达),非法乘数(负数 / NaN)同样构造期抛错。
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
703
|
+
- ~~未定价模型会「响」~~ **(订正 2026-09-27:这条不是本版新增,发布时误记)**:
|
|
704
|
+
`usage.unpriced` 事件 / 指标 `model_unpriced_turns_total` / `onUnpricedModel` 回调自
|
|
705
|
+
**v0.6.0** 起就在(#75),本窗口对它们零改动。本版真实的成本口径改动只有上面那条
|
|
706
|
+
cache 乘数与下面的图片估算。条目不删、留痕。
|
|
47
707
|
- **图片块的 token 按尺寸上界估**:官方按**尺寸**计费(28×28 像素 = 1 visual token),与文件字节数
|
|
48
708
|
无关;框架取「长边缩到 1568px」的上界 3136 token/块 —— 宁可高估(`maxTotalTokens` 提前拦)
|
|
49
709
|
也不低估(护栏迟触发)。
|
|
@@ -66,6 +726,29 @@
|
|
|
66
726
|
|
|
67
727
|
### 仓库自身(不面向使用者)
|
|
68
728
|
|
|
729
|
+
- **补两份「口径单测」**(`tests/integrations/mcp-protocol.test.ts` + `tests/transport/http-io.test.ts`,
|
|
730
|
+
2026-09-28,结构收口的尾巴):这两组 helper 是「桥 + 两个连接器」与「宿主 + 端点」的**共用面**,
|
|
731
|
+
抽成独立文件(断值环 / 拆路由体)后**没有同名单测** —— 行覆盖靠既有套件就够(都是 100% 行),
|
|
732
|
+
但**口径**从没被正面钉过。按 `http-shapes.test.ts` / `http-route.test.ts` 的先例补两份、共 35 条。
|
|
733
|
+
**不追覆盖率**:纯防御分支**刻意不补** —— 典型是 `readBody` 的 `if (done) return`
|
|
734
|
+
(Promise 二次 `resolve` 是 no-op,少这行行为完全一样),为它写断言就是真空变绿。
|
|
735
|
+
⚠️ 同一轮查清一个会误导人的读数:`src/engine/mcp-server-stdio.ts` 报 **22.22% 行覆盖**不是缺口,
|
|
736
|
+
是**采集假象**(用例走真子进程,c8 只采父进程;实测该用例 22/22 全过),且该数字拆分前就存在。
|
|
737
|
+
变异验证 17/17 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ⑩ 第 5 条。
|
|
738
|
+
|
|
739
|
+
- **补 7 条「正常输入下可达、却从没走过」的功能分支**(2026-09-28,同轮尾巴;跨 3 个测试文件 +8 条用例):
|
|
740
|
+
先纠正一个读法 —— c8 的 branch **不是 if/else 两条臂**,而是「**代码块**」(全仓条目**全是
|
|
741
|
+
1 个 location**,`b[id]` = 该块被进入的次数,`0` = 这条路径**从未走到**),所以它比语句覆盖细。
|
|
742
|
+
据此排查后补:`toolkit/subagent.ts` 的 **`spec.system` 函数形态**(三态联合里唯一「框架不追加
|
|
743
|
+
REPORT_HINT、作者全权」的一态);`engine/trace-diff.ts` 的 **status / error 差异**三条判定
|
|
744
|
+
(顺带带出 `errorEqual` 里那句从未执行过的三元组比较,并钉住「比内容不比对象引用」);
|
|
745
|
+
`transport/async.ts` 的**缓冲溢出** `truncated` 帧与「**已终态后**才连上来」的 `end` 收口;
|
|
746
|
+
`transport/http-endpoints.ts` 的 SSE `stream.truncated` 帧。
|
|
747
|
+
**分母不变大的前提下**:语句 295 → **287** 未覆盖、块 347 → **338**、分支 91.82% → **92.04%**,
|
|
748
|
+
用例 1465 → **1473**。函数那一档**刻意不动**(那 11 个「从未被调用的函数」全是只读访问器,
|
|
749
|
+
另有一个 `TaskEventStreams.forget` 是**未导出的内部死方法** —— 为它写测试等于锁死死代码)。
|
|
750
|
+
变异验证 7/7 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ⑩ 第 6 条。
|
|
751
|
+
|
|
69
752
|
- **`packages/cli/test/inspector.test.mjs` 新增 ⑫「选完文件夹 → 透传给 run 的就是所选目录」**:
|
|
70
753
|
2026-09-24 真用户反馈「选完之后透传给 agent 的不是所选文件夹」,逐环核实后**链路是通的**
|
|
71
754
|
(`createApp` 把显式 `providers` 拼在 discover 结果**之后** ⇒ 模板的 `WORKDIR = opts.workdir`
|
|
@@ -1459,7 +2142,8 @@
|
|
|
1459
2142
|
首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
|
|
1460
2143
|
框架本体单包;CLI 独立成包(workspaces)。
|
|
1461
2144
|
|
|
1462
|
-
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.
|
|
2145
|
+
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.9.5...HEAD
|
|
2146
|
+
[0.9.5]: https://github.com/retrychx/agentia/releases/tag/v0.9.5
|
|
1463
2147
|
[0.9.4]: https://github.com/retrychx/agentia/releases/tag/v0.9.4
|
|
1464
2148
|
[0.9.3]: https://github.com/retrychx/agentia/releases/tag/v0.9.3
|
|
1465
2149
|
[0.9.2]: https://github.com/retrychx/agentia/releases/tag/v0.9.2
|