@visual-z/xeno 0.7.1 → 0.8.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 (112) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +39 -12
  3. package/docs/foundations.md +1 -1
  4. package/docs/install.md +18 -5
  5. package/package.json +3 -1
  6. package/src/apps/cli.ts +4 -3
  7. package/src/apps/commands.ts +9 -1
  8. package/src/apps/goal-flow.ts +70 -0
  9. package/src/apps/render.ts +0 -2
  10. package/src/apps/session-hub.ts +6 -5
  11. package/src/apps/session-lifecycle.ts +64 -0
  12. package/src/apps/shell.ts +90 -35
  13. package/src/apps/startup.ts +33 -32
  14. package/src/apps/ui/app.ts +47 -10
  15. package/src/apps/ui/control-model.ts +18 -1
  16. package/src/apps/ui/surface.ts +2 -0
  17. package/src/apps/update-check.ts +34 -4
  18. package/src/core/acceptance.ts +41 -9
  19. package/src/core/blind-checks.ts +72 -0
  20. package/src/core/branch-receipt.ts +3 -1
  21. package/src/core/checkpoints.ts +21 -0
  22. package/src/core/churn.ts +2 -2
  23. package/src/core/completion-review.ts +18 -11
  24. package/src/core/continuation.ts +11 -10
  25. package/src/core/control-view.ts +5 -8
  26. package/src/core/deadline.ts +2 -1
  27. package/src/core/derive.ts +0 -7
  28. package/src/core/environment.ts +2 -2
  29. package/src/core/event-validation.ts +19 -6
  30. package/src/core/events.ts +41 -32
  31. package/src/core/evidence-ladder.ts +62 -0
  32. package/src/core/evidence-links.ts +0 -10
  33. package/src/core/evidence.ts +1 -2
  34. package/src/core/export.ts +6 -11
  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 +2 -1
  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/core/update-check.ts +40 -0
  55. package/src/runtime/blind-check.ts +36 -0
  56. package/src/runtime/counter-check.ts +74 -0
  57. package/src/runtime/exec-sessions.ts +11 -0
  58. package/src/runtime/foundation.ts +46 -8
  59. package/src/runtime/loop.ts +239 -430
  60. package/src/runtime/oauth.ts +1 -3
  61. package/src/runtime/provider-request.ts +2 -1
  62. package/src/runtime/runtime-target.ts +2 -2
  63. package/src/runtime/self-update.ts +74 -0
  64. package/src/runtime/session-control.ts +4 -4
  65. package/src/runtime/tools.ts +44 -42
  66. package/src/runtime/version.ts +12 -0
  67. package/src/runtime/workspace-lifecycle.ts +31 -0
  68. package/src/store/blind-checks.ts +32 -0
  69. package/src/store/checkpoint-history.ts +1 -12
  70. package/src/store/file-index.ts +88 -7
  71. package/src/store/log.ts +25 -27
  72. package/src/store/recovery.ts +1 -3
  73. package/src/store/replay.ts +2 -2
  74. package/src/store/restore.ts +10 -202
  75. package/src/store/search.ts +7 -5
  76. package/src/store/session-archive.ts +0 -1
  77. package/src/store/sessions.ts +31 -33
  78. package/src/store/workspace-clone.ts +42 -0
  79. package/src/store/workspace.ts +10 -157
  80. package/src/tui/ui/screen.ts +5 -4
  81. package/src/tui/ui/status.ts +6 -2
  82. package/src/core/benchmark.ts +0 -195
  83. package/src/core/evaluation-coverage.ts +0 -74
  84. package/src/core/experiment.ts +0 -173
  85. package/src/core/harness-comparison.ts +0 -90
  86. package/src/core/harness-diagnostics.ts +0 -206
  87. package/src/core/native-eval.ts +0 -25
  88. package/src/core/policy-evaluation.ts +0 -55
  89. package/src/core/query-eval-contract.ts +0 -14
  90. package/src/core/recall-analysis.ts +0 -90
  91. package/src/core/recall-trial.ts +0 -153
  92. package/src/core/recovery-corpus.ts +0 -17
  93. package/src/core/recovery-evaluation.ts +0 -60
  94. package/src/core/tool-mode-protocol.ts +0 -25
  95. package/src/core/trial-lifecycle.ts +0 -14
  96. package/src/core/window-trial.ts +0 -65
  97. package/src/index.ts +0 -10
  98. package/src/runtime/benchmark.ts +0 -187
  99. package/src/runtime/harness-comparison.ts +0 -376
  100. package/src/runtime/harness-eval.ts +0 -462
  101. package/src/runtime/native-eval-fixtures.ts +0 -33
  102. package/src/runtime/native-eval.ts +0 -146
  103. package/src/runtime/query-eval-verifier.ts +0 -28
  104. package/src/runtime/recall-trial.ts +0 -226
  105. package/src/runtime/recovery-coverage.ts +0 -71
  106. package/src/runtime/recovery-evidence.ts +0 -76
  107. package/src/runtime/recovery-fixtures.ts +0 -116
  108. package/src/runtime/tool-mode-eval.ts +0 -146
  109. package/src/runtime/trial-control.ts +0 -45
  110. package/src/runtime/window-trial.ts +0 -141
  111. package/src/store/git-branch.ts +0 -35
  112. package/src/tui/ui/testing.ts +0 -5
@@ -1,32 +1,18 @@
1
1
  /**
2
- * Workspace scanning, reconciliation and restore.
2
+ * A bounded content scan of a workspace: relative path -> digest, and one
3
+ * version digest over the whole set.
3
4
  *
4
- * The tree can move a cursor between nodes; this is what lets the files move
5
- * with it. Two properties shape everything here:
6
- *
7
- * TURN GRANULARITY. The workspace is scanned once per turn, not per tool
8
- * call. Per-call is too fine to be meaningful -- the documented criticism
9
- * of character-level versioning is that a single keystroke is not a change
10
- * -- and a turn is the same unit as "a message and the edit it produced".
11
- *
12
- * TWO INDEPENDENT SOURCES. A tool's effect says what the tool CLAIMED to
13
- * do; a scan says what ACTUALLY changed. bash can write anywhere and its
14
- * effect records only an exit code, so without the scan an edit outside
15
- * every declared path would be invisible -- and a restore would silently
16
- * fail to undo it. Comparing the two makes that detectable, which is the
17
- * same rule the handoff applies to the model: claims are checked, not
18
- * believed.
19
- *
20
- * A restore moves the workspace and nothing else. Migrations, sent requests
21
- * and started processes do not come back, and applyRestore says so rather
22
- * than implying it undid the world.
5
+ * Session file history no longer comes from per-turn scans. P.3 replaced them
6
+ * with journaled checkpoints and source fingerprints (workspace-observation.ts,
7
+ * WorkspaceLifecycle), and S.3 removed the compatibility mode that still
8
+ * scanned. What remains is the scanner itself and the default ignore list the
9
+ * search and file index share.
23
10
  */
24
11
 
25
- import { readdir, rm, stat, lstat } from "node:fs/promises";
12
+ import { readdir, stat } from "node:fs/promises";
26
13
  import type { Dirent } from "node:fs";
27
- import { join, dirname, relative, sep, isAbsolute } from "node:path";
28
- import type { Effect } from "../core/events.ts";
29
- import { blobAddress, digestFile, verifyBlob, restoreBlob } from "./blobs.ts";
14
+ import { join, relative, sep } from "node:path";
15
+ import { blobAddress, digestFile } from "./blobs.ts";
30
16
 
31
17
  export interface ScanLimits {
32
18
  maxFiles: number;
@@ -68,28 +54,6 @@ export type ScanResult =
68
54
  | { ok: true; files: FileSet }
69
55
  | { ok: false; error: string };
70
56
 
71
- export interface ScanDelta {
72
- added: FileSet;
73
- changed: FileSet;
74
- removed: string[];
75
- }
76
-
77
- export interface Reconciliation {
78
- /** Changed on disk with no tool claiming it -- usually a shell command. */
79
- undeclared: string[];
80
- /** A tool claimed an edit the disk does not show. */
81
- declaredButUnchanged: string[];
82
- }
83
-
84
- export interface RestoreResult {
85
- restored: string[];
86
- removed: string[];
87
- /** Target files whose content is no longer in the blob store. */
88
- unrecoverable: string[];
89
- /** What this restore did not and could not touch. */
90
- note: string;
91
- }
92
-
93
57
  function isIgnored(rel: string, ignore: readonly string[]): boolean {
94
58
  const parts = rel.split(sep);
95
59
  return ignore.some((i) => parts.includes(i));
@@ -149,114 +113,3 @@ export async function scanWorkspace(
149
113
  return error ? { ok: false, error } : { ok: true, files };
150
114
  } catch (error) { return { ok: false, error: `incomplete workspace scan: ${String(error)}` }; }
151
115
  }
152
-
153
- export function diffScans(before: FileSet, after: FileSet): ScanDelta {
154
- const added: FileSet = {};
155
- const changed: FileSet = {};
156
- const removed: string[] = [];
157
-
158
- for (const [path, sha] of Object.entries(after)) {
159
- const was = before[path];
160
- if (was === undefined) added[path] = sha;
161
- else if (was !== sha) changed[path] = sha;
162
- }
163
- for (const path of Object.keys(before)) {
164
- if (!(path in after)) removed.push(path);
165
- }
166
- return { added, changed, removed: removed.sort() };
167
- }
168
-
169
- export function reconcile(effects: readonly Effect[], observed: ScanDelta): Reconciliation {
170
- // Only edits claim to change the workspace. A read claims nothing, and a
171
- // command claims only its exit code -- which is exactly why the scan is
172
- // needed to see what a command did.
173
- const declared = new Set(
174
- effects.filter((e) => e.kind === "edit").map((e) => e.path),
175
- );
176
- const touched = [
177
- ...Object.keys(observed.added),
178
- ...Object.keys(observed.changed),
179
- ...observed.removed,
180
- ];
181
-
182
- return {
183
- undeclared: touched.filter((p) => !declared.has(p)).sort(),
184
- declaredButUnchanged: [...declared].filter((p) => !touched.includes(p)).sort(),
185
- };
186
- }
187
-
188
- /**
189
- * Replays deltas up to and including `toNode` to get the file set at that
190
- * point. Pure, so a restore can be planned and inspected before anything is
191
- * written.
192
- */
193
- export function planRestore(
194
- deltas: readonly (ScanDelta & { node: number })[],
195
- toNode: number,
196
- ): FileSet {
197
- const files: FileSet = {};
198
- for (const d of deltas) {
199
- if (d.node > toNode) break;
200
- for (const [path, sha] of Object.entries(d.added)) files[path] = sha;
201
- for (const [path, sha] of Object.entries(d.changed)) files[path] = sha;
202
- for (const path of d.removed) delete files[path];
203
- }
204
- return files;
205
- }
206
-
207
- export async function applyRestore(
208
- root: string,
209
- store: string,
210
- target: FileSet,
211
- ignore: readonly string[],
212
- ): Promise<RestoreResult> {
213
- const restored: string[] = [];
214
- const unrecoverable: string[] = [];
215
-
216
- // Validate the complete plan before its first write or deletion. Missing
217
- // objects and a failed scan cannot be interpreted as an empty target (H).
218
- const current = await scanWorkspace(root, ignore);
219
- if (!current.ok) throw new Error(current.error);
220
- for (const [path, sha] of Object.entries(target)) {
221
- if (isAbsolute(path) || path.split(/[\\/]/).some((p) => p === "..") || isIgnored(path, ignore)) {
222
- throw new Error(`unsafe restore path: ${path}`);
223
- }
224
- let parent = dirname(join(root, path));
225
- while (parent !== root && parent.startsWith(root + sep)) {
226
- try { if ((await lstat(parent)).isSymbolicLink()) throw new Error(`restore path crosses symlink: ${path}`); }
227
- catch (error) { if ((error as { code?: string }).code !== "ENOENT") throw error; }
228
- parent = dirname(parent);
229
- }
230
- if (!await verifyBlob(store, sha)) unrecoverable.push(path);
231
- }
232
- if (unrecoverable.length > 0) return {
233
- restored: [], removed: [], unrecoverable: unrecoverable.sort(),
234
- note: "Restore not applied: missing snapshot objects. Workspace and cursor remain unchanged.",
235
- };
236
-
237
- for (const [path, sha] of Object.entries(target)) {
238
- const abs = join(root, path);
239
- await restoreBlob(store, sha, abs);
240
- restored.push(path);
241
- }
242
-
243
- // Anything present but not in the target was created after this point.
244
- const removed: string[] = [];
245
- if (current.ok) {
246
- for (const path of Object.keys(current.files)) {
247
- if (path in target) continue;
248
- await rm(join(root, path), { force: true });
249
- removed.push(path);
250
- }
251
- }
252
-
253
- return {
254
- restored: restored.sort(),
255
- removed: removed.sort(),
256
- unrecoverable: unrecoverable.sort(),
257
- note:
258
- "Restored the workspace only. Anything outside it is not undone: " +
259
- "database migrations, requests already sent, processes already " +
260
- "started, and files outside the workspace root all remain as they are.",
261
- };
262
- }
@@ -96,7 +96,7 @@ export class Screen {
96
96
  private sidebarWanted = true;
97
97
  private status: StatusModel | undefined;
98
98
  private dockState: DockState = { model: "", thinking: "", busy: false, activity: "" };
99
- private transientActivity: { readonly label: string; readonly startedAt: number; readonly timeoutMs: number } | undefined;
99
+ private transientActivity: { readonly label: string; readonly startedAt: number; readonly timeoutMs: number; readonly detail?: string } | undefined;
100
100
  private home: Greeting | undefined;
101
101
  private greetingPlayed = false;
102
102
  private greetingFrame = MASCOT_FRAMES;
@@ -349,8 +349,9 @@ export class Screen {
349
349
  this.paintEdges();
350
350
  }
351
351
 
352
- setTransientActivity(activity: { readonly label: string; readonly timeoutMs: number } | undefined): void {
353
- this.transientActivity = activity ? { ...activity, startedAt: Date.now() } : undefined;
352
+ /** `startedAt` keeps the elapsed clock running when a long activity only refreshes its detail line. */
353
+ setTransientActivity(activity: { readonly label: string; readonly timeoutMs: number; readonly startedAt?: number; readonly detail?: string } | undefined): void {
354
+ this.transientActivity = activity ? { ...activity, startedAt: activity.startedAt ?? Date.now() } : undefined;
354
355
  this.syncActivity();
355
356
  this.paintEdges();
356
357
  }
@@ -427,7 +428,7 @@ export class Screen {
427
428
  const activity = this.transientActivity!;
428
429
  const elapsed = Math.max(0, Math.floor((Date.now() - activity.startedAt) / 1000));
429
430
  const deadline = Math.max(1, Math.ceil(activity.timeoutMs / 1000));
430
- return `${activity.label} · ${elapsed}s / ${deadline}s max`;
431
+ return `${activity.label} · ${elapsed}s / ${deadline}s max${activity.detail ? ` · ${activity.detail}` : ""}`;
431
432
  }
432
433
 
433
434
  /** The spinner's current interval (tests). */
@@ -30,7 +30,9 @@ export interface StatusModel {
30
30
  readonly version: string;
31
31
  /** `/name`, else the first prompt; null for a session with neither. */
32
32
  readonly title: string | null;
33
- readonly goal?: { readonly text: string; readonly status: string; readonly execution: string; readonly observations: number; readonly retries: number };
33
+ readonly goal?: { readonly text: string; readonly status: string; readonly execution: string; readonly observations: number; readonly retries: number;
34
+ /** S.1: one glyph per rung (ran, tests the change, independent, human) and the reading of them, prepared by apps. */
35
+ readonly ladder?: { readonly glyphs: string; readonly summary: string } };
34
36
  readonly workspace: string;
35
37
  readonly workspaceShort: string;
36
38
  readonly branch: string | null;
@@ -203,7 +205,9 @@ function sidebarBlocks(model: StatusModel, w: number): Block[] {
203
205
  ] });
204
206
  const context = model.context;
205
207
  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" }],
208
+ [{ text: "Goal", role: "text", bold: true }, { text: ` · ${model.goal.status}`, role: model.goal.status === "verified" || model.goal.status === "checked" ? "success" : model.goal.status === "not_applicable" ? "muted" : "warning" }],
209
+ ...(model.goal.ladder ? [[{ text: model.goal.ladder.glyphs, role: "text" }] as Line,
210
+ ...wrap(model.goal.ladder.summary, w, 2).map(text => [{ text, role: "muted" }] as Line)] : []),
207
211
  ...wrap(firstLine(model.goal.text), w, 2).map(text => [{ text, role: "text" }] as Line),
208
212
  [{ text: model.goal.execution, role: "muted" }],
209
213
  [{ text: `${model.goal.observations} observations · ${model.goal.retries} retries`, role: "muted" }],
@@ -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
- }