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
@@ -8,8 +8,10 @@
8
8
  // docs/engineering/testing/e2e/cli.md「反馈输出格式」在真实进程输出上验收。
9
9
 
10
10
  import { describe, expect, it } from "vitest";
11
- import { computeExitCode, renderJsonPlanDocument } from "./json.ts";
12
- import type { InvocationCompletion, InvocationSummary } from "../types.ts";
11
+ import { computeExitCode, createJsonRenderer, renderJsonPlanDocument } from "./json.ts";
12
+ import { createInitialRunFeedbackState, reduceRunFeedback } from "./reducer.ts";
13
+ import { createFakeFeedbackIO } from "./testing.ts";
14
+ import { HALT_DIAGNOSTIC_CODE, type DurableFeedbackEvent, type InvocationCompletion, type InvocationSummary } from "../types.ts";
13
15
 
14
16
  function summary(overrides: Partial<InvocationSummary> = {}): InvocationSummary {
15
17
  return {
@@ -77,6 +79,113 @@ describe("computeExitCode:CompletionStatus 驱动退出码,不只看 failed/erro
77
79
  });
78
80
  });
79
81
 
82
+ /** 依次喂进 reducer 再交给 json renderer(与生产的 coordinator 同序:先 reduce 后 render),
83
+ * 返回逐行解析出的事件对象。 */
84
+ function emitDurable(events: readonly DurableFeedbackEvent[]): Record<string, unknown>[] {
85
+ const { io, stdout } = createFakeFeedbackIO();
86
+ const renderer = createJsonRenderer({ io });
87
+ let state = createInitialRunFeedbackState();
88
+ for (const event of events) {
89
+ state = reduceRunFeedback(state, event);
90
+ renderer.appendDurable(event, state);
91
+ }
92
+ return stdout.writes
93
+ .join("")
94
+ .split("\n")
95
+ .filter(Boolean)
96
+ .map((line) => JSON.parse(line) as Record<string, unknown>);
97
+ }
98
+
99
+ describe("warning 事件:code 是稳定词法,折叠身份走具名字段", () => {
100
+ it("code 用 DiagnosticInput.code 的干净字面量,不透传编了身份的去重 key", () => {
101
+ const [warning] = emitDurable([
102
+ {
103
+ type: "diagnostic",
104
+ at: 1,
105
+ key: "lock-taken-over:compare/codex|memory/retention",
106
+ code: "lock-taken-over",
107
+ severity: "warning",
108
+ message: "took over a stale lock",
109
+ data: { experimentId: "compare/codex", evalId: "memory/retention" },
110
+ },
111
+ ]);
112
+ expect(warning).toMatchObject({
113
+ event: "warning",
114
+ code: "lock-taken-over",
115
+ level: "warning",
116
+ experimentId: "compare/codex",
117
+ evalId: "memory/retention",
118
+ });
119
+ });
120
+
121
+ it("没给 code 的诊断回落到 key(折叠身份本就不进 key 的那些天生是干净字面量)", () => {
122
+ const [warning] = emitDurable([
123
+ { type: "diagnostic", at: 1, key: "memory-warmup-degraded", severity: "warning", message: "cold index" },
124
+ ]);
125
+ expect(warning).toMatchObject({ code: "memory-warmup-degraded" });
126
+ expect(warning).not.toHaveProperty("experimentId");
127
+ expect(warning).not.toHaveProperty("evalId");
128
+ });
129
+
130
+ it("eval 闸的 dispatch-halted:code 干净、evalId 与 phase 都在事件流里透得出", () => {
131
+ const [warning] = emitDurable([
132
+ {
133
+ type: "diagnostic",
134
+ at: 1,
135
+ key: "dispatch-halted:eval:compare/codex|memory/retention",
136
+ code: HALT_DIAGNOSTIC_CODE,
137
+ severity: "error",
138
+ message: "eval halted: fixture db is empty; run scripts/seed.ts",
139
+ data: {
140
+ experimentId: "compare/codex",
141
+ scope: "eval",
142
+ evalId: "memory/retention",
143
+ phase: "eval.run",
144
+ unstarted: 0,
145
+ },
146
+ },
147
+ ]);
148
+ expect(warning).toMatchObject({
149
+ event: "warning",
150
+ code: "dispatch-halted",
151
+ level: "error",
152
+ phase: "eval.run",
153
+ experimentId: "compare/codex",
154
+ evalId: "memory/retention",
155
+ });
156
+ });
157
+
158
+ it("身份从 data 取(闸不是 attempt 级、不伪造 identity);有 identity 时 identity 优先", () => {
159
+ const [fromIdentity] = emitDurable([
160
+ {
161
+ type: "diagnostic",
162
+ at: 1,
163
+ key: "fail-fast:x",
164
+ code: "fail-fast",
165
+ severity: "warning",
166
+ message: "deterministic failure",
167
+ identity: { experimentId: "compare/codex", evalId: "memory/a", attempt: 0 },
168
+ data: { experimentId: "other/exp", evalId: "memory/z" },
169
+ },
170
+ ]);
171
+ expect(fromIdentity).toMatchObject({ experimentId: "compare/codex", evalId: "memory/a" });
172
+ });
173
+
174
+ it("同一 dedupeKey 只追加一次:emitter 为刷新 data.unstarted 反复报同一条闸,事件流不重复", () => {
175
+ const halted = (at: number, unstarted: number): DurableFeedbackEvent => ({
176
+ type: "diagnostic",
177
+ at,
178
+ key: "dispatch-halted:experiment:compare/codex",
179
+ code: HALT_DIAGNOSTIC_CODE,
180
+ severity: "error",
181
+ message: "experiment halted (dispatch-halted): shared service is down",
182
+ data: { experimentId: "compare/codex", scope: "experiment", phase: "eval.run", unstarted },
183
+ });
184
+ const events = emitDurable([halted(1, 0), halted(2, 1), halted(3, 2)]);
185
+ expect(events.filter((e) => e.event === "warning")).toHaveLength(1);
186
+ });
187
+ });
188
+
80
189
  describe("renderJsonPlanDocument:单个 ExpPlanDocument,不是事件流", () => {
81
190
  it("输出恰好一行 JSON,可解析为单个对象而不是逐行事件序列", () => {
82
191
  const text = renderJsonPlanDocument({
@@ -101,6 +210,22 @@ describe("renderJsonPlanDocument:单个 ExpPlanDocument,不是事件流", () =>
101
210
  expect(doc.matrix).toHaveLength(2);
102
211
  });
103
212
 
213
+ it("locked 为 true 的行原样透传;省略的行不出现 locked 字段(JSON.stringify 丢弃 undefined 属性)", () => {
214
+ const text = renderJsonPlanDocument({
215
+ total: 2,
216
+ evals: 2,
217
+ configs: 1,
218
+ runs: 1,
219
+ matrix: [
220
+ { experimentId: "compare/codex", evalId: "memory/a", reused: false, locked: true },
221
+ { experimentId: "compare/codex", evalId: "memory/b", reused: false },
222
+ ],
223
+ });
224
+ const doc = JSON.parse(text);
225
+ expect(doc.matrix[0]).toMatchObject({ evalId: "memory/a", locked: true });
226
+ expect(doc.matrix[1]).not.toHaveProperty("locked");
227
+ });
228
+
104
229
  it("reused 是 matrix 逐行 reused 之和(命中数量,不是 attempt 数)", () => {
105
230
  const text = renderJsonPlanDocument({
106
231
  total: 3,
@@ -104,13 +104,24 @@ export function createJsonRenderer(options: JsonRendererOptions): FeedbackRender
104
104
  noteCheckpoint(event.at);
105
105
  if (!isFirstOccurrence(state, event.key)) return; // 去重后只追加一次(cli.md)
106
106
  const phase = typeof event.data?.phase === "string" ? (event.data.phase as LifecyclePhase) : undefined;
107
+ // `code` 是 cli.md `WarningEvent` 里那个稳定词法(`lock-taken-over` / `dispatch-halted`),
108
+ // **不是**去重 key:去重 key 常把折叠身份编进去(`lock-taken-over:<exp>|<eval>`),原样
109
+ // 透出会让消费方拿到一个每次运行都不同的 code、没法按值分支。折叠到哪一条实验/用例
110
+ // 由下面的 experimentId/evalId 两个具名字段回答。
111
+ const code = event.code ?? event.key;
112
+ // 身份两字段:attempt 级诊断从 identity 取;运行级诊断(实验闸 / eval 闸这类不属于
113
+ // 任何单条 attempt 的事实)不许伪造 identity,从 `data` 的同名字段取(见 ../types.ts
114
+ // "diagnostic" 变体的 identity 注释)。
115
+ const experimentId = event.identity?.experimentId ?? stringField(event.data?.experimentId);
116
+ const evalId = event.identity?.evalId ?? stringField(event.data?.evalId);
107
117
  writeEvent(io, {
108
118
  event: "warning",
109
- code: event.key,
119
+ code,
110
120
  level: event.severity,
111
121
  message: event.message,
112
122
  ...(phase !== undefined ? { phase } : {}),
113
- ...(event.identity?.experimentId !== undefined ? { experimentId: event.identity.experimentId } : {}),
123
+ ...(experimentId !== undefined ? { experimentId } : {}),
124
+ ...(evalId !== undefined ? { evalId } : {}),
114
125
  });
115
126
  return;
116
127
  }
@@ -148,6 +159,26 @@ export function createJsonRenderer(options: JsonRendererOptions): FeedbackRender
148
159
  return;
149
160
  }
150
161
 
162
+ case "lock-wait": {
163
+ noteCheckpoint(event.at);
164
+ writeEvent(io, {
165
+ event: "lock_wait",
166
+ experimentId: event.experimentId,
167
+ evalId: event.evalId,
168
+ status: event.status,
169
+ ...(event.holderPid !== undefined ? { holderPid: event.holderPid } : {}),
170
+ ...(event.holderHost !== undefined ? { holderHost: event.holderHost } : {}),
171
+ // 折叠 carried/dispatched 两个内部计数为一个公开 resolution:仍有 attempt 需要真实
172
+ // 派发(dispatched > 0)记 "dispatched",全部由携带满足才记 "carried"
173
+ // (见 ../types.ts 里 DurableFeedbackEvent "lock-wait" 变体的字段注释)。
174
+ ...(event.status === "resolved"
175
+ ? { resolution: (event.dispatched ?? 0) > 0 ? "dispatched" : "carried" }
176
+ : {}),
177
+ ...(event.waitedMs !== undefined ? { waitedMs: event.waitedMs } : {}),
178
+ });
179
+ return;
180
+ }
181
+
151
182
  case "interrupted": {
152
183
  noteCheckpoint(event.at);
153
184
  if (!isFirstOccurrence(state, "interrupted")) return;
@@ -208,8 +239,14 @@ export function createJsonRenderer(options: JsonRendererOptions): FeedbackRender
208
239
  total: state.total,
209
240
  reused: state.reused,
210
241
  running: state.running,
242
+ elsewhere: state.elsewhere,
211
243
  queued: state.queued,
212
- completed: state.completed,
244
+ // 与 human 首行同一份状态、同一套划分:已了结的 attempt 按 verdict 分项,消费方不必
245
+ // 自己从合计数里猜成败(契约见 docs/feature/experiments/cli.md 的 ProgressEvent)。
246
+ passed: state.passed,
247
+ failed: state.failed,
248
+ errored: state.errored,
249
+ skipped: state.skipped,
213
250
  });
214
251
  },
215
252
 
@@ -225,6 +262,12 @@ function isFirstOccurrence(state: RunFeedbackState, key: string): boolean {
225
262
  return (state.diagnostics.find((d) => d.key === key)?.count ?? 0) <= 1;
226
263
  }
227
264
 
265
+ /** `data` 里的一个字符串字段;非字符串(或缺失)一律当没有,不把数字/对象硬塞进只接受
266
+ * 字符串的事件字段里。 */
267
+ function stringField(value: JsonValue | undefined): string | undefined {
268
+ return typeof value === "string" ? value : undefined;
269
+ }
270
+
228
271
  // ───────────────────────── failure / error 事件(不设上限) ─────────────────────────
229
272
 
230
273
  function writeFailureOrError(io: FeedbackIO, event: DurableFeedbackEvent & { type: "failure" }): void {
@@ -346,6 +389,11 @@ export interface JsonPlanRow {
346
389
  evalId: string;
347
390
  /** 命中缓存指纹,本次不会派发新 attempt。 */
348
391
  reused: boolean;
392
+ /** 该用例正被另一条并行 Invocation 持锁运行,真实运行时将等待后携带或补跑(见
393
+ * docs/feature/experiments/architecture.md「并发 Invocation:用例锁」)。`--dry` 只读锁
394
+ * 目录,不取锁、不等待;省略等于 `false`(JSON.stringify 丢弃 `undefined` 属性,
395
+ * 天然满足这条省略语义,不需要显式写 `locked: false`)。 */
396
+ locked?: boolean;
349
397
  }
350
398
 
351
399
  export interface JsonPlanInput {