niceeval 0.10.3-canary.7 → 0.11.0

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.
Files changed (121) hide show
  1. package/dist/agents/types.d.ts +5 -4
  2. package/dist/context/turn-errors.d.ts +27 -23
  3. package/dist/i18n/en.d.ts +14 -0
  4. package/dist/i18n/en.js +15 -1
  5. package/dist/i18n/zh-CN.d.ts +15 -1
  6. package/dist/i18n/zh-CN.js +15 -1
  7. package/dist/o11y/derive.js +28 -24
  8. package/dist/o11y/types.d.ts +8 -5
  9. package/dist/report/components/attempt-detail/UsageTable.js +4 -7
  10. package/dist/report/components/attempt-detail/compute.d.ts +2 -2
  11. package/dist/report/components/attempt-detail/compute.js +2 -6
  12. package/dist/report/components/attempt-detail/faces.js +7 -7
  13. package/dist/report/components/attempt-detail/index.js +0 -3
  14. package/dist/report/components/entity-lists/EvalList.js +0 -0
  15. package/dist/report/components/metric-views/compute.js +1 -1
  16. package/dist/report/model/types.d.ts +3 -4
  17. package/dist/results/locator.js +0 -0
  18. package/dist/results/select.d.ts +6 -0
  19. package/dist/results/select.js +8 -0
  20. package/dist/runner/feedback/sink.d.ts +26 -1
  21. package/dist/runner/fingerprint.d.ts +23 -0
  22. package/dist/runner/types.d.ts +105 -7
  23. package/dist/sandbox/errors.d.ts +29 -0
  24. package/dist/sandbox/resolve.d.ts +9 -0
  25. package/dist/shared/failure-class.d.ts +91 -0
  26. package/dist/types.d.ts +1 -0
  27. package/dist/util.d.ts +3 -2
  28. package/dist/util.js +31 -5
  29. package/docs-site/zh/explanation/runner.mdx +35 -0
  30. package/docs-site/zh/reference/cli.mdx +3 -3
  31. package/docs-site/zh/reference/events.mdx +4 -4
  32. package/docs-site/zh/troubleshooting/debugging.mdx +2 -2
  33. package/docs-site/zh/tutorials/viewing-results.mdx +4 -5
  34. package/package.json +4 -12
  35. package/src/agents/ai-sdk.test.ts +26 -0
  36. package/src/agents/ai-sdk.ts +7 -4
  37. package/src/agents/index.ts +5 -3
  38. package/src/agents/langgraph.test.ts +30 -0
  39. package/src/agents/langgraph.ts +5 -2
  40. package/src/agents/openai-compat.test.ts +35 -0
  41. package/src/agents/openai-compat.ts +16 -4
  42. package/src/agents/sdk-streams.test.ts +66 -0
  43. package/src/agents/sdk-streams.ts +3 -1
  44. package/src/agents/types.ts +5 -4
  45. package/src/cli.ts +55 -14
  46. package/src/context/context.test.ts +34 -0
  47. package/src/context/context.ts +14 -5
  48. package/src/context/send-retry.test.ts +86 -0
  49. package/src/context/send-retry.ts +37 -12
  50. package/src/context/session.ts +24 -0
  51. package/src/context/turn-errors.test.ts +124 -17
  52. package/src/context/turn-errors.ts +60 -50
  53. package/src/define.ts +5 -0
  54. package/src/i18n/en.ts +20 -1
  55. package/src/i18n/zh-CN.ts +20 -1
  56. package/src/index.ts +8 -0
  57. package/src/o11y/cost.ts +4 -2
  58. package/src/o11y/derive.test.ts +40 -0
  59. package/src/o11y/derive.ts +28 -22
  60. package/src/o11y/otlp/sandbox-receiver.test.ts +201 -0
  61. package/src/o11y/otlp/sandbox-receiver.ts +73 -27
  62. package/src/o11y/parsers/bub.test.ts +30 -0
  63. package/src/o11y/parsers/bub.ts +5 -2
  64. package/src/o11y/parsers/codex.test.ts +19 -0
  65. package/src/o11y/parsers/codex.ts +5 -2
  66. package/src/o11y/types.ts +8 -5
  67. package/src/report/components/attempt-detail/UsageTable.tsx +4 -6
  68. package/src/report/components/attempt-detail/attempt-components.test.tsx +5 -8
  69. package/src/report/components/attempt-detail/compute.ts +2 -7
  70. package/src/report/components/attempt-detail/faces.ts +7 -7
  71. package/src/report/components/attempt-detail/index.tsx +0 -3
  72. package/src/report/components/entity-lists/EvalList.tsx +0 -0
  73. package/src/report/components/metric-views/compute.ts +1 -1
  74. package/src/report/model/types.ts +3 -4
  75. package/src/results/format.ts +9 -2
  76. package/src/results/index.ts +2 -0
  77. package/src/results/locator.ts +0 -0
  78. package/src/results/open.ts +132 -21
  79. package/src/results/select.ts +12 -0
  80. package/src/results/skipped-notice.ts +0 -0
  81. package/src/runner/attempt.test.ts +116 -0
  82. package/src/runner/attempt.ts +89 -8
  83. package/src/runner/discover.ts +21 -3
  84. package/src/runner/feedback/coordinator.ts +24 -2
  85. package/src/runner/feedback/eval-conclusions.ts +6 -3
  86. package/src/runner/feedback/human.test.ts +300 -6
  87. package/src/runner/feedback/human.ts +132 -30
  88. package/src/runner/feedback/json.test.ts +127 -2
  89. package/src/runner/feedback/json.ts +51 -3
  90. package/src/runner/feedback/reducer.test.ts +328 -29
  91. package/src/runner/feedback/reducer.ts +84 -9
  92. package/src/runner/feedback/sink.ts +39 -1
  93. package/src/runner/fingerprint.ts +49 -19
  94. package/src/runner/gate-lease.test.ts +510 -0
  95. package/src/runner/gate-lease.ts +350 -0
  96. package/src/runner/lock.test.ts +454 -0
  97. package/src/runner/lock.ts +288 -0
  98. package/src/runner/report.test.ts +1 -0
  99. package/src/runner/run.test.ts +2044 -9
  100. package/src/runner/run.ts +825 -61
  101. package/src/runner/teardown-registry.ts +20 -78
  102. package/src/runner/types.ts +103 -7
  103. package/src/sandbox/errors.test.ts +72 -0
  104. package/src/sandbox/errors.ts +83 -0
  105. package/src/sandbox/keep-registry.ts +22 -46
  106. package/src/sandbox/resolve.test.ts +100 -0
  107. package/src/sandbox/resolve.ts +84 -27
  108. package/src/shared/entry-file-store.test.ts +149 -0
  109. package/src/shared/entry-file-store.ts +117 -0
  110. package/src/shared/failure-class.test.ts +137 -0
  111. package/src/shared/failure-class.ts +175 -0
  112. package/src/show/index.ts +5 -6
  113. package/src/show/render.test.ts +116 -15
  114. package/src/show/render.ts +124 -35
  115. package/src/types.ts +9 -0
  116. package/src/util.ts +31 -4
  117. package/src/view/app/App.tsx +5 -1
  118. package/src/view/client-dist/app.js +1 -1
  119. package/src/view/data.ts +5 -9
  120. package/src/view/shared/types.ts +7 -1
  121. package/src/view/view-report.test.ts +54 -0
@@ -369,10 +369,11 @@ export interface Agent {
369
369
  spanMapper?: SpanMapper;
370
370
  send(input: TurnInput, ctx: AgentContext): Promise<Turn>;
371
371
  /**
372
- * 可选 turn 失败分类器:按重试安全性归类一次 send 失败(抛出或返回 `status: "failed"` 的
373
- * Turn),返回 `undefined` 回落保守兜底。分类器只声明决策与诊断词,不影响重试策略(次数、
374
- * 退避对所有 agent 一致);抛错按不可重试处理并被吞掉。形状与分类链、执行体时序见
375
- * docs/feature/error-classification/architecture.md。
372
+ * 可选 turn 失败分类器:归类一次 send 失败(抛出或返回 `status: "failed"` 的 Turn),
373
+ * 返回 `undefined` 表示不认识、回落保守兜底。链上排在实验的 `classifyFailure` 之后,
374
+ * 实验作者认领过的失败问不到这里。分类器只声明决策轴与诊断词,不影响重试策略(次数、
375
+ * 退避对所有 agent 一致);抛错按 `undefined` 回落并被吞掉,不掩盖原始失败。形状与分类链、
376
+ * 执行体时序见 docs/feature/error-classification/architecture.md。
376
377
  */
377
378
  classifyTurnError?: TurnErrorClassifier;
378
379
  teardown?: AgentTeardown;
@@ -1,17 +1,5 @@
1
1
  import type { Turn } from "../types.ts";
2
- /**
3
- * 一次 send 失败的分类结果:`retryable` 是执行体唯一消费的决策轴;`reason` 是开放词表的
4
- * 细分诊断,只进 activity 与耗尽摘要,不参与策略。内建兜底产出 reason `"rate_limit"` /
5
- * `"network"`;adapter 分类器可自造词。`retryable: true` 时 `reason` 必填——可重试的失败
6
- * 一定会出现在 activity 行与可能的耗尽摘要里,那里需要一个给人读的词。
7
- */
8
- export type TurnErrorClass = {
9
- readonly retryable: true;
10
- readonly reason: string;
11
- } | {
12
- readonly retryable: false;
13
- readonly reason?: string;
14
- };
2
+ import { type AttemptFailureClassifier, type FailureClass } from "../shared/failure-class.ts";
15
3
  /** 一次 send 失败的两种浮出形态:`send()` 抛出异常,或返回 `status: "failed"` 的 Turn。 */
16
4
  export type TurnFailure = {
17
5
  readonly type: "thrown";
@@ -21,10 +9,10 @@ export type TurnFailure = {
21
9
  readonly turn: Turn;
22
10
  };
23
11
  /**
24
- * adapter 可选分类器:返回 `undefined` 表示「不认识,交给保守兜底」。分类器必须快、纯、
25
- * 不抛错——执行体按「抛错等价于不可重试」处理,自身错误被吞掉,不会掩盖原始失败。
12
+ * adapter 可选分类器:返回 `undefined` 表示「不认识,交给后续链路」。分类器必须快、纯、
13
+ * 不抛错——抛错按 `undefined` 回落处理,自身错误被吞掉,不会掩盖原始失败。
26
14
  */
27
- export type TurnErrorClassifier = (failure: TurnFailure) => TurnErrorClass | undefined;
15
+ export type TurnErrorClassifier = (failure: TurnFailure) => FailureClass | undefined;
28
16
  /**
29
17
  * 失败 Turn 的错误摘要:取 `events` 里最后一个 `type: "error"` 事件的 message。
30
18
  * 与 `context.turnFailed` 报错文案、保守兜底分类器读的同一段文本同源——不出现
@@ -35,15 +23,31 @@ export declare function turnErrorText(turn: Turn): string | undefined;
35
23
  /** 两种 `TurnFailure` 形态统一取「给人读也给分类器看」的那段文本。 */
36
24
  export declare function turnFailureText(failure: TurnFailure): string;
37
25
  /**
38
- * 保守兜底分类器:三道分类链里的第二道。对失败文本做正则匹配,认不出的一律 `{ retryable: false }`
39
- * ——宁可判死一个 attempt,不产出不可信的 verdict(判据见 README「分类」)
26
+ * 保守兜底分类器:turn 链里的第四道。对失败文本做正则匹配,认不出的一律 `{ retryable: false }`
27
+ * ——宁可判死一个 attempt,不产出不可信的 verdict(判据见 README「分类」)。兜底永不给出超出
28
+ * `"attempt"` 的 scope:框架无法从文案证明兄弟必死,扩 scope 只属于携带作者知识的通道。
40
29
  */
41
- export declare function classifyTurnError(failure: TurnFailure): TurnErrorClass;
30
+ export declare function classifyTurnError(failure: TurnFailure): FailureClass;
42
31
  /** 受理证据门:失败 Turn 的 events 里已出现任何 agent 侧产出,即证明 agent 已受理并开始工作。 */
43
32
  export declare function hasAgentEvidence(turn: Turn): boolean;
33
+ /** turn 链上两个可选声明通道;都省略时链退化成「抛出点 → 兜底 → 证据门」。 */
34
+ export interface TurnClassifiers {
35
+ /** 实验作者的 `ExperimentDef.classifyFailure`,按自家坐标识别共享基建死因。 */
36
+ experiment?: AttemptFailureClassifier;
37
+ /** adapter 作者的 `Agent.classifyTurnError`,识别自家协议的错误形状。 */
38
+ adapter?: TurnErrorClassifier;
39
+ }
44
40
  /**
45
- * 三道分类链的完整决议:adapter 分类器(可选,抛错按不可重试处理并吞掉)→ 保守兜底
46
- * 受理证据门(否决权,失败 Turn 带 agent 产出事件时强制降级)。执行体只需要调这一个函数,
47
- * 不必自己拼三道链的顺序。
41
+ * turn 失败分类链的完整决议(五道,先给出非 `undefined` 结果的一道定分类):
42
+ *
43
+ * 1. 抛出点携带的分类(`failureClassOf`,含 cause 链穿透)——作者知识优先级最高;
44
+ * 2. 实验分类器——按自家坐标(host、路径)过滤,特异性高于协议通用形状,排在 adapter 之前
45
+ * 保证「两者同时认领时 scope 赢」(裁决见 memory/failure-chain-experiment-before-adapter.md);
46
+ * 3. adapter 分类器;
47
+ * 4. 保守兜底正则;
48
+ * 5. 受理证据门(执行体的否决权,只裁时间轴):失败 Turn 里已有 agent 产出事件时 `retryable`
49
+ * 强制降为 `false`,`reason` 与 `scope` 原样保留——门裁的是重发安全性,不是波及范围。
50
+ *
51
+ * 分类器抛错按 `undefined` 回落(继续问后续通道),分类是旁路,不得用新错误掩盖原始失败。
48
52
  */
49
- export declare function resolveTurnErrorClass(failure: TurnFailure, adapterClassifier?: TurnErrorClassifier): TurnErrorClass;
53
+ export declare function resolveTurnFailureClass(failure: TurnFailure, classifiers?: TurnClassifiers): FailureClass;
package/dist/i18n/en.d.ts CHANGED
@@ -55,6 +55,11 @@ export declare const en: {
55
55
  "runner.setupReturnedCleanup": string;
56
56
  "runner.experimentTeardownLate": string;
57
57
  "runner.teardownRegistrationWriteFailed": string;
58
+ "runner.lockTakenOver": string;
59
+ "runner.gateLeaseTakenOver": string;
60
+ "runner.gateLeaseWaiting": string;
61
+ "runner.dispatchHaltedExperiment": string;
62
+ "runner.dispatchHaltedEval": string;
58
63
  "judge.modelMissing": string;
59
64
  "loaders.yamlMissing": string;
60
65
  "cli.flag.parseError": string;
@@ -130,6 +135,7 @@ export declare const en: {
130
135
  "define.experimentFlagNotJson": string;
131
136
  "define.experimentLabelInvalid": string;
132
137
  "define.experimentSetupNotFunction": string;
138
+ "define.experimentClassifyFailureNotFunction": string;
133
139
  "define.experimentIdRejected": string;
134
140
  "define.sandboxAgentNameRequired": string;
135
141
  "define.sandboxCreateRequired": string;
@@ -176,6 +182,14 @@ export declare const en: {
176
182
  "feedback.human.hookFailed": string;
177
183
  "feedback.human.precheckJudge": string;
178
184
  "feedback.human.precheckJudgeDone": string;
185
+ "feedback.human.countsWithElsewhere": string;
186
+ "feedback.human.waitingOnAnotherRun": string;
187
+ "feedback.human.lockWaitDetail": string;
188
+ "feedback.human.lockWaitStarted": string;
189
+ "feedback.human.lockWaitResolved": string;
190
+ "feedback.human.lockWaitCarried": string;
191
+ "feedback.human.lockWaitDispatched": string;
192
+ "feedback.human.lockedRowSuffix": string;
179
193
  "feedback.phase.sandboxSetup": string;
180
194
  "feedback.phase.scoring": string;
181
195
  "feedback.phase.teardown": string;
package/dist/i18n/en.js CHANGED
@@ -60,6 +60,11 @@ export const en = {
60
60
  "runner.setupReturnedCleanup": "{{layer}} returned a function. setup does not carry cleanup and the returned value will not be executed — put the cleanup in the paired teardown of the same layer ({{hint}}); see the experiments tutorial on docs-site or docs/runner.md.\n",
61
61
  "runner.experimentTeardownLate": "experiment {{experimentId}}'s teardown was not triggered by the normal countdown path; it has been executed by the end-of-run sweep instead. Results are unaffected; seeing this line means an unlocated intermittent scheduling issue fired — please record this run in the memory ledger.\n",
62
62
  "runner.teardownRegistrationWriteFailed": "writing the crash-recovery teardown registration for experiment {{experimentId}} failed: {{message}}. The run continues normally, but a SIGKILL during this run cannot be recovered via `niceeval exp --teardown` or the startup self-heal — check disk space/permissions under .niceeval/teardowns/.\n",
63
+ "runner.lockTakenOver": "took over an expired case lock for {{experimentId}}/{{evalId}} (previously held by pid {{pid}} on {{host}}; its heartbeat went stale) — that run likely died without releasing it; this run now owns dispatching this case.\n",
64
+ "runner.gateLeaseTakenOver": "took over an expired concurrency-slot lease for experiment {{experimentId}} (slot {{slot}}, previously held by pid {{pid}} on {{host}}; its heartbeat went stale) — that run likely died without releasing it; this run now owns the slot.\n",
65
+ "runner.gateLeaseWaiting": "waiting on another run for experiment {{experimentId}}'s concurrency slots: all {{effectiveN}} in use ({{holders}}). Concurrent runs share this experiment's slots, and the smallest maxConcurrency in play wins — this run declared {{declaredN}}. Nothing dispatches until a slot frees up; the other run's slots release when its attempts finish, or 30s after it dies.\n",
66
+ "runner.dispatchHaltedExperiment": "experiment halted (dispatch-halted): {{message}}\n",
67
+ "runner.dispatchHaltedEval": "eval halted: {{message}}\n",
63
68
  "judge.modelMissing": "No judge model configured. Set it in defineConfig({ judge: { model: \"...\" } }), the eval's judge config, or the NICEEVAL_JUDGE_MODEL environment variable (there is no built-in default model).\n" +
64
69
  " Docs: node_modules/niceeval/docs-site/zh/tutorials/scoring-guide.mdx",
65
70
  "loaders.yamlMissing": "loadYaml(\"{{path}}\") needs a YAML parser: run `pnpm add yaml` first (or switch to loadJson with a JSON dataset).",
@@ -198,6 +203,7 @@ export const en = {
198
203
  "define.experimentFlagNotJson": "experiment.flags.{{key}} is not JSON-serializable (functions / undefined / cycles / bigint are not allowed); flags are persisted verbatim into result snapshots and must be plain JSON.",
199
204
  "define.experimentLabelInvalid": "experiment.labels.{{key}} must be a string or a finite number; labels are report-side grouping coordinates persisted verbatim into result snapshots.",
200
205
  "define.experimentSetupNotFunction": "experiment.setup must be a function ((ctx) => void); use experiment.teardown for cleanup; to prepare the in-sandbox environment per experiment, chain .setup() hooks on the sandbox spec instead.",
206
+ "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure must be a function ((failure) => FailureClass | undefined); it classifies failures that surface as third-party errors and must return undefined for anything it does not recognize.",
201
207
  "define.experimentIdRejected": "defineExperiment does not accept id; ids are derived from file paths.",
202
208
  "define.sandboxAgentNameRequired": "defineSandboxAgent requires name.",
203
209
  "define.sandboxCreateRequired": "defineSandbox requires a create() function.",
@@ -211,7 +217,7 @@ export const en = {
211
217
  "feedback.human.active": "ACTIVE",
212
218
  "feedback.human.budgetExhausted": "budget exhausted for {{experimentId}} (spent {{spent}}, unstarted {{unstarted}})",
213
219
  "feedback.human.compare": "Compare: niceeval view",
214
- "feedback.human.counts": "{{total}} total · {{reused}} reused · {{running}} running · {{queued}} queued · {{completed}} completed",
220
+ "feedback.human.counts": "{{total}} total · {{reused}} reused · {{running}} running · {{queued}} queued · {{passed}} passed · {{failed}} failed · {{errored}} errored · {{skipped}} skipped",
215
221
  "feedback.human.diffHint": "Diff: niceeval show {{locator}} --diff",
216
222
  "feedback.human.evalHint": "Eval: niceeval show {{locator}} --source",
217
223
  "feedback.human.failuresHeader": "FAILURES",
@@ -244,6 +250,14 @@ export const en = {
244
250
  "feedback.human.hookFailed": "failed",
245
251
  "feedback.human.precheckJudge": "prechecking judge config",
246
252
  "feedback.human.precheckJudgeDone": "judge config ok",
253
+ "feedback.human.countsWithElsewhere": "{{total}} total · {{reused}} reused · {{running}} running · {{elsewhere}} elsewhere · {{queued}} queued · {{passed}} passed · {{failed}} failed · {{errored}} errored · {{skipped}} skipped",
254
+ "feedback.human.waitingOnAnotherRun": "waiting on another run",
255
+ "feedback.human.lockWaitDetail": "{{count}} evals · pid {{pid}}",
256
+ "feedback.human.lockWaitStarted": "waiting on another run · {{experimentId}} ({{count}} evals, pid {{pid}})",
257
+ "feedback.human.lockWaitResolved": "lock wait resolved · {{experimentId}} ({{summary}}, {{elapsed}})",
258
+ "feedback.human.lockWaitCarried": "{{count}} carried",
259
+ "feedback.human.lockWaitDispatched": "{{count}} to run",
260
+ "feedback.human.lockedRowSuffix": "locked",
247
261
  "feedback.phase.sandboxSetup": "sandbox setup",
248
262
  "feedback.phase.scoring": "scoring",
249
263
  "feedback.phase.teardown": "cleaning up",
@@ -55,6 +55,11 @@ export declare const zhCN: {
55
55
  readonly "runner.setupReturnedCleanup": "{{layer}} 返回了一个函数。setup 不承载收尾,返回值不会被执行——收尾写在同层成对的 teardown 里({{hint}});见 docs-site 的实验教程或 docs/runner.md「环境预置」。\n";
56
56
  readonly "runner.experimentTeardownLate": "实验 {{experimentId}} 的 teardown 未被正常计数路径触发,已在运行收尾兜底执行。结果不受影响;这行出现说明命中了一个未定位的调度间歇问题,请把本次运行信息记入 memory 台账。\n";
57
57
  readonly "runner.teardownRegistrationWriteFailed": "实验 {{experimentId}} 的强杀恢复收尾登记写入失败:{{message}}。本次运行照常继续,但这次运行期间若被 SIGKILL,`niceeval exp --teardown` 与启动自愈都无法找到它——检查 .niceeval/teardowns/ 下的磁盘空间或权限。\n";
58
+ readonly "runner.lockTakenOver": "接管了 {{experimentId}}/{{evalId}} 的过期用例锁(原持有者 pid {{pid}}@{{host}},心跳已过期)——那次运行大概率没能正常释放它;本次运行现在接手派发这条用例。\n";
59
+ readonly "runner.gateLeaseTakenOver": "接管了实验 {{experimentId}} 的过期并发名额租约(槽位 {{slot}},原持有者 pid {{pid}}@{{host}},心跳已过期)——那次运行大概率没能正常释放它;本次运行现在接手这个名额。\n";
60
+ readonly "runner.gateLeaseWaiting": "在等别的运行让出实验 {{experimentId}} 的并发名额:生效的 {{effectiveN}} 个位子全被占着({{holders}})。并行运行共用同一实验的名额,生效值取在场声明里最小的那个——本次运行声明的是 {{declaredN}}。名额腾出来之前不会派发任何 attempt;对方的名额会在它的 attempt 跑完时释放,它若已死则 30s 后过期被接管。\n";
61
+ readonly "runner.dispatchHaltedExperiment": "实验已止损(dispatch-halted):{{message}}\n";
62
+ readonly "runner.dispatchHaltedEval": "eval 已止损:{{message}}\n";
58
63
  readonly "judge.modelMissing": string;
59
64
  readonly "loaders.yamlMissing": "loadYaml(\"{{path}}\") 需要 YAML 解析器:请先 `pnpm add yaml`(或改用 loadJson + JSON 数据集)。";
60
65
  readonly "cli.flag.parseError": "{{message}}\n运行 `niceeval --help` 查看用法。\n";
@@ -130,6 +135,7 @@ export declare const zhCN: {
130
135
  readonly "define.experimentFlagNotJson": "experiment.flags.{{key}} 不是可 JSON 序列化的值(函数 / undefined / 循环引用 / bigint 不允许);flags 会原样进入结果快照,必须是纯 JSON。";
131
136
  readonly "define.experimentLabelInvalid": "experiment.labels.{{key}} 必须是字符串或有限数字;labels 是报告侧的归类坐标,会原样进入结果快照。";
132
137
  readonly "define.experimentSetupNotFunction": "experiment.setup 必须是函数((ctx) => void);要清理请挂 experiment.teardown;要按实验准备沙箱内环境请挂 sandbox spec 的 .setup() 钩子链。";
138
+ readonly "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure 必须是函数((failure) => FailureClass | undefined):它识别以第三方错误形态浮出的失败,认不出的一律返回 undefined 交给后续链路。";
133
139
  readonly "define.experimentIdRejected": "defineExperiment 不接受 id —— id 由文件路径推导。";
134
140
  readonly "define.sandboxAgentNameRequired": "defineSandboxAgent 需要 name。";
135
141
  readonly "define.sandboxCreateRequired": "defineSandbox 需要一个 create() 函数。";
@@ -143,7 +149,7 @@ export declare const zhCN: {
143
149
  readonly "feedback.human.active": "ACTIVE";
144
150
  readonly "feedback.human.budgetExhausted": "{{experimentId}} 预算已耗尽(已花 {{spent}},未跑 {{unstarted}})";
145
151
  readonly "feedback.human.compare": "Compare: niceeval view";
146
- readonly "feedback.human.counts": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 排队 {{queued}} · 已完成 {{completed}}";
152
+ readonly "feedback.human.counts": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 排队 {{queued}} · 通过 {{passed}} · 失败 {{failed}} · 出错 {{errored}} · 跳过 {{skipped}}";
147
153
  readonly "feedback.human.diffHint": "Diff: niceeval show {{locator}} --diff";
148
154
  readonly "feedback.human.evalHint": "Eval: niceeval show {{locator}} --source";
149
155
  readonly "feedback.human.failuresHeader": "FAILURES";
@@ -176,6 +182,14 @@ export declare const zhCN: {
176
182
  readonly "feedback.human.hookFailed": "失败";
177
183
  readonly "feedback.human.precheckJudge": "预检 judge 配置";
178
184
  readonly "feedback.human.precheckJudgeDone": "judge 配置就绪";
185
+ readonly "feedback.human.countsWithElsewhere": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 等待中 {{elsewhere}} · 排队 {{queued}} · 通过 {{passed}} · 失败 {{failed}} · 出错 {{errored}} · 跳过 {{skipped}}";
186
+ readonly "feedback.human.waitingOnAnotherRun": "等待另一个并行 run";
187
+ readonly "feedback.human.lockWaitDetail": "{{count}} 条用例 · pid {{pid}}";
188
+ readonly "feedback.human.lockWaitStarted": "等待另一个并行 run · {{experimentId}}({{count}} 条用例,pid {{pid}})";
189
+ readonly "feedback.human.lockWaitResolved": "等待结束 · {{experimentId}}({{summary}},{{elapsed}})";
190
+ readonly "feedback.human.lockWaitCarried": "{{count}} 条携入";
191
+ readonly "feedback.human.lockWaitDispatched": "{{count}} 条自跑";
192
+ readonly "feedback.human.lockedRowSuffix": "locked";
179
193
  readonly "feedback.phase.sandboxSetup": "沙箱预置";
180
194
  readonly "feedback.phase.scoring": "评分";
181
195
  readonly "feedback.phase.teardown": "清理中";
@@ -60,6 +60,11 @@ export const zhCN = {
60
60
  "runner.setupReturnedCleanup": "{{layer}} 返回了一个函数。setup 不承载收尾,返回值不会被执行——收尾写在同层成对的 teardown 里({{hint}});见 docs-site 的实验教程或 docs/runner.md「环境预置」。\n",
61
61
  "runner.experimentTeardownLate": "实验 {{experimentId}} 的 teardown 未被正常计数路径触发,已在运行收尾兜底执行。结果不受影响;这行出现说明命中了一个未定位的调度间歇问题,请把本次运行信息记入 memory 台账。\n",
62
62
  "runner.teardownRegistrationWriteFailed": "实验 {{experimentId}} 的强杀恢复收尾登记写入失败:{{message}}。本次运行照常继续,但这次运行期间若被 SIGKILL,`niceeval exp --teardown` 与启动自愈都无法找到它——检查 .niceeval/teardowns/ 下的磁盘空间或权限。\n",
63
+ "runner.lockTakenOver": "接管了 {{experimentId}}/{{evalId}} 的过期用例锁(原持有者 pid {{pid}}@{{host}},心跳已过期)——那次运行大概率没能正常释放它;本次运行现在接手派发这条用例。\n",
64
+ "runner.gateLeaseTakenOver": "接管了实验 {{experimentId}} 的过期并发名额租约(槽位 {{slot}},原持有者 pid {{pid}}@{{host}},心跳已过期)——那次运行大概率没能正常释放它;本次运行现在接手这个名额。\n",
65
+ "runner.gateLeaseWaiting": "在等别的运行让出实验 {{experimentId}} 的并发名额:生效的 {{effectiveN}} 个位子全被占着({{holders}})。并行运行共用同一实验的名额,生效值取在场声明里最小的那个——本次运行声明的是 {{declaredN}}。名额腾出来之前不会派发任何 attempt;对方的名额会在它的 attempt 跑完时释放,它若已死则 30s 后过期被接管。\n",
66
+ "runner.dispatchHaltedExperiment": "实验已止损(dispatch-halted):{{message}}\n",
67
+ "runner.dispatchHaltedEval": "eval 已止损:{{message}}\n",
63
68
  "judge.modelMissing": "judge 未配置模型:在 defineConfig({ judge: { model: \"...\" } })、eval 的 judge 配置或环境变量 NICEEVAL_JUDGE_MODEL 里指定裁判模型(没有内置默认模型)。\n" +
64
69
  " 文档:node_modules/niceeval/docs-site/zh/tutorials/scoring-guide.mdx",
65
70
  "loaders.yamlMissing": "loadYaml(\"{{path}}\") 需要 YAML 解析器:请先 `pnpm add yaml`(或改用 loadJson + JSON 数据集)。",
@@ -194,6 +199,7 @@ export const zhCN = {
194
199
  "define.experimentFlagNotJson": "experiment.flags.{{key}} 不是可 JSON 序列化的值(函数 / undefined / 循环引用 / bigint 不允许);flags 会原样进入结果快照,必须是纯 JSON。",
195
200
  "define.experimentLabelInvalid": "experiment.labels.{{key}} 必须是字符串或有限数字;labels 是报告侧的归类坐标,会原样进入结果快照。",
196
201
  "define.experimentSetupNotFunction": "experiment.setup 必须是函数((ctx) => void);要清理请挂 experiment.teardown;要按实验准备沙箱内环境请挂 sandbox spec 的 .setup() 钩子链。",
202
+ "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure 必须是函数((failure) => FailureClass | undefined):它识别以第三方错误形态浮出的失败,认不出的一律返回 undefined 交给后续链路。",
197
203
  "define.experimentIdRejected": "defineExperiment 不接受 id —— id 由文件路径推导。",
198
204
  "define.sandboxAgentNameRequired": "defineSandboxAgent 需要 name。",
199
205
  "define.sandboxCreateRequired": "defineSandbox 需要一个 create() 函数。",
@@ -207,7 +213,7 @@ export const zhCN = {
207
213
  "feedback.human.active": "ACTIVE",
208
214
  "feedback.human.budgetExhausted": "{{experimentId}} 预算已耗尽(已花 {{spent}},未跑 {{unstarted}})",
209
215
  "feedback.human.compare": "Compare: niceeval view",
210
- "feedback.human.counts": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 排队 {{queued}} · 已完成 {{completed}}",
216
+ "feedback.human.counts": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 排队 {{queued}} · 通过 {{passed}} · 失败 {{failed}} · 出错 {{errored}} · 跳过 {{skipped}}",
211
217
  "feedback.human.diffHint": "Diff: niceeval show {{locator}} --diff",
212
218
  "feedback.human.evalHint": "Eval: niceeval show {{locator}} --source",
213
219
  "feedback.human.failuresHeader": "FAILURES",
@@ -240,6 +246,14 @@ export const zhCN = {
240
246
  "feedback.human.hookFailed": "失败",
241
247
  "feedback.human.precheckJudge": "预检 judge 配置",
242
248
  "feedback.human.precheckJudgeDone": "judge 配置就绪",
249
+ "feedback.human.countsWithElsewhere": "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 等待中 {{elsewhere}} · 排队 {{queued}} · 通过 {{passed}} · 失败 {{failed}} · 出错 {{errored}} · 跳过 {{skipped}}",
250
+ "feedback.human.waitingOnAnotherRun": "等待另一个并行 run",
251
+ "feedback.human.lockWaitDetail": "{{count}} 条用例 · pid {{pid}}",
252
+ "feedback.human.lockWaitStarted": "等待另一个并行 run · {{experimentId}}({{count}} 条用例,pid {{pid}})",
253
+ "feedback.human.lockWaitResolved": "等待结束 · {{experimentId}}({{summary}},{{elapsed}})",
254
+ "feedback.human.lockWaitCarried": "{{count}} 条携入",
255
+ "feedback.human.lockWaitDispatched": "{{count}} 条自跑",
256
+ "feedback.human.lockedRowSuffix": "locked",
243
257
  "feedback.phase.sandboxSetup": "沙箱预置",
244
258
  "feedback.phase.scoring": "评分",
245
259
  "feedback.phase.teardown": "清理中",
@@ -46,10 +46,16 @@ function pickExitCode(output) {
46
46
  }
47
47
  // ───────────────────────── deriveRunFacts ─────────────────────────
48
48
  export function deriveRunFacts(events) {
49
- const toolCallMap = new Map();
50
- const toolCallOrder = [];
51
- const subagentMap = new Map();
52
- const subagentOrder = [];
49
+ // 折叠是逐条按发生顺序进行的:called 追加一条新调用,result 回填「当前还没配上 result 的
50
+ // callId 调用」。callId 只在一个 called→result 配对内保证稳定,不保证跨轮唯一——adapter
51
+ // 常按轮各自编号(OpenAI 兼容协议、transcript 归一都会复用 c1/c2…)。所以一个 callId 在其
52
+ // result 之后再次以 called 出现,是新的一次调用,起一条新记录,不覆盖前一轮那条(否则跨轮
53
+ // 聚合会把前面几轮的工具调用抹成「只剩最后一轮」)。用 open*ByCallId 只跟踪各 callId 当前
54
+ // 敞口的那条,配上 result 即关闭。
55
+ const toolCalls = [];
56
+ const openToolByCallId = new Map();
57
+ const subagentCalls = [];
58
+ const openSubagentByCallId = new Map();
53
59
  const inputRequests = [];
54
60
  let messageCount = 0;
55
61
  let compactions = 0;
@@ -63,9 +69,8 @@ export function deriveRunFacts(events) {
63
69
  contextInjections += 1;
64
70
  break;
65
71
  case "action.called": {
66
- if (!toolCallMap.has(ev.callId))
67
- toolCallOrder.push(ev.callId);
68
- toolCallMap.set(ev.callId, {
72
+ openToolByCallId.set(ev.callId, toolCalls.length);
73
+ toolCalls.push({
69
74
  callId: ev.callId,
70
75
  name: ev.tool ?? "unknown",
71
76
  originalName: ev.name,
@@ -76,15 +81,15 @@ export function deriveRunFacts(events) {
76
81
  break;
77
82
  }
78
83
  case "action.result": {
79
- const existing = toolCallMap.get(ev.callId);
80
- if (existing) {
81
- existing.output = ev.output;
82
- existing.status = ev.status;
84
+ const idx = openToolByCallId.get(ev.callId);
85
+ if (idx !== undefined) {
86
+ toolCalls[idx].output = ev.output;
87
+ toolCalls[idx].status = ev.status;
88
+ openToolByCallId.delete(ev.callId);
83
89
  }
84
90
  else {
85
91
  // 只有结果、没配上调用:补一条占位 ToolCall。
86
- toolCallOrder.push(ev.callId);
87
- toolCallMap.set(ev.callId, {
92
+ toolCalls.push({
88
93
  callId: ev.callId,
89
94
  name: "unknown",
90
95
  input: null,
@@ -95,9 +100,8 @@ export function deriveRunFacts(events) {
95
100
  break;
96
101
  }
97
102
  case "subagent.called": {
98
- if (!subagentMap.has(ev.callId))
99
- subagentOrder.push(ev.callId);
100
- subagentMap.set(ev.callId, {
103
+ openSubagentByCallId.set(ev.callId, subagentCalls.length);
104
+ subagentCalls.push({
101
105
  callId: ev.callId,
102
106
  name: ev.name,
103
107
  remoteUrl: ev.remoteUrl,
@@ -107,14 +111,14 @@ export function deriveRunFacts(events) {
107
111
  break;
108
112
  }
109
113
  case "subagent.completed": {
110
- const existing = subagentMap.get(ev.callId);
111
- if (existing) {
112
- existing.output = ev.output;
113
- existing.status = ev.status;
114
+ const idx = openSubagentByCallId.get(ev.callId);
115
+ if (idx !== undefined) {
116
+ subagentCalls[idx].output = ev.output;
117
+ subagentCalls[idx].status = ev.status;
118
+ openSubagentByCallId.delete(ev.callId);
114
119
  }
115
120
  else {
116
- subagentOrder.push(ev.callId);
117
- subagentMap.set(ev.callId, {
121
+ subagentCalls.push({
118
122
  callId: ev.callId,
119
123
  name: "unknown",
120
124
  output: ev.output,
@@ -143,8 +147,8 @@ export function deriveRunFacts(events) {
143
147
  break;
144
148
  }
145
149
  return {
146
- toolCalls: toolCallOrder.map((id) => toolCallMap.get(id)),
147
- subagentCalls: subagentOrder.map((id) => subagentMap.get(id)),
150
+ toolCalls,
151
+ subagentCalls,
148
152
  inputRequests,
149
153
  parked,
150
154
  messageCount,
@@ -2,18 +2,21 @@ import type { JsonValue, SourceLoc } from "../shared/types.ts";
2
2
  /**
3
3
  * 一次运行的 token 用量(沙箱型从 transcript/OTel span 的 `gen_ai.usage.*` 属性抠,remote 由
4
4
  * send 的 `Turn.usage` 直接返回)。每个字段只在协议真实提供该值时存在——原始协议没有 usage 时
5
- * 省略,不编造数值;不存在「默认 0」或「默认 1」的字段(docs/feature/results/architecture.md#usage)。
5
+ * 省略,不编造数值;不存在「默认 0」或「默认 1」的字段。三个输入侧桶恒互斥:相加才是送进模型的
6
+ * 完整上下文量;把协议原生口径归一到互斥是 adapter 的落值义务——OpenAI 系报「含缓存的输入总量 +
7
+ * 缓存命中子集」,落 inputTokens 前先扣掉子集(docs/feature/results/architecture.md#usage,
8
+ * 各协议明细见 docs/feature/adapters/sdk/<name>/cost.md)。
6
9
  */
7
10
  export interface Usage {
8
- /** 计费口径的输入 token 总量(协议报什么记什么,含 cache read 时如实包含,不换算)。 */
11
+ /** 未命中缓存、按全价计费的输入 token;与两个 cache 桶互斥。 */
9
12
  inputTokens?: number;
10
13
  /** 输出(completion)token 数。 */
11
14
  outputTokens?: number;
12
- /** 从缓存命中的输入部分;与 inputTokens 同一计量口径(省略表示该 agent 不上报此项)。 */
15
+ /** 从提示缓存命中的输入 token;独立计价桶,不包含在 inputTokens (省略表示该 agent 不上报此项)。 */
13
16
  cacheReadTokens?: number;
14
- /** 写入 prompt 缓存创建的 token 数(省略表示该 agent 不上报此项)。 */
17
+ /** 写入提示缓存的输入 token;独立计价桶,不包含在 inputTokens (省略表示该 agent 不上报此项)。 */
15
18
  cacheCreationTokens?: number;
16
- /** 推理(thinking)token 数,只在协议真实提供时存在(省略表示该 agent 不上报此项)。 */
19
+ /** 推理(thinking)token 数,outputTokens 的已含明细,单列展示用;只在协议真实提供时存在。 */
17
20
  reasoningTokens?: number;
18
21
  /** 真实发生的模型请求数。协议不提供请求计数就省略,绝不写 1 凑数。 */
19
22
  requests?: number;
@@ -9,13 +9,10 @@ export function UsageTable({ data, className }) {
9
9
  rows.push(["turns", String(data.turns)]);
10
10
  if (data.toolCalls !== undefined)
11
11
  rows.push(["tool calls", String(data.toolCalls)]);
12
- // 未缓存输入是消费端派生量,只在两个输入都在场时显示;缺任一个回退显示原始
13
- // inputTokens(不猜 0),两种来源用不同行标签区分。
14
- if (data.uncachedInputTokens !== undefined) {
15
- rows.push(["uncached in", data.uncachedInputTokens.toLocaleString()]);
16
- }
17
- else if (usage?.inputTokens !== undefined) {
18
- rows.push(["in", usage.inputTokens.toLocaleString()]);
12
+ // 桶恒互斥,inputTokens 就是未缓存输入;"uncached in" 标签只在 cache 拆分在场时用,
13
+ // cache 桶缺席的数字不贴标注。
14
+ if (usage?.inputTokens !== undefined) {
15
+ rows.push([usage.cacheReadTokens !== undefined ? "uncached in" : "in", usage.inputTokens.toLocaleString()]);
19
16
  }
20
17
  if (usage?.cacheReadTokens !== undefined)
21
18
  rows.push(["cache read", usage.cacheReadTokens.toLocaleString()]);
@@ -30,8 +30,8 @@ export declare function attemptDiagnosticsData(evidence: AttemptEvidence): Attem
30
30
  * identity 字段(locator/experimentId/evalId/attempt/verdict)恒有;turns/toolCalls 是 events
31
31
  * 派生(与 o11y.json 行为摘要同源,buildO11ySummary 与 o11y.json 落盘走同一份纯函数),没有
32
32
  * events 就整对省略——不因为其中一个恰好是 0 就当作"缺失"处理,0 是观测到的事实。
33
- * uncachedInputTokens 只在 inputTokens 与 cacheReadTokens 都存在时派生,缺任一个不猜 0
34
- * (text 面回退显示原始 inputTokens)。turns/toolCalls/usage 三者全部缺失时返回 null——
33
+ * token 桶恒互斥,inputTokens 本身就是未缓存输入,不派生第二个字段。
34
+ * turns/toolCalls/usage 三者全部缺失时返回 null——
35
35
  * 没有任何用量事实可摆,与其余叶子同一条"没有 usage 时零输出"规则。
36
36
  */
37
37
  export declare function usageTableData(evidence: AttemptEvidence): UsageTableData | null;
@@ -441,8 +441,8 @@ export function attemptDiagnosticsData(evidence) {
441
441
  * identity 字段(locator/experimentId/evalId/attempt/verdict)恒有;turns/toolCalls 是 events
442
442
  * 派生(与 o11y.json 行为摘要同源,buildO11ySummary 与 o11y.json 落盘走同一份纯函数),没有
443
443
  * events 就整对省略——不因为其中一个恰好是 0 就当作"缺失"处理,0 是观测到的事实。
444
- * uncachedInputTokens 只在 inputTokens 与 cacheReadTokens 都存在时派生,缺任一个不猜 0
445
- * (text 面回退显示原始 inputTokens)。turns/toolCalls/usage 三者全部缺失时返回 null——
444
+ * token 桶恒互斥,inputTokens 本身就是未缓存输入,不派生第二个字段。
445
+ * turns/toolCalls/usage 三者全部缺失时返回 null——
446
446
  * 没有任何用量事实可摆,与其余叶子同一条"没有 usage 时零输出"规则。
447
447
  */
448
448
  export function usageTableData(evidence) {
@@ -453,9 +453,6 @@ export function usageTableData(evidence) {
453
453
  const usage = result.usage;
454
454
  if (turns === undefined && toolCalls === undefined && usage === undefined)
455
455
  return null;
456
- const uncachedInputTokens = usage && typeof usage.inputTokens === "number" && typeof usage.cacheReadTokens === "number"
457
- ? usage.inputTokens - usage.cacheReadTokens
458
- : undefined;
459
456
  const estimatedCostUSD = attemptCostUSD(result);
460
457
  return {
461
458
  locator: evidence.locator,
@@ -466,7 +463,6 @@ export function usageTableData(evidence) {
466
463
  ...(turns !== undefined ? { turns } : {}),
467
464
  ...(toolCalls !== undefined ? { toolCalls } : {}),
468
465
  ...(usage !== undefined ? { usage } : {}),
469
- ...(uncachedInputTokens !== undefined ? { uncachedInputTokens } : {}),
470
466
  ...(estimatedCostUSD !== null ? { estimatedCostUSD } : {}),
471
467
  };
472
468
  }
@@ -293,13 +293,13 @@ export function usageTableText(data, _ctx) {
293
293
  if (data === null)
294
294
  return "";
295
295
  const usage = data.usage;
296
- // 未缓存输入是消费端派生量,只在两个输入都在场时显示;缺任一个回退显示原始 inputTokens
297
- // (不猜 0),这里用 label 区分"派生值"与"原始值",不假装两者是同一件事。
298
- const inFragment = data.uncachedInputTokens !== undefined
299
- ? `${formatMetricValue(data.uncachedInputTokens)} uncached in`
300
- : usage?.inputTokens !== undefined
301
- ? `${formatMetricValue(usage.inputTokens)} in`
302
- : undefined;
296
+ // 桶恒互斥,inputTokens 就是未缓存输入;"uncached" 标注只在 cache 拆分真实在场时给,
297
+ // cache 桶缺席的数字不贴标注——不给没有拆分事实的数字暗示拆分。
298
+ const inFragment = usage?.inputTokens !== undefined
299
+ ? usage.cacheReadTokens !== undefined
300
+ ? `${formatMetricValue(usage.inputTokens)} uncached in`
301
+ : `${formatMetricValue(usage.inputTokens)} in`
302
+ : undefined;
303
303
  const cacheFragment = usage?.cacheReadTokens !== undefined ? `${formatMetricValue(usage.cacheReadTokens)} cache read` : undefined;
304
304
  const outFragment = usage?.outputTokens !== undefined ? `${formatMetricValue(usage.outputTokens)} out` : undefined;
305
305
  const inCache = [inFragment, cacheFragment].filter((s) => s !== undefined).join(" + ");
@@ -596,9 +596,6 @@ export function validateUsageData(data) {
596
596
  if (usageProb !== null)
597
597
  return usageProb;
598
598
  }
599
- if (data.uncachedInputTokens !== undefined && typeof data.uncachedInputTokens !== "number") {
600
- return '"uncachedInputTokens" must be a number';
601
- }
602
599
  if (data.estimatedCostUSD !== undefined && typeof data.estimatedCostUSD !== "number") {
603
600
  return '"estimatedCostUSD" must be a number';
604
601
  }
@@ -283,7 +283,7 @@ export async function metricLineData(input, options) {
283
283
  "Fix of() to read experiment-level configuration (numericFlag()/numericRunConfig() do this by construction).");
284
284
  }
285
285
  const series = options.series ? seriesKey(options.series, item) : undefined;
286
- const bucketKey = `${series ?? ""}${x === null ? "null" : String(x)}`;
286
+ const bucketKey = `${series ?? ""}\u0000${x === null ? "null" : String(x)}`;
287
287
  const bucket = buckets.get(bucketKey);
288
288
  if (bucket)
289
289
  bucket.items.push(item);
@@ -750,9 +750,9 @@ export interface AttemptDiagnosticsData {
750
750
  *
751
751
  * - `turns`/`toolCalls`:events 派生(与 `o11y.json` 行为摘要同源),只在有非空 events 时出现,
752
752
  * 哪怕派生值恰好是 0(有 events 但零轮/零工具调用是观测到的事实,不是缺失)。
753
- * - `usage`:落盘 `Usage` 原样,只在 `result.usage` 存在时出现。
754
- * - `uncachedInputTokens`:`usage.inputTokens usage.cacheReadTokens`,只在两个输入都存在时派生;
755
- * 缺任一个不回退猜 0,整字段省略(text 面回退显示原始 `inputTokens`)。
753
+ * - `usage`:落盘 `Usage` 原样,只在 `result.usage` 存在时出现。桶恒互斥,`inputTokens` 本身
754
+ * 就是未缓存输入(契约见 docs/feature/results/architecture.md#usage);"uncached in" 标注只在
755
+ * `cacheReadTokens` 在场时由 face 层给出,不派生第二个字段。
756
756
  * - `estimatedCostUSD`:能算出成本(`usage.costUSD` 或 `result.estimatedCostUSD`)时才出现。
757
757
  *
758
758
  * `turns`/`toolCalls`/`usage` 三者全部缺失时(没有 events 也没有落盘 usage)整个 data 为
@@ -767,7 +767,6 @@ export interface UsageTableData {
767
767
  turns?: number;
768
768
  toolCalls?: number;
769
769
  usage?: Usage;
770
- uncachedInputTokens?: number;
771
770
  estimatedCostUSD?: number;
772
771
  }
773
772
  /** `AttemptTrace` 的 data:不与 runner 节点合并的原始 OTel span 列表;没有 trace 时 null。 */
Binary file
@@ -87,6 +87,12 @@ export declare function dedupeAttempts(attempts: AttemptHandle[]): {
87
87
  };
88
88
  /** 快照新旧比较:startedAt 优先,同刻按快照目录名(时间戳 + 随机后缀,字典序即时序)。 */
89
89
  export declare function isNewerSnapshot(a: Snapshot, b: Snapshot): boolean;
90
+ /**
91
+ * isNewerSnapshot 的原始口径,供还没组装成 Snapshot 的读取面共用同一判定(收窄读
92
+ * `loadLatestResultsForCase` 只拿到目录名 + snapshot.json 的 startedAt)。同一份磁盘在两条
93
+ * 读取面上必须给出同一个「哪份最新」的答案,否则携带与报告会分叉。
94
+ */
95
+ export declare function isNewerSnapshotPlacement(a: Pick<Snapshot, "startedAt" | "dir">, b: Pick<Snapshot, "startedAt" | "dir">): boolean;
90
96
  /**
91
97
  * experiment 选择器过滤(--exp / latest({ experiments }) 同一语义,与 `niceeval exp` 位置参数
92
98
  * 共用 matchExperimentSelector,见 docs/feature/experiments/cli.md「实验选择器怎样解析」);
@@ -317,6 +317,14 @@ export function dedupeAttempts(attempts) {
317
317
  }
318
318
  /** 快照新旧比较:startedAt 优先,同刻按快照目录名(时间戳 + 随机后缀,字典序即时序)。 */
319
319
  export function isNewerSnapshot(a, b) {
320
+ return isNewerSnapshotPlacement(a, b);
321
+ }
322
+ /**
323
+ * isNewerSnapshot 的原始口径,供还没组装成 Snapshot 的读取面共用同一判定(收窄读
324
+ * `loadLatestResultsForCase` 只拿到目录名 + snapshot.json 的 startedAt)。同一份磁盘在两条
325
+ * 读取面上必须给出同一个「哪份最新」的答案,否则携带与报告会分叉。
326
+ */
327
+ export function isNewerSnapshotPlacement(a, b) {
320
328
  const byStart = a.startedAt.localeCompare(b.startedAt);
321
329
  if (byStart !== 0)
322
330
  return byStart > 0;
@@ -7,8 +7,13 @@ import type { AttemptLocator } from "../../results/locator.ts";
7
7
  export interface DiagnosticInput {
8
8
  /** 稳定去重 key —— 同一种 warning/error 用同一个 key(见 cli.md「同一 dedupeKey 并发出现时
9
9
  * 只留一条并显示次数」),不要把可变的实例细节(如具体 sandbox id)编进 key 本身,
10
- * 那些细节放 `data`。 */
10
+ * 那些细节放 `data`。折叠身份(实验 / eval)可以编进 key,那是「折叠到多细」的表达;
11
+ * 对外展示的稳定词由 `code` 单独给,不从 key 反推。 */
11
12
  key: string;
13
+ /** 对外的稳定词法:`--json` 的 `warning.code`、human 诊断行的标题都读它(见 cli.md
14
+ * `WarningEvent`,如 `lock-taken-over` / `dispatch-halted`)。省略 = 与 `key` 相同——
15
+ * 折叠身份不进 key 的那些诊断天生就是干净字面量,不必重复写一遍。 */
16
+ code?: string;
12
17
  severity: "warning" | "error";
13
18
  /** 一句话人类可读摘要;renderer 的 appendDurable 直接展示,不需要再解析。 */
14
19
  message: string;
@@ -52,6 +57,21 @@ export interface PrecheckInput {
52
57
  status: "started" | "done";
53
58
  durationMs?: number;
54
59
  }
60
+ /** `sink.lockWait()` 的输入 —— 与 `DurableFeedbackEvent` 的 "lock-wait" 变体字段一致,省略
61
+ * `type`/`at`(由 coordinator 补上)。调用方(run.ts)只在真正探测到撞上新鲜锁、需要等待时才
62
+ * 调一次 "started"(取锁立即成功或接管成功都不调用这个函数);等待解决(锁释放/接管后重查
63
+ * 携带完毕)时调一次 "resolved"。 */
64
+ export interface LockWaitInput {
65
+ experimentId: string;
66
+ evalId: string;
67
+ status: "started" | "resolved";
68
+ holderPid?: number;
69
+ holderHost?: string;
70
+ attempts?: number;
71
+ carried?: number;
72
+ dispatched?: number;
73
+ waitedMs?: number;
74
+ }
55
75
  /** `sink.kept()` 的输入 —— 与 `DurableFeedbackEvent` 的 "kept" 变体字段一致,省略 type/at。 */
56
76
  export interface KeptInput {
57
77
  locator: AttemptLocator;
@@ -84,6 +104,8 @@ export interface FeedbackSink {
84
104
  experimentHook(input: ExperimentHookInput): void;
85
105
  /** judge 预检的起止(见 `PrecheckInput`)。整次运行至多一次,发生在任何 attempt 派发之前。 */
86
106
  precheck(input: PrecheckInput): void;
107
+ /** 用例锁等待的起止(见 `LockWaitInput`)。 */
108
+ lockWait(input: LockWaitInput): void;
87
109
  /** 实验级 `ctx.progress` 的短命投影:只更新运行级行的 detail。与 `lifecycle` 同级别的
88
110
  * 「只服务正在画着的 dashboard」信号,没有活跃 coordinator 时静默丢弃是安全的。 */
89
111
  experimentProgress(input: ExperimentProgressInput): void;
@@ -123,6 +145,9 @@ export declare function reportExperimentProgress(input: ExperimentProgressInput)
123
145
  /** judge 预检的起止(见 `PrecheckInput`)。没有活跃 coordinator 时退回一行 stderr ——
124
146
  * 慢预检的可见性正是这条通道存在的理由,不能像 lifecycle 那样静默丢弃。 */
125
147
  export declare function reportPrecheck(input: PrecheckInput): void;
148
+ /** 用例锁等待的起止(见 `LockWaitInput`)。没有活跃 coordinator 时退回一行 stderr ——
149
+ * 长等待期间的可见性正是这条通道存在的理由,不能像 lifecycle 那样静默丢弃。 */
150
+ export declare function reportLockWait(input: LockWaitInput): void;
126
151
  export declare function reportBudgetExhausted(input: BudgetExhaustedInput): void;
127
152
  /** 用户中断(Ctrl+C)。没有活跃 coordinator 时的兜底文案与迁移前的 `runner.interrupted` 完全
128
153
  * 相同 —— 调用方(run.ts)不再需要自己持有这段 i18n 文案。 */