@visual-z/xeno 0.7.2 → 0.8.1

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 (117) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +36 -9
  3. package/docs/foundations.md +1 -1
  4. package/package.json +3 -1
  5. package/src/apps/cli.ts +4 -3
  6. package/src/apps/commands.ts +8 -0
  7. package/src/apps/goal-flow.ts +70 -0
  8. package/src/apps/render.ts +0 -2
  9. package/src/apps/session-hub.ts +6 -5
  10. package/src/apps/session-lifecycle.ts +64 -0
  11. package/src/apps/shell.ts +89 -34
  12. package/src/apps/startup.ts +32 -31
  13. package/src/apps/ui/app.ts +105 -13
  14. package/src/apps/ui/control-model.ts +18 -1
  15. package/src/apps/ui/surface.ts +2 -0
  16. package/src/apps/update-check.ts +1 -1
  17. package/src/core/acceptance.ts +41 -9
  18. package/src/core/blind-checks.ts +72 -0
  19. package/src/core/branch-receipt.ts +3 -1
  20. package/src/core/checkpoints.ts +21 -0
  21. package/src/core/churn.ts +2 -2
  22. package/src/core/completion-review.ts +18 -11
  23. package/src/core/continuation.ts +11 -10
  24. package/src/core/control-view.ts +5 -8
  25. package/src/core/deadline.ts +2 -1
  26. package/src/core/derive.ts +0 -7
  27. package/src/core/environment.ts +2 -2
  28. package/src/core/event-validation.ts +19 -6
  29. package/src/core/events.ts +41 -32
  30. package/src/core/evidence-ladder.ts +62 -0
  31. package/src/core/evidence-links.ts +0 -10
  32. package/src/core/evidence.ts +1 -2
  33. package/src/core/export.ts +6 -11
  34. package/src/core/goal-view.ts +64 -0
  35. package/src/core/goal.ts +200 -0
  36. package/src/core/history.ts +1 -11
  37. package/src/core/json-event.ts +0 -6
  38. package/src/core/outcome.ts +0 -1
  39. package/src/core/program-status.ts +11 -2
  40. package/src/core/progress.ts +14 -8
  41. package/src/core/project.ts +6 -16
  42. package/src/core/read-bounds.ts +11 -2
  43. package/src/core/read-observation.ts +24 -0
  44. package/src/core/request-budget.ts +13 -3
  45. package/src/core/request-state.ts +178 -19
  46. package/src/core/response-recovery.ts +15 -2
  47. package/src/core/session-entry.ts +8 -3
  48. package/src/core/session-exit.ts +19 -3
  49. package/src/core/session-format.ts +169 -0
  50. package/src/core/settings.ts +7 -27
  51. package/src/core/stall.ts +30 -7
  52. package/src/core/stop-report.ts +4 -3
  53. package/src/core/transcript.ts +22 -2
  54. package/src/runtime/blind-check.ts +36 -0
  55. package/src/runtime/counter-check.ts +74 -0
  56. package/src/runtime/exec-sessions.ts +11 -0
  57. package/src/runtime/foundation.ts +46 -8
  58. package/src/runtime/loop.ts +239 -430
  59. package/src/runtime/oauth.ts +1 -3
  60. package/src/runtime/openai.ts +2 -2
  61. package/src/runtime/pi-provider.ts +3 -3
  62. package/src/runtime/provider-request.ts +2 -1
  63. package/src/runtime/provider.ts +33 -3
  64. package/src/runtime/retry.ts +9 -2
  65. package/src/runtime/runtime-target.ts +2 -2
  66. package/src/runtime/session-control.ts +4 -4
  67. package/src/runtime/tools.ts +44 -42
  68. package/src/runtime/usage.ts +28 -0
  69. package/src/runtime/version.ts +12 -0
  70. package/src/runtime/workspace-lifecycle.ts +31 -0
  71. package/src/store/blind-checks.ts +32 -0
  72. package/src/store/checkpoint-history.ts +1 -12
  73. package/src/store/file-index.ts +88 -7
  74. package/src/store/log.ts +25 -27
  75. package/src/store/recovery.ts +1 -3
  76. package/src/store/replay.ts +2 -2
  77. package/src/store/restore.ts +10 -202
  78. package/src/store/search.ts +7 -5
  79. package/src/store/session-archive.ts +0 -1
  80. package/src/store/sessions.ts +31 -33
  81. package/src/store/workspace-clone.ts +42 -0
  82. package/src/store/workspace.ts +10 -157
  83. package/src/tui/ui/duration.ts +16 -0
  84. package/src/tui/ui/layout.ts +4 -4
  85. package/src/tui/ui/screen.ts +13 -7
  86. package/src/tui/ui/status.ts +99 -17
  87. package/src/core/benchmark.ts +0 -195
  88. package/src/core/evaluation-coverage.ts +0 -74
  89. package/src/core/experiment.ts +0 -173
  90. package/src/core/harness-comparison.ts +0 -90
  91. package/src/core/harness-diagnostics.ts +0 -206
  92. package/src/core/native-eval.ts +0 -25
  93. package/src/core/policy-evaluation.ts +0 -55
  94. package/src/core/query-eval-contract.ts +0 -14
  95. package/src/core/recall-analysis.ts +0 -90
  96. package/src/core/recall-trial.ts +0 -153
  97. package/src/core/recovery-corpus.ts +0 -17
  98. package/src/core/recovery-evaluation.ts +0 -60
  99. package/src/core/tool-mode-protocol.ts +0 -25
  100. package/src/core/trial-lifecycle.ts +0 -14
  101. package/src/core/window-trial.ts +0 -65
  102. package/src/index.ts +0 -10
  103. package/src/runtime/benchmark.ts +0 -187
  104. package/src/runtime/harness-comparison.ts +0 -376
  105. package/src/runtime/harness-eval.ts +0 -462
  106. package/src/runtime/native-eval-fixtures.ts +0 -33
  107. package/src/runtime/native-eval.ts +0 -146
  108. package/src/runtime/query-eval-verifier.ts +0 -28
  109. package/src/runtime/recall-trial.ts +0 -226
  110. package/src/runtime/recovery-coverage.ts +0 -71
  111. package/src/runtime/recovery-evidence.ts +0 -76
  112. package/src/runtime/recovery-fixtures.ts +0 -116
  113. package/src/runtime/tool-mode-eval.ts +0 -146
  114. package/src/runtime/trial-control.ts +0 -45
  115. package/src/runtime/window-trial.ts +0 -141
  116. package/src/store/git-branch.ts +0 -35
  117. package/src/tui/ui/testing.ts +0 -5
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { clipSegs, clipText, segsWidth, splitLine, type Label, type Seg } from "./edge.ts";
16
16
  import { displayWidth } from "../width.ts";
17
+ import { formatElapsed } from "./duration.ts";
17
18
 
18
19
  export interface ContextStatus {
19
20
  readonly used: number;
@@ -30,7 +31,13 @@ export interface StatusModel {
30
31
  readonly version: string;
31
32
  /** `/name`, else the first prompt; null for a session with neither. */
32
33
  readonly title: string | null;
33
- readonly goal?: { readonly text: string; readonly status: string; readonly execution: string; readonly observations: number; readonly retries: number };
34
+ /**
35
+ * An explicit /goal, prepared by apps from the log (core/goal-view.ts). The
36
+ * block exists only while a goal is set, and for one message after it is met.
37
+ */
38
+ readonly goal?: GoalStatusView;
39
+ /** Without a goal: the latest check after this request's last edit, when it changed files. */
40
+ readonly checks?: { readonly status: "unchecked" | "pass" | "fail" | "stale" | "partial"; readonly command?: string; readonly exit?: number };
34
41
  readonly workspace: string;
35
42
  readonly workspaceShort: string;
36
43
  readonly branch: string | null;
@@ -39,7 +46,8 @@ export interface StatusModel {
39
46
  readonly activity?: { readonly state: string; readonly label: string; readonly detail: string; readonly queued: number };
40
47
  readonly git?: { readonly staged: number; readonly changed: number; readonly untracked: number; readonly conflicted: number; readonly ahead: number; readonly behind: number } | null;
41
48
  readonly context?: ContextStatus;
42
- readonly usage?: { readonly input: number; readonly output: number; readonly cacheRate: number; readonly cost: string | null };
49
+ /** `speed`: output tokens per second over this turn's model requests (runtime/usage.ts generationSpeed). */
50
+ readonly usage?: { readonly input: number; readonly output: number; readonly cacheRate: number; readonly cost: string | null; readonly speed?: number };
43
51
  readonly tools: { readonly running: number; readonly done: number; readonly failed: number };
44
52
  readonly handoff?: { readonly verified: readonly string[]; readonly inFlight: readonly string[]; readonly unverified: readonly string[] };
45
53
  readonly tasks?: readonly { readonly what: string; readonly status: "active" | "blocked" }[];
@@ -49,6 +57,18 @@ export interface StatusModel {
49
57
  readonly extension?: string;
50
58
  }
51
59
 
60
+ export interface GoalStatusView {
61
+ readonly phase: "drafting" | "working" | "needs_you" | "met" | "stopped";
62
+ readonly objective: string;
63
+ readonly elapsedMs: number;
64
+ readonly checks: readonly { readonly id: string; readonly kind: "command" | "human" | "fresh_check"; readonly status: string; readonly guard?: true; readonly exit?: number }[];
65
+ /** S.1: one glyph per rung (ran, tests the change, independent, human), prepared by apps. */
66
+ readonly ladder?: { readonly glyphs: string; readonly summary: string };
67
+ /** Model requests since the goal was set (from the log) and output tokens this process saw. */
68
+ readonly requests: number;
69
+ readonly tokens?: number;
70
+ }
71
+
52
72
  export type Line = readonly Seg[];
53
73
 
54
74
  /** 1234 → "1.2k", 1234567 → "1.2m". The same compact form everywhere a token count appears. */
@@ -148,6 +168,10 @@ interface Block {
148
168
  readonly items: readonly Line[];
149
169
  /** A list block with no items is not shown at all. */
150
170
  readonly list: boolean;
171
+ /** Rows shown after the items, kept with the head (the goal's ladder and spend). */
172
+ readonly tail?: readonly Line[];
173
+ /** A list shown even without items: a goal being drafted has no checks yet. */
174
+ readonly keep?: boolean;
151
175
  }
152
176
 
153
177
  /**
@@ -158,12 +182,12 @@ interface Block {
158
182
  */
159
183
  export function sidebarLines(model: StatusModel, width: number, height: number): Line[] {
160
184
  const w = Math.max(1, Math.floor(width)), h = Math.max(0, Math.floor(height));
161
- const blocks = sidebarBlocks(model, w).filter(block => !block.list || block.items.length > 0);
185
+ const blocks = sidebarBlocks(model, w).filter(block => !block.list || block.items.length > 0 || block.keep);
162
186
  const plan: { block: Block; rows: number }[] = [];
163
187
  let used = 0;
164
188
  for (const block of blocks) {
165
189
  const minimum = block.list ? Math.min(block.items.length, 2) : 0;
166
- const need = (plan.length ? 1 : 0) + block.head.length + minimum;
190
+ const need = (plan.length ? 1 : 0) + block.head.length + minimum + (block.tail?.length ?? 0);
167
191
  if (used + need > h) continue;
168
192
  used += need;
169
193
  plan.push({ block, rows: minimum });
@@ -183,6 +207,7 @@ export function sidebarLines(model: StatusModel, width: number, height: number):
183
207
  out.push(...items.slice(0, entry.rows - 1));
184
208
  out.push([{ text: `+${items.length - entry.rows + 1} more`, role: "muted" }]);
185
209
  }
210
+ out.push(...entry.block.tail ?? []);
186
211
  });
187
212
  return out.slice(0, h).map(line => clipSegs(line, w));
188
213
  }
@@ -201,34 +226,37 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
201
226
  ...(model.git.conflicted ? [[{ text: `${model.git.conflicted} conflicts`, role: "warning" }] as Line] : []),
202
227
  ...(model.git.ahead || model.git.behind ? [[{ text: `↑${model.git.ahead} ↓${model.git.behind}`, role: "muted" }] as Line] : [])] : model.git === null ? [[{ text: "Git unavailable", role: "muted" }] as Line] : []),
203
228
  ] });
229
+ const goal = model.goal;
230
+ if (goal) blocks.push(goalBlock(goal, w, row));
231
+ else if (model.checks) blocks.push({ list: false, items: [], head: [checksLine(model.checks, w)] });
204
232
  const context = model.context;
205
- if (model.goal) blocks.push({ list: false, items: [], head: [
206
- [{ text: "Goal", role: "text", bold: true }, { text: ` · ${model.goal.status}`, role: model.goal.status === "verified" ? "success" : "warning" }],
207
- ...wrap(firstLine(model.goal.text), w, 2).map(text => [{ text, role: "text" }] as Line),
208
- [{ text: model.goal.execution, role: "muted" }],
209
- [{ text: `${model.goal.observations} observations · ${model.goal.retries} retries`, role: "muted" }],
210
- [{ text: "/control · goal, evidence, context", role: "muted" }],
211
- ] });
212
233
  if (context) {
213
234
  const share = contextShare(context), est = context.estimated ? "~" : "";
214
235
  const filled = share === undefined ? 0 : Math.max(share > 0 ? 1 : 0, Math.round(share * w));
215
236
  const tone = share !== undefined && share >= CONTEXT_WARNING ? "warning" : "accent";
216
237
  const shareText = shareSeg(context);
238
+ // Two budgets, each named: the segment cut (what the bar shows) and the model window. Both shares
239
+ // ride on the heading, both denominators on one row below the bar.
240
+ const modelShare = context.window && context.trigger !== null ? `${est}${Math.round(context.used / context.window * 100)}%` : undefined;
241
+ const segment = context.trigger !== null ? `seg ${est}${compact(context.used)} / ${est}${compact(context.trigger)}` : `${est}${compact(context.used)} · no cut`;
217
242
  blocks.push({ list: false, items: [], head: [
218
- row([{ text: "Context", role: "text", bold: true }], shareText ? [{ text: context.trigger === null ? "model " : "seg ", role: "muted" }, shareText] : []),
243
+ row([{ text: "Context", role: "text", bold: true }], [
244
+ ...(shareText ? [{ text: context.trigger === null ? "model " : "seg ", role: "muted" as const }, shareText] : []),
245
+ ...(modelShare ? [{ text: ` · model ${modelShare}`, role: "muted" as const }] : [])]),
219
246
  ...(share === undefined ? [] : [[{ text: "━".repeat(filled), role: tone }, { text: "─".repeat(Math.max(0, w - filled)), role: "line" }] as Line]),
220
- [{ text: `Segment ${est}${compact(context.used)}${context.trigger !== null ? ` / ${est}${compact(context.trigger)}` : " · cut disabled"}`, role: "text" }],
221
- [{ text: context.window ? `Model ${est}${compact(context.used)} / ${compact(context.window)} · ${est}${Math.round(context.used / context.window * 100)}%` : "Model window unknown", role: "muted" }],
222
- [{ text: `node ${context.segment}`, role: "muted" }],
247
+ row([{ text: segment, role: "muted" }], [{ text: context.window ? `model ${compact(context.window)}` : "window unknown", role: "muted" }]),
223
248
  ] });
224
249
  }
225
250
  const usage = model.usage, tools = model.tools;
226
251
  const toolCount = tools.done + tools.failed + tools.running;
227
252
  if (usage || toolCount) {
253
+ const figures = usage ? [`↑${compact(usage.input)} ↓${compact(usage.output)}`, `cache ${Math.round(usage.cacheRate * 100)}%`,
254
+ ...(usage.speed !== undefined ? [speedText(usage.speed)] : [])].join(" · ") : "";
255
+ // Heading with the cost; one row of figures below it. Together they need about 49 columns, more than the 34-42 the sidebar has.
228
256
  blocks.push({ list: false, items: [], head: [
229
257
  row([{ text: "Usage", role: "text", bold: true }], usage?.cost ? [{ text: usage.cost, role: "text" }] : []),
230
- ...(usage ? [[{ text: `↑${compact(usage.input)} ↓${compact(usage.output)}`, role: "text" }, { text: ` cache ${Math.round(usage.cacheRate * 100)}%`, role: "muted" }] as Line] : []),
231
- ...(toolCount ? [[
258
+ ...(figures ? [[{ text: figures, role: "muted" }] as Line] : []),
259
+ ...(tools.failed || tools.running ? [[
232
260
  { text: `${tools.done} done`, role: "muted" },
233
261
  ...(tools.failed ? [{ text: " · ", role: "muted" as const }, { text: `${tools.failed} failed`, role: "error" as const }] : []),
234
262
  ...(tools.running ? [{ text: " · ", role: "muted" as const }, { text: `${tools.running} running`, role: "accent" as const }] : []),
@@ -266,6 +294,60 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
266
294
  return blocks;
267
295
  }
268
296
 
297
+ /** `42 tok/s`; under 10 one decimal, so a slow upstream does not read as zero. */
298
+ export function speedText(speed: number): string {
299
+ return `${speed < 10 ? speed.toFixed(1) : Math.round(speed)} tok/s`;
300
+ }
301
+
302
+ const PHASE: Record<GoalStatusView["phase"], { label: string; role: Seg["role"] }> = {
303
+ drafting: { label: "drafting", role: "accent" }, working: { label: "working", role: "accent" },
304
+ needs_you: { label: "needs you", role: "warning" }, met: { label: "met", role: "success" }, stopped: { label: "stopped", role: "warning" },
305
+ };
306
+
307
+ /**
308
+ * `Goal · needs you 4m 12s`, the objective, one row per accepted check with
309
+ * what it needs, the evidence ladder and what the goal has spent. A met goal
310
+ * is one summary row. The sidebar may cut the checks; a cut ends in "+N more".
311
+ */
312
+ function goalBlock(goal: GoalStatusView, w: number, row: (left: readonly Seg[], right?: readonly Seg[]) => Line): Block {
313
+ const phase = PHASE[goal.phase];
314
+ const passed = goal.checks.filter(check => check.status === "pass" || check.status === "accepted").length;
315
+ const elapsed = goal.phase === "drafting" ? [] : [{ text: formatElapsed(goal.elapsedMs), role: "muted" as const }];
316
+ const title = [{ text: "Goal", role: "text" as const, bold: true }, { text: ` · ${phase.label}`, role: phase.role }];
317
+ if (goal.phase === "met") return { list: false, items: [], head: [row(title, [{ text: `${passed}/${goal.checks.length} · `, role: "muted" }, ...elapsed])] };
318
+ const items = goal.checks.map(check => {
319
+ const done = check.status === "pass" || check.status === "accepted";
320
+ const glyph: Seg = done ? { text: "✓ ", role: "success" } : check.status === "fail" ? { text: "✗ ", role: "error" } : { text: "○ ", role: "muted" };
321
+ const need = check.kind === "human" && !done ? `/accept ${check.id}` : check.status === "fail" && check.exit !== undefined ? `exit ${check.exit}`
322
+ : check.status === "stale" ? "rerun" : check.guard ? "guard" : "";
323
+ return row([glyph, { text: check.id, role: "text" }], need ? [{ text: need, role: check.kind === "human" && !done ? "warning" : "muted" }] : []);
324
+ });
325
+ const spent = [`${goal.requests} ${goal.requests === 1 ? "request" : "requests"}`, ...(goal.tokens ? [`${compact(goal.tokens)} tokens this run`] : [])].join(" · ");
326
+ return { list: true, keep: true, head: [row(title, elapsed), ...wrap(firstLine(goal.objective), w, 2).map(text => [{ text, role: "text" }] as Line)],
327
+ items, ...(goal.phase === "drafting" ? {} : { tail: [...(goal.ladder ? [[{ text: goal.ladder.glyphs, role: "text" }] as Line] : []), [{ text: spent, role: "muted" }] as Line] }) };
328
+ }
329
+
330
+ /** Without a goal: whether a check has passed since this request's last edit. */
331
+ function checksLine(checks: NonNullable<StatusModel["checks"]>, w: number): Line {
332
+ if (checks.status === "unchecked" || !checks.command) return [{ text: "Checks", role: "text", bold: true }, { text: " · not run since last edit", role: "warning" }];
333
+ // partial: some of a declared contract holds; the rest may only be waiting for the user, so it is not a failure mark.
334
+ const mark: Seg[] = checks.status === "pass" ? [{ text: " ✓", role: "success" }] : checks.status === "partial" ? []
335
+ : checks.status === "fail" ? [{ text: " ✗", role: "error" }, ...(checks.exit === undefined ? [] : [{ text: ` exit ${checks.exit}`, role: "muted" as const }])]
336
+ : [{ text: " stale", role: "warning" }];
337
+ const room = Math.max(1, w - segsWidth(mark) - "Checks · ".length);
338
+ return [{ text: "Checks", role: "text", bold: true }, { text: ` · ${clipText(checks.command, room)}`, role: "muted" }, ...mark];
339
+ }
340
+
341
+ /** Without a sidebar the goal rides on the input box's bottom edge: `goal 2/3 · needs you · 4m 12s`. */
342
+ export function goalLabel(model: StatusModel): Label | undefined {
343
+ const goal = model.goal;
344
+ if (!goal) return undefined;
345
+ const passed = goal.checks.filter(check => check.status === "pass" || check.status === "accepted").length;
346
+ const parts = [`goal ${goal.phase === "drafting" ? "" : `${passed}/${goal.checks.length} · `}${PHASE[goal.phase].label}`,
347
+ ...(goal.phase === "drafting" ? [] : [formatElapsed(goal.elapsedMs)])];
348
+ return { segs: [{ text: parts.join(" · "), role: goal.phase === "needs_you" ? "warning" : "muted" }], priority: 2, min: 8 };
349
+ }
350
+
269
351
  function fileLine(file: FileStatus, w: number): Line {
270
352
  const name = file.path.split("/").pop() || file.path;
271
353
  const counts: Seg[] = file.edited && (file.added || file.removed)
@@ -1,195 +0,0 @@
1
- /** Evaluation contracts, separate from production policies (research M.3). */
2
- import { shuffled } from "./experiment.ts";
3
-
4
- export const LONGBENCH_SOURCE = {
5
- revision: "2b48e494f2c7a2f0af81aae178e05c7e1dde0fe9",
6
- sha256: "15d61c22d92c96900b3c4948b6aeea218d3214b676a65df48e7b8555604c7fe2",
7
- scorerRevision: "2e00731f8d0bff23dc4325161044d0ed8af94c1e",
8
- } as const;
9
-
10
- export const BENCHMARK_ARMS = ["node", "replay", "mask"] as const;
11
- export type BenchmarkArm = typeof BENCHMARK_ARMS[number];
12
- export type Choice = "A" | "B" | "C" | "D";
13
- export interface BenchmarkItem {
14
- _id: string;
15
- domain: string;
16
- sub_domain: string;
17
- difficulty: "easy" | "hard";
18
- length: "short" | "medium" | "long";
19
- question: string;
20
- choice_A: string;
21
- choice_B: string;
22
- choice_C: string;
23
- choice_D: string;
24
- answer: Choice;
25
- context: string;
26
- }
27
-
28
- function isBenchmarkItem(value: unknown): value is BenchmarkItem {
29
- if (typeof value !== "object" || value === null) return false;
30
- const row = value as Record<string, unknown>;
31
- for (const key of ["_id", "domain", "sub_domain", "question", "choice_A", "choice_B", "choice_C", "choice_D", "context"]) {
32
- if (typeof row[key] !== "string" || row[key].length === 0) return false;
33
- }
34
- return (row["answer"] === "A" || row["answer"] === "B" || row["answer"] === "C" || row["answer"] === "D") &&
35
- (row["difficulty"] === "easy" || row["difficulty"] === "hard") &&
36
- (row["length"] === "short" || row["length"] === "medium" || row["length"] === "long");
37
- }
38
-
39
- export function parseBenchmark(data: unknown): BenchmarkItem[] {
40
- if (!Array.isArray(data) || data.length === 0) throw new Error("empty or invalid benchmark");
41
- const ids = new Set<string>();
42
- return data.map((item: unknown) => {
43
- if (!isBenchmarkItem(item)) throw new Error("invalid benchmark item");
44
- if (ids.has(item._id)) throw new Error(`duplicate benchmark id: ${item._id}`);
45
- ids.add(item._id);
46
- return item;
47
- });
48
- }
49
-
50
- /** Exact LongBench v2 pred.py extraction, pinned in docs/benchmarks.md (MIT). */
51
- export function extractBenchmarkAnswer(response: string): Choice | null {
52
- const plain = response.replaceAll("*", "");
53
- return (plain.match(/The correct answer is \(([A-D])\)/)?.[1] ??
54
- plain.match(/The correct answer is ([A-D])/)?.[1] ?? null) as Choice | null;
55
- }
56
-
57
- /** Domain-balanced sampling; neither correctness nor answer labels enter selection. */
58
- export function sampleBenchmark(items: readonly BenchmarkItem[], count: number, seed: number): BenchmarkItem[] {
59
- if (!Number.isSafeInteger(count) || count < 1 || count > items.length) throw new RangeError("invalid sample count");
60
- const domains = [...new Set(items.map((item) => item.domain))].sort();
61
- const buckets = shuffled(domains, seed).map((domain, index) => shuffled(
62
- items.filter((item) => item.domain === domain).toSorted((a, b) => a._id.localeCompare(b._id)), seed + index + 1,
63
- ));
64
- const out: BenchmarkItem[] = [];
65
- for (let round = 0; out.length < count; round++) {
66
- for (const bucket of buckets) {
67
- const item = bucket[round];
68
- if (item !== undefined && out.length < count) out.push(item);
69
- }
70
- }
71
- return out;
72
- }
73
-
74
- export interface BenchmarkJob { id: string; arm: BenchmarkArm }
75
- export type HarnessProfile = "control" | "enhanced";
76
- export interface HarnessJob { id: string; arm: "node" | "replay"; profile: HarnessProfile }
77
-
78
- /** A Latin rotation balances each treatment's position before running any model. */
79
- export function harnessSchedule(items: readonly BenchmarkItem[], seed: number): HarnessJob[] {
80
- const treatments = shuffled([
81
- { arm: "node", profile: "control" }, { arm: "replay", profile: "control" },
82
- { arm: "node", profile: "enhanced" }, { arm: "replay", profile: "enhanced" },
83
- ] as const, seed);
84
- const offsets = shuffled(items.map((_, index) => index % treatments.length), seed + 1);
85
- return items.flatMap((item, index) => treatments.map((_, position) => ({
86
- id: item._id, ...treatments[(position + offsets[index]!) % treatments.length]!,
87
- })));
88
- }
89
-
90
- export function summarizeHarness(plan: readonly HarnessJob[], results: readonly (BenchmarkOutcome & { profile: HarnessProfile })[]) {
91
- return (["control", "enhanced"] as const).flatMap((profile) => summarizeBenchmark(
92
- plan.filter((job) => job.profile === profile), results.filter((result) => result.profile === profile),
93
- ).filter((row) => row.planned > 0).map((row) => ({ profile, ...row })));
94
- }
95
- /** Pair nearby in time, randomize treatment order inside each task, once before running. */
96
- export function benchmarkSchedule(items: readonly BenchmarkItem[], seed: number): BenchmarkJob[] {
97
- // Balance all six orders before shuffling. Adjacent LCG seeds yield correlated
98
- // first draws and can accidentally put the same arm last in every block.
99
- const orders: readonly (readonly BenchmarkArm[])[] = [
100
- ["node", "replay", "mask"], ["node", "mask", "replay"], ["replay", "node", "mask"],
101
- ["replay", "mask", "node"], ["mask", "node", "replay"], ["mask", "replay", "node"],
102
- ];
103
- const balanced = shuffled(items.map((_, index) => orders[index % orders.length]!), seed);
104
- return items.flatMap((item, index) => balanced[index]!.map((arm) => ({ id: item._id, arm })));
105
- }
106
-
107
- /** Split losslessly, including surrogate pairs; chunks are files, not a context truncation. */
108
- export function benchmarkChunks(context: string, size = 16_000): string[] {
109
- if (!Number.isSafeInteger(size) || size < 2) throw new RangeError("chunk size must be at least two");
110
- const chunks: string[] = [];
111
- for (let start = 0; start < context.length;) {
112
- let end = Math.min(context.length, start + size);
113
- const last = context.charCodeAt(end - 1);
114
- if (end < context.length && last >= 0xd800 && last <= 0xdbff) end--;
115
- chunks.push(context.slice(start, end));
116
- start = end;
117
- }
118
- return chunks;
119
- }
120
-
121
- /** Explicit field selection prevents evaluator-only metadata from leaking to the agent. */
122
- export function benchmarkPrompt(item: BenchmarkItem, chunks: number): string {
123
- return `Answer the question using the complete source document in corpus/part-00000.txt through corpus/part-${String(chunks - 1).padStart(5, "0")}.txt. ` +
124
- "The numbered files concatenate in order to the original document; none of it was removed. Use read and grep to examine the evidence.\n\n" +
125
- `Question: ${item.question}\nChoices:\n(A) ${item.choice_A}\n(B) ${item.choice_B}\n(C) ${item.choice_C}\n(D) ${item.choice_D}\n\n` +
126
- 'Format your final response as follows: "The correct answer is (insert answer here)".';
127
- }
128
-
129
- export interface BenchmarkPrice { input: number; output: number; cacheRead: number; cacheWrite?: number }
130
- export interface BenchmarkUsage { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }
131
-
132
- /** A conservative allowance, not a tokenizer: wire bytes plus framing, at uncached peak rates. */
133
- export function requestReservation(wireBytes: number, maxOutput: number, price: BenchmarkPrice): number {
134
- if (!Number.isSafeInteger(wireBytes) || wireBytes < 0 || !Number.isSafeInteger(maxOutput) || maxOutput < 1) throw new RangeError("invalid request size");
135
- return ((wireBytes + 4096) * price.input + maxOutput * price.output) / 1_000_000;
136
- }
137
-
138
- export function measuredCharge(usage: BenchmarkUsage, price: BenchmarkPrice): number | null {
139
- const counts = [usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens];
140
- if (counts.some((value) => !Number.isSafeInteger(value) || value < 0) ||
141
- usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens === 0 || usage.output_tokens === 0) return null;
142
- return (usage.input_tokens * price.input + usage.cache_creation_input_tokens * (price.cacheWrite ?? price.input) +
143
- usage.cache_read_input_tokens * price.cacheRead + usage.output_tokens * price.output) / 1_000_000;
144
- }
145
-
146
- export function canReserve(charged: number, reserve: number, budget: number): boolean {
147
- if (![charged, reserve, budget].every(Number.isFinite) || charged < 0 || reserve <= 0 || budget <= 0) return false;
148
- return charged + reserve <= budget;
149
- }
150
-
151
- /** Unknown outcomes keep their reservation; an unexpectedly high bill is never clipped. */
152
- export function settledCharge(reserve: number, measured: number | null): number {
153
- return measured === null ? reserve : measured;
154
- }
155
-
156
- export function wilsonInterval(correct: number, total: number): [number, number] | null {
157
- if (!Number.isSafeInteger(total) || !Number.isSafeInteger(correct) || correct < 0 || total < correct) throw new RangeError("invalid score");
158
- if (total === 0) return null;
159
- const z = 1.959963984540054;
160
- const p = correct / total;
161
- const d = 1 + z * z / total;
162
- const center = (p + z * z / (2 * total)) / d;
163
- const radius = z * Math.sqrt(p * (1 - p) / total + z * z / (4 * total * total)) / d;
164
- return [Math.max(0, center - radius), Math.min(1, center + radius)];
165
- }
166
-
167
- export interface BenchmarkOutcome {
168
- id: string;
169
- arm: BenchmarkArm;
170
- status: "answered" | "error" | "budget" | "stopped";
171
- correct: boolean;
172
- }
173
-
174
- export function summarizeBenchmark(plan: readonly BenchmarkJob[], outcomes: readonly BenchmarkOutcome[]) {
175
- const planned = new Set(plan.map((job) => `${job.id}/${job.arm}`));
176
- const seen = new Set<string>();
177
- for (const outcome of outcomes) {
178
- const key = `${outcome.id}/${outcome.arm}`;
179
- if (!planned.has(key) || seen.has(key)) throw new Error("unknown or duplicate benchmark outcome");
180
- if (outcome.correct && outcome.status !== "answered") throw new Error("unfinished result cannot be correct");
181
- seen.add(key);
182
- }
183
- return BENCHMARK_ARMS.map((arm) => {
184
- const rows = outcomes.filter((row) => row.arm === arm);
185
- const total = plan.filter((job) => job.arm === arm).length;
186
- const correct = rows.filter((row) => row.correct).length;
187
- const attempted = rows.length;
188
- const answered = rows.filter((row) => row.status === "answered").length;
189
- return { arm, planned: total, attempted, answered, correct, missing: total - attempted,
190
- accuracy: attempted > 0 ? correct / attempted : null,
191
- interval95: wilsonInterval(correct, attempted),
192
- plannedSolvedFraction: total > 0 ? correct / total : null,
193
- complete: attempted === total };
194
- });
195
- }
@@ -1,74 +0,0 @@
1
- /** R12c: missing final rows cannot erase attempted work or measured charges. */
2
- import type { EvalJob, EvalResult } from "./experiment.ts";
3
- import { recoveryCase } from "./recovery-corpus.ts";
4
- import { TOOL_MODE_FACTORS, isToolModeTask, isToolModeMetrics } from "./tool-mode-protocol.ts";
5
-
6
- export interface BillingRecord {
7
- kind: "reserved" | "settled"; request: number; inputSha256: string;
8
- reservation: number; charge: number; measured: number | null; ledgerCharged: number;
9
- }
10
- export function requestAccounting(records: readonly BillingRecord[], before: number) {
11
- const nonnegative = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
12
- if (!nonnegative(before)) throw Error("invalid trial ledger start");
13
- let charged = before, pending: BillingRecord | undefined, requests = 0, known = 0, unknown = 0;
14
- for (const row of records) {
15
- if (![row.reservation, row.charge, row.ledgerCharged].every(nonnegative) || row.measured !== null && !nonnegative(row.measured) ||
16
- !Number.isSafeInteger(row.request) || row.request < 1 || !/^[a-f0-9]{64}$/.test(row.inputSha256)) throw Error("invalid billing record");
17
- if (row.kind === "reserved") {
18
- if (pending || row.request !== ++requests || row.measured !== null || row.charge !== row.reservation) throw Error("invalid reservation order");
19
- pending = row; charged += row.reservation;
20
- } else if (row.kind === "settled") {
21
- if (!pending || row.request !== pending.request || row.inputSha256 !== pending.inputSha256 || row.reservation !== pending.reservation ||
22
- row.charge !== (row.measured ?? row.reservation)) throw Error("invalid settlement identity");
23
- charged += row.charge - pending.reservation;
24
- if (row.measured === null) unknown += row.reservation; else known += row.measured;
25
- pending = undefined;
26
- } else throw Error("invalid billing record kind");
27
- if (row.ledgerCharged !== charged) throw Error("billing ledger sequence changed");
28
- }
29
- return { requests, charged, knownCostSubtotal: known, unsettledRequests: pending ? 1 : 0,
30
- unsettledReservation: pending?.reservation ?? 0, unknownSettlementReserve: unknown,
31
- fullyMeasured: !pending && records.filter(row => row.kind === "settled").every(row => row.measured !== null) };
32
- }
33
- export type RequestAccounting = ReturnType<typeof requestAccounting>;
34
- export type TrialCoverage = { id: string; accounting: RequestAccounting; tail: "complete" | "truncated";
35
- result?: EvalResult; interruption: "deadline" | "caller" | null; partialMetrics?: EvalResult["recovery"] };
36
-
37
- export function summarizeRecoveryCoverage(jobs: readonly EvalJob[], trials: readonly TrialCoverage[], mechanical: boolean) {
38
- const ids = new Set(jobs.map(job => job.id)), byId = new Map(trials.map(trial => [trial.id, trial]));
39
- if (ids.size !== jobs.length || byId.size !== trials.length || trials.some(trial => !ids.has(trial.id) || trial.result && trial.result.id !== trial.id)) throw Error("invalid recovery coverage identities");
40
- const group = (selected: readonly EvalJob[]) => {
41
- const rows = selected.flatMap(job => byId.has(job.id) ? [byId.get(job.id)!] : []), finalized = rows.filter(row => row.result);
42
- const passed = finalized.filter(row => row.result!.scenarioPassed).length;
43
- const unknown = rows.filter(row => !row.result || row.tail !== "complete" || !row.accounting.fullyMeasured).length;
44
- const known = rows.reduce((n, row) => n + row.accounting.knownCostSubtotal, 0);
45
- const cost = !mechanical && rows.length === selected.length && unknown === 0 ? known : null;
46
- const metrics = rows.flatMap(row => row.result?.recovery ?? row.partialMetrics ?? []);
47
- const modes = finalized.flatMap(row => row.result?.toolMode ?? []);
48
- if (modes.some(mode => !isToolModeMetrics(mode))) throw Error("invalid tool mode measurements");
49
- const toolMode = selected.some(job => isToolModeTask(job.task)) ? { observed: modes.length, missing: selected.length - modes.length,
50
- queryCalls: modes.reduce((n, mode) => n + mode.queryCalls, 0),
51
- primitiveCalls: modes.reduce((n, mode) => n + mode.primitiveCalls, 0), sourcesComplete: modes.filter(mode => mode.sourceComplete).length,
52
- meanElapsedMs: modes.length === selected.length && modes.length > 0 ? modes.reduce((n, mode) => n + mode.elapsedMs, 0) / modes.length : null } : undefined;
53
- return { ...(toolMode ? { toolMode } : {}), planned: selected.length, attempted: rows.length, finalized: finalized.length, incomplete: rows.length - finalized.length,
54
- unattempted: selected.length - rows.length, failed: finalized.length - passed, passed, plannedPassFraction: selected.length ? passed / selected.length : null,
55
- cancelled: rows.filter(row => row.interruption === "caller").length, timedOut: rows.filter(row => row.interruption === "deadline").length,
56
- requests: rows.reduce((n, row) => n + row.accounting.requests, 0), truncatedRequestLogs: rows.filter(row => row.tail === "truncated").length,
57
- knownCostSubtotal: mechanical ? null : known, costUsd: cost,
58
- unsettledRequests: rows.reduce((n, row) => n + row.accounting.unsettledRequests, 0),
59
- unsettledReservationUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unsettledReservation, 0),
60
- unknownSettlementReserveUsd: mechanical ? null : rows.reduce((n, row) => n + row.accounting.unknownSettlementReserve, 0),
61
- unknownCostTrials: mechanical ? rows.length : unknown, costPerScenarioSuccess: cost !== null && passed > 0 ? cost / passed : null,
62
- unverifiedEndings: finalized.filter(row => row.result!.unverifiedEnding).length,
63
- recovery: { observed: metrics.length, missing: selected.length - metrics.length,
64
- firstToolSuccess: metrics.filter(row => row.firstToolSuccess === true).length,
65
- firstToolUnknown: metrics.filter(row => row.firstToolSuccess === null).length,
66
- firstPassTaskSuccess: metrics.filter(row => row.firstPassTaskSuccess === true).length,
67
- ...Object.fromEntries((["toolCalls", "outerCalls", "nestedCalls", "unresolvedCalls", "toolFailures", "classifiedFailures", "unclassifiedFailures", "recoveryCalls", "unchangedFailureRetries", "originalEvidenceRetrievals", "unsupportedClaims"] as const)
68
- .map(key => [key, metrics.reduce((n, row) => n + row[key], 0)])) } };
69
- };
70
- const toolModes = jobs.some(job => isToolModeTask(job.task));
71
- return { ...group(jobs), arms: (toolModes ? TOOL_MODE_FACTORS : ["profile", "workingSet", "strictVerification"]).flatMap(factor => ["control", "treatment"].map(arm =>
72
- ({ factor, arm, ...group(jobs.filter(job => job.factor === factor && job.arm === arm)) }))),
73
- partitions: toolModes ? [] : ["development", "holdout"].map(partition => ({ partition, ...group(jobs.filter(job => recoveryCase(job.task)?.partition === partition)) })) };
74
- }
@@ -1,173 +0,0 @@
1
- /** Experimental projections never mutate the durable event stream (D8, L). */
2
- import type { Message } from "./project.ts";
3
- import { isTrialLifecycle, type TrialLifecycle } from "./trial-lifecycle.ts";
4
- import { RECOVERY_TASKS, recoveryCase, type RecoveryTask } from "./recovery-corpus.ts";
5
- import { isRecoveryMetrics, type RecoveryMetrics } from "./recovery-evaluation.ts";
6
- import { TOOL_MODE_FACTORS, TOOL_MODE_TASKS, isToolModeTask, isToolModeMetrics, type ToolModeTask, type ToolModeMetrics } from "./tool-mode-protocol.ts";
7
-
8
- export type ContextArm = "node" | "replay" | "truncate" | "mask";
9
-
10
- export function experimentalContext(messages: readonly Message[], arm: ContextArm, keepRounds: number): Message[] {
11
- if (!Number.isSafeInteger(keepRounds) || keepRounds < 1) throw new RangeError("keepRounds must be positive");
12
- if (arm === "node" || arm === "replay") return [...messages];
13
- const groups: Message[][] = [[]];
14
- for (const message of messages) {
15
- if (message.role === "assistant") groups.push([]);
16
- groups.at(-1)!.push(message);
17
- }
18
- const recent = Math.max(1, groups.length - keepRounds);
19
- if (arm === "truncate") return [...groups[0]!, ...groups.slice(recent).flat()];
20
- return groups.flatMap((group, i) => i >= recent ? group : group.map((message) => ({
21
- ...message,
22
- content: message.content.map((block) => block.type === "tool_result"
23
- ? { ...block, content: "[observation masked by experiment; original remains in log]" } : block),
24
- })));
25
- }
26
-
27
- /** Fisher-Yates with a fixed seed; scheduling and intervals are reproducible. */
28
- export function seededRandom(seed: number): () => number {
29
- let state = seed >>> 0;
30
- return () => {
31
- state = (Math.imul(1664525, state) + 1013904223) >>> 0;
32
- return state / 4294967296;
33
- };
34
- }
35
-
36
- export function shuffled<T>(items: readonly T[], seed: number): T[] {
37
- const random = seededRandom(seed);
38
- const out = [...items];
39
- for (let i = out.length - 1; i > 0; i--) {
40
- const j = Math.floor(random() * (i + 1));
41
- [out[i], out[j]] = [out[j]!, out[i]!];
42
- }
43
- return out;
44
- }
45
-
46
- /** O.16: each contrast changes exactly one policy, with paired task repeats. */
47
- export const EVAL_FACTORS = ["profile", "workingSet", "strictVerification"] as const;
48
- export const EVAL_TASKS = ["short", "boundary-recall", "false-completion", "human-boundary"] as const;
49
- const supportedFactors = [...EVAL_FACTORS, ...TOOL_MODE_FACTORS] as const;
50
- export type EvalFactor = typeof supportedFactors[number];
51
- export type EvalTask = typeof EVAL_TASKS[number] | RecoveryTask | ToolModeTask;
52
- const supportedTasks:readonly EvalTask[]=[...EVAL_TASKS,...RECOVERY_TASKS,...TOOL_MODE_TASKS];
53
- export type EvalArm = "control" | "treatment";
54
- export type EvalPolicy = { profile: "full" | "coding"; workingSet: boolean; strictVerification: boolean; querySyntax?: "json" | "sql" };
55
- export type EvalJob = { id: string; factor: EvalFactor; task: EvalTask; repeat: number; arm: EvalArm; policy: EvalPolicy };
56
- export function evalPolicy(factor: EvalFactor, arm: EvalArm): EvalPolicy {
57
- const policy: EvalPolicy = { profile: "coding", workingSet: true, strictVerification: true };
58
- if (factor === "querySyntax") return { ...policy, querySyntax: arm === "treatment" ? "sql" : "json" };
59
- if (arm === "control") {
60
- if (factor === "profile") policy.profile = "full";
61
- else policy[factor] = false;
62
- }
63
- return policy;
64
- }
65
- export function singleFactorSchedule(seed: number, repeats = 1, tasks: readonly EvalTask[] = EVAL_TASKS, factors: readonly EvalFactor[] = EVAL_FACTORS): EvalJob[] {
66
- if (!Number.isSafeInteger(seed) || !Number.isSafeInteger(repeats) || repeats < 1 || repeats > 20 || !tasks.length ||
67
- new Set(tasks).size !== tasks.length || tasks.some(task => !supportedTasks.includes(task)) || !factors.length || new Set(factors).size !== factors.length ||
68
- factors.some(factor => !supportedFactors.includes(factor) || tasks.some(task => isToolModeTask(task) !== (TOOL_MODE_FACTORS as readonly string[]).includes(factor)))) throw new RangeError("invalid eval schedule");
69
- const pairs = shuffled(factors.flatMap(factor => [...tasks].sort().flatMap(task =>
70
- Array.from({ length: repeats }, (_, repeat) => ({ factor, task, repeat })))), seed);
71
- // Alternate first position before shuffling: adjacent LCG seeds are correlated.
72
- const positions = new Map<EvalFactor, number>();
73
- const order = shuffled(["control", "treatment"] as const, seed + 1);
74
- return pairs.flatMap(pair => {
75
- const offset = positions.get(pair.factor) ?? 0; positions.set(pair.factor, offset + 1);
76
- return order.map((_, i) => {
77
- const arm = order[(offset + i) % 2]!;
78
- return { ...pair, arm, id: `${pair.factor}/${pair.task}/${pair.repeat}/${arm}`, policy: evalPolicy(pair.factor, arm) };
79
- });
80
- });
81
- }
82
- export function validateEvalJobs(jobs: readonly EvalJob[]): void {
83
- const seen = new Set<string>();
84
- for (const job of jobs) {
85
- if (!supportedFactors.includes(job.factor) || !supportedTasks.includes(job.task) || !["control", "treatment"].includes(job.arm) ||
86
- isToolModeTask(job.task) !== (TOOL_MODE_FACTORS as readonly string[]).includes(job.factor) ||
87
- !Number.isSafeInteger(job.repeat) || job.repeat < 0 || job.id !== `${job.factor}/${job.task}/${job.repeat}/${job.arm}` || seen.has(job.id) ||
88
- JSON.stringify(job.policy) !== JSON.stringify(evalPolicy(job.factor, job.arm))) throw new Error("invalid or confounded eval job");
89
- seen.add(job.id);
90
- }
91
- for (const job of jobs) if (!seen.has(`${job.factor}/${job.task}/${job.repeat}/${job.arm === "control" ? "treatment" : "control"}`)) throw new Error("unpaired eval job");
92
- }
93
- export interface EvalResult {
94
- toolMode?: ToolModeMetrics;
95
- recovery?: RecoveryMetrics;
96
- /** v2 separates an unmet final state from a machine-readable unsupported claim. */
97
- unverifiedEnding?: boolean;
98
- /** Absent in the frozen O.16 archives; absence is unknown, not an uninterrupted run. */
99
- lifecycle?: TrialLifecycle;
100
- id: string;
101
- status: "finished" | "error" | "budget" | "stopped";
102
- scenarioPassed: boolean;
103
- artifactCorrect: boolean;
104
- verified: boolean;
105
- awaitingHuman: boolean;
106
- falseCompletion: boolean;
107
- requests: number;
108
- toolFailures: number;
109
- repeatedReads: number;
110
- promptTokens: number | null;
111
- cacheTokens: number | null;
112
- outputTokens: number | null;
113
- costUsd: number | null;
114
- reservedUnknownUsd: number;
115
- }
116
- export function summarizeSingleFactor(jobs: readonly EvalJob[], results: readonly EvalResult[]) {
117
- validateEvalJobs(jobs);
118
- const byId = new Map<string, EvalResult>(), planned = new Set(jobs.map(job => job.id));
119
- for (const row of results) {
120
- if (row.toolMode !== undefined && (!isToolModeMetrics(row.toolMode) || row.scenarioPassed && !row.toolMode.sourceComplete)) throw Error("invalid tool mode metrics");
121
- if (row.unverifiedEnding !== undefined && typeof row.unverifiedEnding !== "boolean") throw Error("invalid ending metric");
122
- if(row.recovery!==undefined&&(!isRecoveryMetrics(row.recovery)||row.recovery.finalTaskSuccess!==row.scenarioPassed||row.recovery.toolFailures!==row.toolFailures||row.falseCompletion!==(row.recovery.unsupportedClaims>0)))throw Error("invalid recovery metrics");
123
- if (row.lifecycle !== undefined && (!isTrialLifecycle(row.lifecycle) || row.lifecycle.backendRequests > row.requests ||
124
- row.lifecycle.interruption !== null && row.status === "finished" || row.lifecycle.cleanup === "failed" && row.status !== "error"))
125
- throw new Error("invalid eval lifecycle result");
126
- if (!planned.has(row.id) || byId.has(row.id) || !["finished", "error", "budget", "stopped"].includes(row.status) ||
127
- [row.scenarioPassed, row.artifactCorrect, row.verified, row.awaitingHuman, row.falseCompletion].some(value => typeof value !== "boolean") ||
128
- row.scenarioPassed && (row.status !== "finished" || !row.verified || row.falseCompletion && row.recovery === undefined) ||
129
- [row.requests, row.toolFailures, row.repeatedReads].some(n => !Number.isSafeInteger(n) || n < 0) ||
130
- [row.promptTokens, row.cacheTokens, row.outputTokens].some(n => n !== null && (!Number.isSafeInteger(n) || n < 0)) ||
131
- [row.costUsd, row.reservedUnknownUsd].some(n => n !== null && (!Number.isFinite(n) || n < 0)) ||
132
- row.cacheTokens !== null && row.promptTokens !== null && row.cacheTokens > row.promptTokens) throw new Error("invalid or duplicate eval result");
133
- byId.set(row.id, row);
134
- }
135
- const factors = supportedFactors.filter(factor => jobs.some(job => job.factor === factor));
136
- const arms = factors.flatMap(factor => (["control", "treatment"] as const).map(arm => {
137
- const selected = jobs.filter(job => job.factor === factor && job.arm === arm), rows = selected.flatMap(job => byId.has(job.id) ? [byId.get(job.id)!] : []);
138
- const complete = rows.length === selected.length, scenarioPassed = rows.filter(row => row.scenarioPassed).length;
139
- const knownCostSubtotal = rows.reduce((sum, row) => sum + (row.costUsd ?? 0), 0);
140
- // Missing jobs and unknown charges cannot become a zero-priced success.
141
- const costUsd = complete && rows.length > 0 && rows.every(row => row.costUsd !== null) ? knownCostSubtotal : null;
142
- const total = (field: "promptTokens" | "cacheTokens" | "outputTokens") => complete && rows.length > 0 && rows.every(row => row[field] !== null)
143
- ? rows.reduce((sum, row) => sum + row[field]!, 0) : null;
144
- const recovery=selected.some(job=>recoveryCase(job.task)||isToolModeTask(job.task))?{observed:rows.filter(row=>row.recovery).length,
145
- missing:selected.length-rows.filter(row=>row.recovery).length,
146
- firstToolSuccess:rows.filter(row=>row.recovery?.firstToolSuccess===true).length,
147
- firstPassTaskSuccess:rows.filter(row=>row.recovery?.firstPassTaskSuccess===true).length,
148
- ...Object.fromEntries((["toolCalls","outerCalls","nestedCalls","unresolvedCalls","recoveryCalls","unchangedFailureRetries","originalEvidenceRetrievals","unsupportedClaims"] as const)
149
- .map(key=>[key,rows.reduce((n,row)=>n+(row.recovery?.[key]??0),0)]))}:undefined;
150
- return { factor, arm, ...(recovery?{recovery}:{}), planned: selected.length, attempted: rows.length, missing: selected.length - rows.length,
151
- failed: rows.filter(row => !row.scenarioPassed).length, scenarioPassed, verified: rows.filter(row => row.verified).length,
152
- awaitingHuman: rows.filter(row => row.awaitingHuman).length, falseCompletions: rows.filter(row => row.falseCompletion).length,
153
- plannedPassFraction: selected.length ? scenarioPassed / selected.length : null, complete,
154
- requests: rows.reduce((n, row) => n + row.requests, 0), toolFailures: rows.reduce((n, row) => n + row.toolFailures, 0),
155
- repeatedReads: rows.reduce((n, row) => n + row.repeatedReads, 0), promptTokens: total("promptTokens"), cacheTokens: total("cacheTokens"), outputTokens: total("outputTokens"),
156
- knownCostSubtotal, costUsd, unknownCostJobs: rows.filter(row => row.costUsd === null).length,
157
- reservedUnknownUsd: rows.reduce((n, row) => n + row.reservedUnknownUsd, 0),
158
- costPerScenarioSuccess: costUsd !== null && scenarioPassed > 0 ? costUsd / scenarioPassed : null };
159
- }));
160
- const comparisons = factors.map(factor => {
161
- const pairs = jobs.filter(job => job.factor === factor && job.arm === "control");
162
- const clusters = [...new Set(pairs.map(job => job.task))].sort().map(task => {
163
- const matched = pairs.filter(job => job.task === task).flatMap(job => {
164
- const control = byId.get(job.id), treatment = byId.get(`${factor}/${task}/${job.repeat}/treatment`);
165
- return control && treatment ? [{ delta: Number(treatment.scenarioPassed) - Number(control.scenarioPassed) }] : [];
166
- });
167
- return { task, matchedPairs: matched.length, passDelta: matched.length ? matched.reduce((n, pair) => n + pair.delta, 0) / matched.length : null };
168
- });
169
- const matchedPairs = clusters.reduce((n, row) => n + row.matchedPairs, 0);
170
- return { factor, matchedPairs, missingPairs: pairs.length - matchedPairs, clusters };
171
- });
172
- return { arms, comparisons };
173
- }