@aigne/afs-llm-bench 1.12.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE.md +26 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
  4. package/dist/_virtual/rolldown_runtime.cjs +29 -0
  5. package/dist/bench.cjs +944 -0
  6. package/dist/bench.d.cts +89 -0
  7. package/dist/bench.d.cts.map +1 -0
  8. package/dist/bench.d.mts +89 -0
  9. package/dist/bench.d.mts.map +1 -0
  10. package/dist/bench.mjs +944 -0
  11. package/dist/bench.mjs.map +1 -0
  12. package/dist/errors.cjs +14 -0
  13. package/dist/errors.d.cts +13 -0
  14. package/dist/errors.d.cts.map +1 -0
  15. package/dist/errors.d.mts +13 -0
  16. package/dist/errors.d.mts.map +1 -0
  17. package/dist/errors.mjs +14 -0
  18. package/dist/errors.mjs.map +1 -0
  19. package/dist/index.cjs +38 -0
  20. package/dist/index.d.cts +10 -0
  21. package/dist/index.d.mts +10 -0
  22. package/dist/index.mjs +10 -0
  23. package/dist/judge.cjs +189 -0
  24. package/dist/judge.d.cts +67 -0
  25. package/dist/judge.d.cts.map +1 -0
  26. package/dist/judge.d.mts +67 -0
  27. package/dist/judge.d.mts.map +1 -0
  28. package/dist/judge.mjs +185 -0
  29. package/dist/judge.mjs.map +1 -0
  30. package/dist/outlier.cjs +73 -0
  31. package/dist/outlier.d.cts +30 -0
  32. package/dist/outlier.d.cts.map +1 -0
  33. package/dist/outlier.d.mts +30 -0
  34. package/dist/outlier.d.mts.map +1 -0
  35. package/dist/outlier.mjs +73 -0
  36. package/dist/outlier.mjs.map +1 -0
  37. package/dist/patcher.cjs +296 -0
  38. package/dist/patcher.d.cts +119 -0
  39. package/dist/patcher.d.cts.map +1 -0
  40. package/dist/patcher.d.mts +119 -0
  41. package/dist/patcher.d.mts.map +1 -0
  42. package/dist/patcher.mjs +291 -0
  43. package/dist/patcher.mjs.map +1 -0
  44. package/dist/prompts.cjs +123 -0
  45. package/dist/prompts.d.cts +30 -0
  46. package/dist/prompts.d.cts.map +1 -0
  47. package/dist/prompts.d.mts +30 -0
  48. package/dist/prompts.d.mts.map +1 -0
  49. package/dist/prompts.mjs +121 -0
  50. package/dist/prompts.mjs.map +1 -0
  51. package/dist/reporter.cjs +322 -0
  52. package/dist/reporter.d.cts +27 -0
  53. package/dist/reporter.d.cts.map +1 -0
  54. package/dist/reporter.d.mts +27 -0
  55. package/dist/reporter.d.mts.map +1 -0
  56. package/dist/reporter.mjs +322 -0
  57. package/dist/reporter.mjs.map +1 -0
  58. package/dist/runner.cjs +710 -0
  59. package/dist/runner.d.cts +345 -0
  60. package/dist/runner.d.cts.map +1 -0
  61. package/dist/runner.d.mts +345 -0
  62. package/dist/runner.d.mts.map +1 -0
  63. package/dist/runner.mjs +697 -0
  64. package/dist/runner.mjs.map +1 -0
  65. package/dist/scoring.cjs +47 -0
  66. package/dist/scoring.d.cts +28 -0
  67. package/dist/scoring.d.cts.map +1 -0
  68. package/dist/scoring.d.mts +28 -0
  69. package/dist/scoring.d.mts.map +1 -0
  70. package/dist/scoring.mjs +46 -0
  71. package/dist/scoring.mjs.map +1 -0
  72. package/manifest.json +39 -0
  73. package/package.json +62 -0
@@ -0,0 +1,710 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_errors = require('./errors.cjs');
3
+ const require_judge = require('./judge.cjs');
4
+ const require_scoring = require('./scoring.cjs');
5
+ const require_outlier = require('./outlier.cjs');
6
+ let node_fs = require("node:fs");
7
+ let node_crypto = require("node:crypto");
8
+ let node_path = require("node:path");
9
+
10
+ //#region src/runner.ts
11
+ /**
12
+ * Phase 1 — runOne + execRun matrix + results writer.
13
+ *
14
+ * Dispatches a single (model, prompt) sample through `/dev/agent/.actions/run`,
15
+ * scores the trace via `scoring.ts`, and writes per-model aggregates to a JSON
16
+ * file. Credentials are NOT held by bench — the caller forwards a model id and
17
+ * ai-device routing injects vault secrets at the LLM call site.
18
+ *
19
+ * See plan.md L294-378 (impl) + L745-752 (test spec).
20
+ */
21
+ const DEFAULT_SAMPLES = 3;
22
+ const MAX_MODELS = 5;
23
+ const MAX_PROMPTS_PER_RUN = 5;
24
+ const MAX_MODELS_FULL = 200;
25
+ const MAX_PROMPTS_FULL = 200;
26
+ const MAX_SAMPLES = 100;
27
+ const MAX_PARALLEL = 4;
28
+ const DEFAULT_TOTAL_TOKENS = 24e4;
29
+ const DEFAULT_WALL_TIME_MS = 5 * 6e4;
30
+ /**
31
+ * Hub used to expand short model ids (e.g. `"claude-haiku-4-5"`) into a full
32
+ * AFS path before dispatching to `/dev/agent/.actions/run`. agent-run's own
33
+ * `resolveAgentModelPath` defaults to AigneHub, which requires credits — we
34
+ * default to Vertex so OAuth-backed local installs work without billing.
35
+ *
36
+ * Override via `runFull` arg `hub` (or by passing the full
37
+ * `/dev/ai/hubs/<hub>/models/<id>` path as the model id).
38
+ */
39
+ const DEFAULT_HUB = "vertex";
40
+ /** Expand a short model id to its full hub path. Full paths pass through. */
41
+ function resolveModelPath(model, hub = DEFAULT_HUB) {
42
+ if (model.startsWith("/")) return model;
43
+ return `/dev/ai/hubs/${hub}/models/${model}`;
44
+ }
45
+ const RATE_LIMIT_MAX_RETRIES = 3;
46
+ const RATE_LIMIT_BASE_BACKOFF_MS = 2e3;
47
+ const MAX_RESULTS_SIZE_BYTES = 10 * 1024 * 1024;
48
+ const ALLOWED_JUDGE_DIMENSIONS = ["instruction_following"];
49
+ /**
50
+ * Hard cap on multi-judge fanout (Phase 0 of Run 4). Each judge is one extra
51
+ * `/dev/ai/.actions/chat` call per successful sample, so a 200×200 matrix
52
+ * with 100 samples explodes quickly. 10 is generous enough for ensemble
53
+ * experiments and small enough that runaway configs surface as validation
54
+ * errors rather than silently burning cost.
55
+ */
56
+ const MAX_JUDGES = 10;
57
+ const JUDGE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
58
+ const SECRET_VALUE_PATTERNS = [
59
+ /-----BEGIN [A-Z0-9 ]*?PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*?PRIVATE KEY-----/g,
60
+ /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi,
61
+ /\bsk-[A-Za-z0-9_-]*[0-9][A-Za-z0-9_-]{6,}/g,
62
+ /\bya29\.[A-Za-z0-9._-]+/g,
63
+ /\bxox[baprs]-[A-Za-z0-9-]+/gi,
64
+ /\bAKIA[0-9A-Z]{16}\b/g
65
+ ];
66
+ const SENSITIVE_PATTERNS = [
67
+ /\bapi[_-]?key\b/gi,
68
+ /\bauthorization\b/gi,
69
+ /\bbearer\b/gi,
70
+ /\bservice[_-]?account\b/gi,
71
+ /\bprivate[_-]?key\b/gi,
72
+ /\bAKIA[0-9A-Z]{0,16}\b/g
73
+ ];
74
+ /** Permissive read-only AFS sandbox so benchmark prompts can explore mounts. */
75
+ function defaultBenchTools() {
76
+ return [{
77
+ path: "/**",
78
+ ops: [
79
+ "list",
80
+ "read",
81
+ "explain",
82
+ "stat",
83
+ "exec"
84
+ ],
85
+ maxDepth: 10
86
+ }];
87
+ }
88
+ function defaultBenchSystem() {
89
+ return [
90
+ "You are an AFS power user evaluating mounts and actions.",
91
+ "Tools: afs_list, afs_read, afs_explain, afs_stat, afs_exec.",
92
+ "Prefer parallel tool calls when listing or explaining sibling paths.",
93
+ "Stop and summarize as soon as you have enough context to answer."
94
+ ].join("\n");
95
+ }
96
+ async function runOne(input) {
97
+ const taskBody = input.prompt.sections.Task ?? input.prompt.body;
98
+ const systemBody = input.prompt.sections["System prompt"] ?? defaultBenchSystem();
99
+ const exec = await input.deps.exec("/dev/agent/.actions/run", {
100
+ task: taskBody,
101
+ model: resolveModelPath(input.model, input.hub),
102
+ tools: defaultBenchTools(),
103
+ budget: {
104
+ max_rounds: input.prompt.frontmatter.max_rounds * 2,
105
+ total_tokens: DEFAULT_TOTAL_TOKENS,
106
+ wall_time_ms: DEFAULT_WALL_TIME_MS
107
+ },
108
+ system: systemBody
109
+ });
110
+ const isBudgetExhausted = !exec.success && exec.error?.code === "BUDGET_EXHAUSTED" && exec.data && typeof exec.data === "object" && exec.data.status === "budget_exhausted";
111
+ if (!exec.success && !isBudgetExhausted) {
112
+ const err = (typeof exec.data === "object" && exec.data !== null ? exec.data.error : void 0) ?? exec.error?.message ?? "agent-run dispatch failed";
113
+ throw new Error(`[runOne] agent-run failed for model=${input.model}: ${err}`);
114
+ }
115
+ const result = exec.data;
116
+ return {
117
+ model: input.model,
118
+ prompt: input.prompt.id,
119
+ scores: require_scoring.scoreFromTrace(input.benchPrompt, result),
120
+ raw: result
121
+ };
122
+ }
123
+ function promptToBenchPrompt(prompt) {
124
+ return {
125
+ max_rounds: prompt.frontmatter.max_rounds,
126
+ expected_paths: extractExpectedPaths(prompt),
127
+ expected_outcomes: extractExpectedOutcomes(prompt)
128
+ };
129
+ }
130
+ /**
131
+ * Extract the raw `# Expected outcomes` section text. Returns the entire
132
+ * section body (including any markdown table) so the LLM judge has the full
133
+ * rubric. Empty string when the section is missing — the judge still runs
134
+ * but has no rubric to score against; in practice this means a low score.
135
+ */
136
+ function extractExpectedOutcomes(prompt) {
137
+ const key = Object.keys(prompt.sections).find((k) => k.startsWith("Expected outcomes"));
138
+ if (!key) return "";
139
+ return prompt.sections[key].trim();
140
+ }
141
+ /**
142
+ * Extract expected_paths from the `# Expected paths` body section's first
143
+ * fenced code block. Strips `// …` line comments. Lines that don't start with
144
+ * `/` are ignored (so prose inside the block doesn't pollute the path set).
145
+ */
146
+ function extractExpectedPaths(prompt) {
147
+ const key = Object.keys(prompt.sections).find((k) => k.startsWith("Expected paths"));
148
+ if (!key) return [];
149
+ const section = prompt.sections[key];
150
+ const fenced = section.match(/```[^\n]*\n([\s\S]*?)```/);
151
+ const block = fenced ? fenced[1] : section;
152
+ const out = [];
153
+ for (const raw of block.split(/\r?\n/)) {
154
+ const noComment = raw.split("//")[0].trim();
155
+ if (noComment.startsWith("/")) out.push(noComment);
156
+ }
157
+ return out;
158
+ }
159
+ function validateRunArgs(args) {
160
+ const models = args.models;
161
+ if (!Array.isArray(models)) throw new require_errors.ValidationError("Phase 1: 'models' must be an explicit array (no 'all' shortcut)");
162
+ if (models.length < 1 || models.length > MAX_MODELS) throw new require_errors.ValidationError(`Phase 1: 'models' length must be in [1, ${MAX_MODELS}]`);
163
+ for (const m of models) if (typeof m !== "string" || m.length === 0) throw new require_errors.ValidationError("Phase 1: 'models' entries must be non-empty strings");
164
+ const promptsArg = args.prompts;
165
+ let prompts;
166
+ if (promptsArg === "all") prompts = "all";
167
+ else if (Array.isArray(promptsArg)) {
168
+ if (promptsArg.length < 1 || promptsArg.length > MAX_PROMPTS_PER_RUN) throw new require_errors.ValidationError(`Phase 1: 'prompts' length must be in [1, ${MAX_PROMPTS_PER_RUN}]`);
169
+ for (const p of promptsArg) if (typeof p !== "string" || p.length === 0) throw new require_errors.ValidationError("Phase 1: 'prompts' entries must be non-empty strings");
170
+ prompts = promptsArg;
171
+ } else throw new require_errors.ValidationError("Phase 1: 'prompts' must be 'all' or an array of prompt ids");
172
+ const samples = clampInt(args.samples, DEFAULT_SAMPLES, 1, MAX_SAMPLES);
173
+ const parallel = clampInt(args.parallel, 1, 1, MAX_PARALLEL);
174
+ let hub;
175
+ if (args.hub !== void 0) {
176
+ if (typeof args.hub !== "string" || args.hub.length === 0) throw new require_errors.ValidationError("'hub' must be a non-empty string");
177
+ if (!/^[A-Za-z0-9._-]+$/.test(args.hub)) throw new require_errors.ValidationError(`'hub' has invalid characters: ${args.hub}`);
178
+ hub = args.hub;
179
+ }
180
+ return {
181
+ models,
182
+ prompts,
183
+ samples,
184
+ parallel,
185
+ ...hub !== void 0 ? { hub } : {}
186
+ };
187
+ }
188
+ function clampInt(raw, fallback, lo, hi) {
189
+ const v = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
190
+ return Math.max(lo, Math.min(hi, v));
191
+ }
192
+ function validateRunFullArgs(args) {
193
+ const models = args.models;
194
+ if (!Array.isArray(models)) throw new require_errors.ValidationError("'models' must be an explicit array (no 'all' shortcut)");
195
+ if (models.length < 1 || models.length > MAX_MODELS_FULL) throw new require_errors.ValidationError(`'models' length must be in [1, ${MAX_MODELS_FULL}]`);
196
+ for (const m of models) if (typeof m !== "string" || m.length === 0) throw new require_errors.ValidationError("'models' entries must be non-empty strings");
197
+ const promptsArg = args.prompts;
198
+ let prompts;
199
+ if (promptsArg === "all") prompts = "all";
200
+ else if (Array.isArray(promptsArg)) {
201
+ if (promptsArg.length < 1 || promptsArg.length > MAX_PROMPTS_FULL) throw new require_errors.ValidationError(`'prompts' length must be in [1, ${MAX_PROMPTS_FULL}]`);
202
+ for (const p of promptsArg) if (typeof p !== "string" || p.length === 0) throw new require_errors.ValidationError("'prompts' entries must be non-empty strings");
203
+ prompts = promptsArg;
204
+ } else throw new require_errors.ValidationError("'prompts' must be 'all' or an array of prompt ids");
205
+ const samples = clampInt(args.samples, DEFAULT_SAMPLES, 1, MAX_SAMPLES);
206
+ const parallel = clampInt(args.parallel, 1, 1, MAX_PARALLEL);
207
+ let judgeFor;
208
+ if (args.judgeFor !== void 0) {
209
+ if (!Array.isArray(args.judgeFor)) throw new require_errors.ValidationError("'judgeFor' must be an array of dimension names");
210
+ const allowed = new Set(ALLOWED_JUDGE_DIMENSIONS);
211
+ for (const d of args.judgeFor) if (typeof d !== "string" || !allowed.has(d)) throw new require_errors.ValidationError(`'judgeFor' entries must be one of: ${ALLOWED_JUDGE_DIMENSIONS.join(", ")}`);
212
+ judgeFor = args.judgeFor;
213
+ }
214
+ let includeRaw = false;
215
+ if (args.includeRaw !== void 0) {
216
+ if (typeof args.includeRaw !== "boolean") throw new require_errors.ValidationError("'includeRaw' must be a boolean");
217
+ includeRaw = args.includeRaw;
218
+ }
219
+ let outlierThreshold;
220
+ if (args.outlierThreshold !== void 0) {
221
+ if (typeof args.outlierThreshold !== "number" || !Number.isFinite(args.outlierThreshold)) throw new require_errors.ValidationError("'outlierThreshold' must be a finite number");
222
+ outlierThreshold = Math.max(.01, Math.min(1, args.outlierThreshold));
223
+ }
224
+ let outlierRerun = false;
225
+ if (args.outlierRerun !== void 0) {
226
+ if (typeof args.outlierRerun !== "boolean") throw new require_errors.ValidationError("'outlierRerun' must be a boolean");
227
+ outlierRerun = args.outlierRerun;
228
+ }
229
+ let judges;
230
+ if (args.judges !== void 0) {
231
+ if (!Array.isArray(args.judges)) throw new require_errors.ValidationError("'judges' must be an array of judge model ids");
232
+ if (args.judges.length < 1 || args.judges.length > MAX_JUDGES) throw new require_errors.ValidationError(`'judges' length must be in [1, ${MAX_JUDGES}]`);
233
+ for (const j of args.judges) {
234
+ if (typeof j !== "string" || j.length === 0) throw new require_errors.ValidationError("'judges' entries must be non-empty strings");
235
+ if (!JUDGE_ID_PATTERN.test(j)) throw new require_errors.ValidationError(`'judges' entry "${j}" has invalid characters`);
236
+ }
237
+ judges = args.judges;
238
+ }
239
+ let hub;
240
+ if (args.hub !== void 0) {
241
+ if (typeof args.hub !== "string" || args.hub.length === 0) throw new require_errors.ValidationError("'hub' must be a non-empty string");
242
+ if (!/^[A-Za-z0-9._-]+$/.test(args.hub)) throw new require_errors.ValidationError(`'hub' has invalid characters: ${args.hub}`);
243
+ hub = args.hub;
244
+ }
245
+ return {
246
+ models,
247
+ prompts,
248
+ samples,
249
+ parallel,
250
+ ...hub !== void 0 ? { hub } : {},
251
+ judgeFor,
252
+ judges,
253
+ includeRaw,
254
+ outlierThreshold,
255
+ outlierRerun
256
+ };
257
+ }
258
+ const DIMENSIONS = require_scoring.OBJECTIVE_DIMENSIONS;
259
+ function promptDomain(prompt) {
260
+ return prompt.frontmatter.domain ?? "english";
261
+ }
262
+ /** Mean of the populated dimension scores for a single sample. */
263
+ function sampleDimensionMean(s) {
264
+ const vals = DIMENSIONS.map((d) => s[d]);
265
+ if (typeof s.instruction_following === "number" && Number.isFinite(s.instruction_following)) vals.push(s.instruction_following);
266
+ return vals.reduce((a, b) => a + b, 0) / vals.length;
267
+ }
268
+ /**
269
+ * Population stddev (divides by N, not N-1) — matches the convention the
270
+ * benchmark spec uses. For N=1 this is 0 by definition; the existing path
271
+ * just returns 0 without dividing. N=0 short-circuits to 0 above the loop.
272
+ */
273
+ function meanAndStddev(values) {
274
+ if (values.length === 0) return {
275
+ mean: 0,
276
+ stddev: 0
277
+ };
278
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
279
+ return {
280
+ mean,
281
+ stddev: Math.sqrt(values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length)
282
+ };
283
+ }
284
+ function emptyDimensionScores() {
285
+ return {
286
+ exploration: 0,
287
+ tool_reliability: 0,
288
+ self_stop: 0,
289
+ parallelism: 0,
290
+ efficiency: 0
291
+ };
292
+ }
293
+ function aggregateAcrossPrompts(samples, prompts) {
294
+ const scores = emptyDimensionScores();
295
+ const stddev = { ...scores };
296
+ if (samples.length === 0) return {
297
+ n_samples: 0,
298
+ scores,
299
+ stddev,
300
+ per_prompt: {}
301
+ };
302
+ for (const dim of DIMENSIONS) {
303
+ const { mean, stddev: sd } = meanAndStddev(samples.map((s) => s.scores[dim]));
304
+ scores[dim] = mean;
305
+ stddev[dim] = sd;
306
+ }
307
+ const ifValues = [];
308
+ for (const s of samples) {
309
+ const v = s.scores.instruction_following;
310
+ if (typeof v === "number" && Number.isFinite(v)) ifValues.push(v);
311
+ }
312
+ if (ifValues.length > 0) {
313
+ const { mean, stddev: sd } = meanAndStddev(ifValues);
314
+ scores.instruction_following = mean;
315
+ stddev.instruction_following = sd;
316
+ }
317
+ const byPrompt = /* @__PURE__ */ new Map();
318
+ for (const s of samples) {
319
+ const arr = byPrompt.get(s.prompt) ?? [];
320
+ arr.push(s);
321
+ byPrompt.set(s.prompt, arr);
322
+ }
323
+ const per_prompt = {};
324
+ for (const promptId of [...byPrompt.keys()].sort()) {
325
+ const promptSamples = byPrompt.get(promptId);
326
+ const dims = emptyDimensionScores();
327
+ for (const dim of DIMENSIONS) dims[dim] = meanAndStddev(promptSamples.map((s) => s.scores[dim])).mean;
328
+ const ifPerPrompt = [];
329
+ for (const s of promptSamples) {
330
+ const v = s.scores.instruction_following;
331
+ if (typeof v === "number" && Number.isFinite(v)) ifPerPrompt.push(v);
332
+ }
333
+ if (ifPerPrompt.length > 0) dims.instruction_following = meanAndStddev(ifPerPrompt).mean;
334
+ per_prompt[promptId] = dims;
335
+ }
336
+ const judgeScores = /* @__PURE__ */ new Map();
337
+ for (const s of samples) {
338
+ if (!s.judges_raw) continue;
339
+ for (const [id, score] of Object.entries(s.judges_raw)) {
340
+ const arr = judgeScores.get(id) ?? [];
341
+ if (typeof score === "number" && Number.isFinite(score)) arr.push(score);
342
+ judgeScores.set(id, arr);
343
+ }
344
+ }
345
+ const out = {
346
+ n_samples: samples.length,
347
+ scores,
348
+ stddev,
349
+ per_prompt
350
+ };
351
+ if (prompts && prompts.length > 0) {
352
+ const domainOf = /* @__PURE__ */ new Map();
353
+ for (const p of prompts) domainOf.set(p.id, promptDomain(p));
354
+ const samplesByDomain = /* @__PURE__ */ new Map();
355
+ for (const s of samples) {
356
+ const d = domainOf.get(s.prompt) ?? "english";
357
+ const arr = samplesByDomain.get(d) ?? [];
358
+ arr.push(s);
359
+ samplesByDomain.set(d, arr);
360
+ }
361
+ if (samplesByDomain.size > 0) {
362
+ const domains = {};
363
+ for (const d of [...samplesByDomain.keys()].sort()) {
364
+ const means = samplesByDomain.get(d).map((s) => sampleDimensionMean(s.scores));
365
+ domains[d] = means.reduce((a, b) => a + b, 0) / means.length;
366
+ }
367
+ out.domains = domains;
368
+ }
369
+ }
370
+ if (judgeScores.size > 0) {
371
+ const judges_raw = {};
372
+ for (const id of [...judgeScores.keys()].sort()) {
373
+ const vs = judgeScores.get(id);
374
+ judges_raw[id] = vs.length === 0 ? null : meanAndStddev(vs).mean;
375
+ }
376
+ out.judges_raw = judges_raw;
377
+ }
378
+ return out;
379
+ }
380
+ async function runMatrix(input) {
381
+ const benchPromptCache = /* @__PURE__ */ new Map();
382
+ for (const p of input.prompts) benchPromptCache.set(p.id, promptToBenchPrompt(p));
383
+ const results = {};
384
+ for (const model of input.models) {
385
+ const samples = [];
386
+ for (const prompt of input.prompts) {
387
+ const batches = chunk(Array.from({ length: input.samples }, () => prompt), input.parallel);
388
+ for (const batch of batches) {
389
+ const out = await Promise.all(batch.map((p) => runOne({
390
+ deps: input.deps,
391
+ model,
392
+ hub: input.hub,
393
+ prompt: p,
394
+ benchPrompt: benchPromptCache.get(p.id)
395
+ })));
396
+ samples.push(...out);
397
+ }
398
+ }
399
+ results[model] = aggregateAcrossPrompts(samples, input.prompts);
400
+ }
401
+ const promptIds = input.prompts.map((p) => p.id);
402
+ return {
403
+ results,
404
+ file: await writeResultsAtomic({
405
+ version: (/* @__PURE__ */ new Date()).toISOString(),
406
+ config: {
407
+ samples_per_pair: input.samples,
408
+ models_tested: input.models,
409
+ prompts_used: promptIds
410
+ },
411
+ results
412
+ }, input.resultsDir),
413
+ ranBatch: {
414
+ models: input.models,
415
+ prompts: promptIds,
416
+ samples: input.samples
417
+ }
418
+ };
419
+ }
420
+ function chunk(arr, size) {
421
+ const out = [];
422
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
423
+ return out;
424
+ }
425
+ /**
426
+ * `runMatrixFull` is the headless full-matrix runner used by `/bench/.actions/runFull`.
427
+ *
428
+ * Differences vs `runMatrix`:
429
+ * - Allows up to 200 models / 200 prompts (vs 5/5 for the fast `run` action).
430
+ * - Flushes the results JSON to disk after every (model, prompt) pair so a
431
+ * SIGKILL mid-run preserves the entries already completed.
432
+ * - Retries rate-limit errors with exponential backoff (2s/4s/8s, max 3
433
+ * retries). Auth errors (`INVALID_API_KEY`, `UNAUTHENTICATED`, etc.) fail
434
+ * immediately without retry.
435
+ * - Records per-sample failures on the model aggregate's `errors[]` instead
436
+ * of throwing — one bad sample doesn't block the rest of the matrix.
437
+ * - Emits per-sample progress via `onProgress` so callers can stream live
438
+ * output (`▸ <model> × <prompt> sample N/M: ...`).
439
+ */
440
+ async function runMatrixFull(input) {
441
+ const benchPromptCache = /* @__PURE__ */ new Map();
442
+ for (const p of input.prompts) benchPromptCache.set(p.id, promptToBenchPrompt(p));
443
+ const sleep = input.sleep ?? defaultSleep;
444
+ const promptIds = input.prompts.map((p) => p.id);
445
+ const isoVersion = (/* @__PURE__ */ new Date()).toISOString();
446
+ const includeRaw = input.includeRaw ?? false;
447
+ const effectiveJudges = input.judges && input.judges.length > 0 ? [...input.judges] : [];
448
+ const judgeForRequested = input.judgeFor?.includes("instruction_following") ?? false;
449
+ let resultsFileJudgeModel = null;
450
+ if (judgeForRequested) if (effectiveJudges.length > 1) resultsFileJudgeModel = [...effectiveJudges];
451
+ else if (effectiveJudges.length === 1) resultsFileJudgeModel = effectiveJudges[0];
452
+ else resultsFileJudgeModel = input.judgeModel ?? require_judge.DEFAULT_JUDGE_MODEL;
453
+ const results = {};
454
+ const samplesByModel = {};
455
+ const errorsByModel = {};
456
+ const rawByModel = {};
457
+ let lastFile = "";
458
+ let lastRawFile = null;
459
+ const outlierThreshold = input.outlierThreshold ?? require_outlier.DEFAULT_OUTLIER_THRESHOLD;
460
+ const outlierRerun = input.outlierRerun ?? false;
461
+ const runSampleBatch = async (model, prompt, sampleIndices, totalSamples) => {
462
+ for (const batch of chunk(sampleIndices, input.parallel)) {
463
+ const settled = await Promise.all(batch.map(async (sampleIndex) => ({
464
+ sampleIndex,
465
+ outcome: await runOneWithRetry({
466
+ deps: input.deps,
467
+ model,
468
+ hub: input.hub,
469
+ prompt,
470
+ benchPrompt: benchPromptCache.get(prompt.id),
471
+ sleep
472
+ })
473
+ })));
474
+ for (const { sampleIndex, outcome } of settled) {
475
+ const event = {
476
+ model,
477
+ prompt: prompt.id,
478
+ sampleIndex,
479
+ totalSamples,
480
+ status: outcome.kind === "ok" ? "ok" : "error"
481
+ };
482
+ if (outcome.kind === "ok") {
483
+ if (judgeForRequested) if (effectiveJudges.length > 0) {
484
+ const perJudge = await Promise.all(effectiveJudges.map((j) => require_judge.judgeInstructionFollowing(input.deps, benchPromptCache.get(prompt.id), outcome.value.raw, j, input.judgeHub)));
485
+ const judgesRaw = {};
486
+ effectiveJudges.forEach((j, i) => {
487
+ judgesRaw[j] = perJudge[i];
488
+ });
489
+ outcome.value.judges_raw = judgesRaw;
490
+ const aggregate = require_judge.aggregateJudgeScores(perJudge);
491
+ if (aggregate !== null) outcome.value.scores.instruction_following = aggregate;
492
+ } else {
493
+ const score = await require_judge.judgeInstructionFollowing(input.deps, benchPromptCache.get(prompt.id), outcome.value.raw, input.judgeModel, input.judgeHub);
494
+ if (score !== null) outcome.value.scores.instruction_following = score;
495
+ }
496
+ samplesByModel[model].push(outcome.value);
497
+ event.scores = outcome.value.scores;
498
+ if (includeRaw) rawByModel[model][prompt.id].push({
499
+ sampleIndex,
500
+ raw: outcome.value.raw
501
+ });
502
+ } else {
503
+ errorsByModel[model].push(outcome.error);
504
+ event.error = outcome.error;
505
+ }
506
+ input.onProgress?.(event);
507
+ }
508
+ }
509
+ };
510
+ const buildResultsFile = (outliers$1) => ({
511
+ version: isoVersion,
512
+ judge_model: resultsFileJudgeModel,
513
+ config: {
514
+ samples_per_pair: input.samples,
515
+ models_tested: input.models,
516
+ prompts_used: promptIds
517
+ },
518
+ results,
519
+ outliers: outliers$1
520
+ });
521
+ for (const model of input.models) {
522
+ samplesByModel[model] = [];
523
+ errorsByModel[model] = [];
524
+ if (includeRaw) rawByModel[model] = {};
525
+ for (const prompt of input.prompts) {
526
+ if (includeRaw) rawByModel[model][prompt.id] = [];
527
+ await runSampleBatch(model, prompt, Array.from({ length: input.samples }, (_, i) => i + 1), input.samples);
528
+ const agg = aggregateAcrossPrompts(samplesByModel[model], input.prompts);
529
+ const errs = errorsByModel[model];
530
+ results[model] = errs.length > 0 ? {
531
+ ...agg,
532
+ errors: errs
533
+ } : agg;
534
+ lastFile = await writeResultsAtomic(buildResultsFile([]), input.resultsDir);
535
+ if (includeRaw) lastRawFile = await writeRawResultsAtomic({
536
+ version: isoVersion,
537
+ config: {
538
+ samples_per_pair: input.samples,
539
+ models_tested: input.models,
540
+ prompts_used: promptIds
541
+ },
542
+ raw: rawByModel
543
+ }, input.resultsDir);
544
+ await input.onPairComplete?.(model, prompt.id, lastFile);
545
+ }
546
+ await input.onModelComplete?.(model, lastFile);
547
+ }
548
+ let outliers = require_outlier.findOutliers(samplesByModel, outlierThreshold);
549
+ if (outlierRerun && outliers.length > 0) {
550
+ const promptById = new Map(input.prompts.map((p) => [p.id, p]));
551
+ const rerunByModel = /* @__PURE__ */ new Map();
552
+ for (const o of outliers) {
553
+ if (!promptById.has(o.prompt)) continue;
554
+ const set = rerunByModel.get(o.model) ?? /* @__PURE__ */ new Set();
555
+ set.add(o.prompt);
556
+ rerunByModel.set(o.model, set);
557
+ }
558
+ for (const [model, promptIds$1] of rerunByModel) {
559
+ const countByPrompt = /* @__PURE__ */ new Map();
560
+ for (const s of samplesByModel[model]) countByPrompt.set(s.prompt, (countByPrompt.get(s.prompt) ?? 0) + 1);
561
+ for (const promptId of promptIds$1) {
562
+ const prompt = promptById.get(promptId);
563
+ const existingForPair = countByPrompt.get(promptId) ?? 0;
564
+ const extra = Math.min(3, 2 * input.samples - existingForPair);
565
+ if (extra <= 0) continue;
566
+ await runSampleBatch(model, prompt, Array.from({ length: extra }, (_, i) => existingForPair + i + 1), existingForPair + extra);
567
+ }
568
+ }
569
+ for (const model of rerunByModel.keys()) {
570
+ const agg = aggregateAcrossPrompts(samplesByModel[model], input.prompts);
571
+ const errs = errorsByModel[model];
572
+ results[model] = errs.length > 0 ? {
573
+ ...agg,
574
+ errors: errs
575
+ } : agg;
576
+ }
577
+ outliers = require_outlier.findOutliers(samplesByModel, outlierThreshold);
578
+ }
579
+ lastFile = await writeResultsAtomic(buildResultsFile(outliers), input.resultsDir);
580
+ if (includeRaw) lastRawFile = await writeRawResultsAtomic({
581
+ version: isoVersion,
582
+ config: {
583
+ samples_per_pair: input.samples,
584
+ models_tested: input.models,
585
+ prompts_used: promptIds
586
+ },
587
+ raw: rawByModel
588
+ }, input.resultsDir);
589
+ return {
590
+ results,
591
+ file: lastFile,
592
+ rawFile: lastRawFile,
593
+ ranBatch: {
594
+ models: input.models,
595
+ prompts: promptIds,
596
+ samples: input.samples
597
+ }
598
+ };
599
+ }
600
+ /**
601
+ * Wraps `runOne` with bounded exponential backoff for rate-limit errors and
602
+ * fast-fail for auth errors. Other errors are non-retryable but classified as
603
+ * "other" so they still surface in `errors[]`.
604
+ */
605
+ async function runOneWithRetry(input) {
606
+ for (let attempts = 1;; attempts++) try {
607
+ return {
608
+ kind: "ok",
609
+ value: await runOne({
610
+ deps: input.deps,
611
+ model: input.model,
612
+ hub: input.hub,
613
+ prompt: input.prompt,
614
+ benchPrompt: input.benchPrompt
615
+ })
616
+ };
617
+ } catch (err) {
618
+ const message = err instanceof Error ? err.message : String(err);
619
+ const cls = classifyError(message);
620
+ if (cls === "rate_limit" && attempts <= RATE_LIMIT_MAX_RETRIES) {
621
+ await input.sleep(RATE_LIMIT_BASE_BACKOFF_MS * 2 ** (attempts - 1));
622
+ continue;
623
+ }
624
+ return {
625
+ kind: "error",
626
+ error: {
627
+ kind: cls,
628
+ path: "/dev/agent/.actions/run",
629
+ message,
630
+ attempts
631
+ }
632
+ };
633
+ }
634
+ }
635
+ const RATE_LIMIT_PATTERNS = /(rate.*limit|429|too many requests)/i;
636
+ const AUTH_ERROR_PATTERNS = /(invalid[ _]api[ _]key|unauthenticated|unauthorized)/i;
637
+ function classifyError(message) {
638
+ if (RATE_LIMIT_PATTERNS.test(message)) return "rate_limit";
639
+ if (AUTH_ERROR_PATTERNS.test(message)) return "auth";
640
+ return "other";
641
+ }
642
+ function defaultSleep(ms) {
643
+ return new Promise((resolve) => setTimeout(resolve, ms));
644
+ }
645
+ /**
646
+ * Write `payload` to `<dir>/<ISO-date>.json` atomically (write to a randomised
647
+ * `.tmp-<id>.json` then rename). Best-effort redaction runs before
648
+ * serialisation: both sensitive labels (api_key / Authorization / …) and
649
+ * common credential VALUE shapes (Bearer tokens, `sk-`/`ya29.` keys, PEM
650
+ * blocks, AWS/Slack ids) are blanked so credentials that leak into agent-run
651
+ * traces are scrubbed from the on-disk record. This is a safety net, not a
652
+ * guarantee — the design keeps credentials out of traces. Aborts (without
653
+ * writing) when
654
+ * the serialised payload would exceed `MAX_RESULTS_SIZE_BYTES` — the failure
655
+ * mode is intentionally noisy so a bug that bloats the results dir never lands
656
+ * on disk silently.
657
+ */
658
+ async function writeResultsAtomic(payload, dir) {
659
+ const safe = sanitiseForResults(payload);
660
+ const json = `${JSON.stringify(safe, null, 2)}\n`;
661
+ const bytes = Buffer.byteLength(json, "utf8");
662
+ if (bytes > MAX_RESULTS_SIZE_BYTES) throw new require_errors.ValidationError(`results JSON is ${bytes} bytes — exceeds ${MAX_RESULTS_SIZE_BYTES} byte cap. If raw traces are needed pass \`includeRaw: true\` (writes a separate \`<stamp>-raw.json\`).`);
663
+ return writeAtomic(dir, `${payload.version.replace(/[:.]/g, "-")}.json`, json);
664
+ }
665
+ /**
666
+ * Write the raw-trace sidecar to `<dir>/<ISO-date>-raw.json` atomically. Uses
667
+ * the same sensitive-string redaction as the main results file. Has no size
668
+ * cap — the maintainer opted in via `includeRaw: true` and a hard fail
669
+ * mid-run would discard the agent traces they explicitly asked to keep.
670
+ */
671
+ async function writeRawResultsAtomic(payload, dir) {
672
+ const safe = sanitiseForResults(payload);
673
+ const json = `${JSON.stringify(safe, null, 2)}\n`;
674
+ return writeAtomic(dir, `${payload.version.replace(/[:.]/g, "-")}-raw.json`, json);
675
+ }
676
+ async function writeAtomic(dir, filename, json) {
677
+ await node_fs.promises.mkdir(dir, { recursive: true });
678
+ const finalPath = (0, node_path.join)(dir, filename);
679
+ const tmpPath = (0, node_path.join)(dir, `.tmp-${(0, node_crypto.randomBytes)(6).toString("hex")}.json`);
680
+ await node_fs.promises.writeFile(tmpPath, json, "utf8");
681
+ try {
682
+ await node_fs.promises.rename(tmpPath, finalPath);
683
+ } catch (err) {
684
+ await node_fs.promises.rm(tmpPath, { force: true });
685
+ throw err;
686
+ }
687
+ return finalPath;
688
+ }
689
+ function sanitiseForResults(value) {
690
+ let cleaned = JSON.stringify(value);
691
+ for (const re of SECRET_VALUE_PATTERNS) cleaned = cleaned.replace(re, "[REDACTED]");
692
+ for (const re of SENSITIVE_PATTERNS) cleaned = cleaned.replace(re, "[REDACTED]");
693
+ return JSON.parse(cleaned);
694
+ }
695
+
696
+ //#endregion
697
+ exports.aggregateAcrossPrompts = aggregateAcrossPrompts;
698
+ exports.defaultBenchSystem = defaultBenchSystem;
699
+ exports.defaultBenchTools = defaultBenchTools;
700
+ exports.extractExpectedOutcomes = extractExpectedOutcomes;
701
+ exports.extractExpectedPaths = extractExpectedPaths;
702
+ exports.meanAndStddev = meanAndStddev;
703
+ exports.promptToBenchPrompt = promptToBenchPrompt;
704
+ exports.runMatrix = runMatrix;
705
+ exports.runMatrixFull = runMatrixFull;
706
+ exports.runOne = runOne;
707
+ exports.validateRunArgs = validateRunArgs;
708
+ exports.validateRunFullArgs = validateRunFullArgs;
709
+ exports.writeRawResultsAtomic = writeRawResultsAtomic;
710
+ exports.writeResultsAtomic = writeResultsAtomic;