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