@axlsdk/eval 0.17.9 → 0.18.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.
- package/README.md +48 -7
- package/dist/{chunk-NETD4AUB.js → chunk-BWBX5YVQ.js} +231 -162
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +497 -207
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +279 -47
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +301 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -1
- package/dist/index.d.ts +176 -1
- package/dist/index.js +79 -16
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NETD4AUB.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
aggregateRuns,
|
|
4
4
|
evalCompare,
|
|
5
|
+
evaluateScorerErrorRateGate,
|
|
5
6
|
rescore,
|
|
6
|
-
runEval
|
|
7
|
-
|
|
7
|
+
runEval,
|
|
8
|
+
scorerCounts
|
|
9
|
+
} from "./chunk-BWBX5YVQ.js";
|
|
8
10
|
|
|
9
11
|
// src/cli.ts
|
|
10
12
|
import { readdirSync, statSync } from "fs";
|
|
@@ -57,10 +59,22 @@ function validateEvalConfig(cfg) {
|
|
|
57
59
|
if (badScorerIdx !== -1) {
|
|
58
60
|
return `exports a scorers array with a non-scorer entry at index ${badScorerIdx} \u2014 each scorer must have a score() method (use the scorer() factory).`;
|
|
59
61
|
}
|
|
62
|
+
for (const key of ["concurrency", "scorerConcurrency"]) {
|
|
63
|
+
const v = c[key];
|
|
64
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
65
|
+
return `exports an invalid ${key} (${typeof v === "number" ? v : typeof v}) \u2014 must be a positive integer.`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
{
|
|
69
|
+
const v = c.failOnScorerErrorRate;
|
|
70
|
+
if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1)) {
|
|
71
|
+
return `exports an invalid failOnScorerErrorRate (${typeof v === "number" ? v : typeof v}) \u2014 must be a number between 0 and 1.`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
60
74
|
return void 0;
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
// src/cli.ts
|
|
77
|
+
// src/cli-args.ts
|
|
64
78
|
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
65
79
|
"--output",
|
|
66
80
|
"--config",
|
|
@@ -68,8 +82,97 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
68
82
|
"--fail-on-regression",
|
|
69
83
|
"--threshold",
|
|
70
84
|
"--runs",
|
|
71
|
-
"--capture-traces"
|
|
85
|
+
"--capture-traces",
|
|
86
|
+
"--concurrency",
|
|
87
|
+
"--scorers"
|
|
72
88
|
]);
|
|
89
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
90
|
+
"--output",
|
|
91
|
+
"--config",
|
|
92
|
+
"--conditions",
|
|
93
|
+
"--runs",
|
|
94
|
+
"--concurrency",
|
|
95
|
+
"--scorers"
|
|
96
|
+
]);
|
|
97
|
+
function envInt(name) {
|
|
98
|
+
const raw = process.env[name];
|
|
99
|
+
if (raw == null || raw === "") return void 0;
|
|
100
|
+
const n = parseInt(raw, 10);
|
|
101
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
function parseEvalArgs(args) {
|
|
105
|
+
let outputPath;
|
|
106
|
+
let configArg;
|
|
107
|
+
let conditions = [];
|
|
108
|
+
let runs = 1;
|
|
109
|
+
let captureTraces = false;
|
|
110
|
+
let concurrency;
|
|
111
|
+
let scorerNames;
|
|
112
|
+
const paths = [];
|
|
113
|
+
for (let i = 0; i < args.length; i++) {
|
|
114
|
+
const arg = args[i];
|
|
115
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
116
|
+
if (i + 1 >= args.length) {
|
|
117
|
+
console.error(`Error: ${arg} requires a value`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const value = args[++i];
|
|
121
|
+
if (arg === "--output") outputPath = value;
|
|
122
|
+
else if (arg === "--config") configArg = value;
|
|
123
|
+
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
124
|
+
else if (arg === "--concurrency") {
|
|
125
|
+
const n = parseInt(value, 10);
|
|
126
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
127
|
+
console.error(`[axl-eval] Ignoring invalid --concurrency "${value}"; using 1.`);
|
|
128
|
+
concurrency = 1;
|
|
129
|
+
} else {
|
|
130
|
+
concurrency = n;
|
|
131
|
+
}
|
|
132
|
+
} else if (arg === "--scorers") {
|
|
133
|
+
scorerNames = [
|
|
134
|
+
...new Set(
|
|
135
|
+
value.split(",").map((s) => s.trim()).filter(Boolean)
|
|
136
|
+
)
|
|
137
|
+
];
|
|
138
|
+
} else {
|
|
139
|
+
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
140
|
+
}
|
|
141
|
+
} else if (arg === "--capture-traces") {
|
|
142
|
+
captureTraces = true;
|
|
143
|
+
} else if (arg.startsWith("--")) {
|
|
144
|
+
if (!KNOWN_FLAGS.has(arg)) {
|
|
145
|
+
console.error(`Unknown flag: ${arg}`);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
paths.push(arg);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
outputPath,
|
|
154
|
+
configArg,
|
|
155
|
+
conditions,
|
|
156
|
+
runs,
|
|
157
|
+
captureTraces,
|
|
158
|
+
concurrency,
|
|
159
|
+
scorerNames,
|
|
160
|
+
paths
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/cli.ts
|
|
165
|
+
function refuseToGate(reason) {
|
|
166
|
+
console.error(`[axl-eval] Refusing to gate: ${reason}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
function scorerSampleCounts(entry, name, items) {
|
|
170
|
+
if (entry.scored != null && entry.failed != null) {
|
|
171
|
+
return { scored: entry.scored, failed: entry.failed };
|
|
172
|
+
}
|
|
173
|
+
if (items) return scorerCounts(items, name);
|
|
174
|
+
return { scored: entry.scored ?? 0, failed: entry.failed ?? 0 };
|
|
175
|
+
}
|
|
73
176
|
function installAbortHandler(controller) {
|
|
74
177
|
let signalled = false;
|
|
75
178
|
const onSignal = (sig) => {
|
|
@@ -96,6 +199,15 @@ Usage:
|
|
|
96
199
|
axl-eval <path> --output <file> Save results to JSON
|
|
97
200
|
axl-eval <path> --config <file> Use config file for runtime
|
|
98
201
|
axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
|
|
202
|
+
axl-eval <path> --concurrency <n> Override item concurrency (flag > env
|
|
203
|
+
AXL_EVAL_CONCURRENCY > config > 5).
|
|
204
|
+
Max concurrent scorer calls is
|
|
205
|
+
concurrency x scorerConcurrency (the
|
|
206
|
+
per-eval scorerConcurrency defaults to 5).
|
|
207
|
+
axl-eval <path> --scorers <a,b> Run only the named scorers (single eval
|
|
208
|
+
file only). Stamps the result as a
|
|
209
|
+
subset so compare won't treat it as a
|
|
210
|
+
full baseline.
|
|
99
211
|
axl-eval <path> --capture-traces Populate EvalItem.traces on every item
|
|
100
212
|
(success + failure). Adds memory
|
|
101
213
|
overhead proportional to dataset size
|
|
@@ -104,6 +216,12 @@ Usage:
|
|
|
104
216
|
axl-eval compare <a> <b> Compare two eval result files
|
|
105
217
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
106
218
|
axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
|
|
219
|
+
axl-eval compare <a> <b> --max-scorer-error-rate <0..1> Exit 1 if a scorer's
|
|
220
|
+
failure rate exceeds the limit on
|
|
221
|
+
either side (deterministic scorers
|
|
222
|
+
tolerate zero failures). Always warns
|
|
223
|
+
when either side rests on a thinned
|
|
224
|
+
sample, even without this flag.
|
|
107
225
|
|
|
108
226
|
Config auto-detection (when --config is not specified):
|
|
109
227
|
${CONFIG_CANDIDATES.join(" -> ")}
|
|
@@ -138,20 +256,61 @@ function parseThresholdArg(args) {
|
|
|
138
256
|
}
|
|
139
257
|
return Object.keys(map).length > 0 ? map : void 0;
|
|
140
258
|
}
|
|
259
|
+
function parseMaxScorerErrorRate(args) {
|
|
260
|
+
const idx = args.indexOf("--max-scorer-error-rate");
|
|
261
|
+
if (idx === -1) return void 0;
|
|
262
|
+
if (idx + 1 >= args.length) {
|
|
263
|
+
console.error("Error: --max-scorer-error-rate requires a value in [0, 1]");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const raw = args[idx + 1];
|
|
267
|
+
const n = /^\d*\.?\d+$/.test(raw.trim()) ? Number(raw) : NaN;
|
|
268
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
269
|
+
console.error(`Error: --max-scorer-error-rate must be a number in [0, 1], got "${raw}"`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
return n;
|
|
273
|
+
}
|
|
274
|
+
function scorerFilteredScorers(r) {
|
|
275
|
+
const first = Array.isArray(r) ? r[0] : r;
|
|
276
|
+
const meta = first?.metadata;
|
|
277
|
+
if (!meta || meta.scorerFiltered !== true) return void 0;
|
|
278
|
+
return Array.isArray(meta.scorersRun) ? meta.scorersRun : [];
|
|
279
|
+
}
|
|
141
280
|
async function runCompare(args) {
|
|
142
281
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
143
282
|
const thresholds = parseThresholdArg(args);
|
|
283
|
+
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
284
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
|
|
144
285
|
const files = args.filter(
|
|
145
|
-
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1]
|
|
286
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
146
287
|
);
|
|
147
288
|
if (files.length !== 2) {
|
|
148
|
-
console.error(
|
|
289
|
+
console.error(
|
|
290
|
+
"Usage: axl-eval compare <baseline.json> <candidate.json> [--threshold <value>] [--fail-on-regression] [--max-scorer-error-rate <0..1>]"
|
|
291
|
+
);
|
|
149
292
|
process.exit(1);
|
|
150
293
|
}
|
|
151
294
|
const baselineRaw = JSON.parse(await readFileAsync(files[0], "utf-8"));
|
|
152
295
|
const candidateRaw = JSON.parse(await readFileAsync(files[1], "utf-8"));
|
|
153
296
|
const baseline = baselineRaw;
|
|
154
297
|
const candidate = candidateRaw;
|
|
298
|
+
const filteredSides = [
|
|
299
|
+
["baseline", baseline],
|
|
300
|
+
["candidate", candidate]
|
|
301
|
+
].filter(([, r]) => scorerFilteredScorers(r) != null);
|
|
302
|
+
for (const [side, r] of filteredSides) {
|
|
303
|
+
const ran = scorerFilteredScorers(r);
|
|
304
|
+
const ranStr = ran && ran.length ? ` (ran: ${ran.join(", ")})` : "";
|
|
305
|
+
console.error(
|
|
306
|
+
`[axl-eval] WARNING: ${side} was run with --scorers \u2014 it tested only a subset${ranStr}, so this comparison is incomplete.`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (failOnRegression && filteredSides.length > 0) {
|
|
310
|
+
refuseToGate(
|
|
311
|
+
`${filteredSides.map(([s]) => s).join(", ")} was run with --scorers (a scorer subset); re-run without --scorers for a complete baseline.`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
155
314
|
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
156
315
|
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
157
316
|
console.log(
|
|
@@ -207,6 +366,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
207
366
|
Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
|
|
208
367
|
`
|
|
209
368
|
);
|
|
369
|
+
let sawThinnedSample = false;
|
|
370
|
+
for (const name of scorerNames) {
|
|
371
|
+
const s = comparison.scorers[name];
|
|
372
|
+
const bFailed = s.baselineFailed ?? 0;
|
|
373
|
+
const cFailed = s.candidateFailed ?? 0;
|
|
374
|
+
if (bFailed > 0 || cFailed > 0) {
|
|
375
|
+
sawThinnedSample = true;
|
|
376
|
+
const bAtt = (s.baselineScored ?? 0) + bFailed;
|
|
377
|
+
const cAtt = (s.candidateScored ?? 0) + cFailed;
|
|
378
|
+
console.error(
|
|
379
|
+
`[axl-eval] WARNING: scorer "${name}" failed on some items (baseline ${bFailed}/${bAtt}, candidate ${cFailed}/${cAtt}); the means above are computed over the surviving sample.`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (sawThinnedSample && maxScorerErrorRate == null) {
|
|
384
|
+
console.error(
|
|
385
|
+
`[axl-eval] (advisory \u2014 pass --max-scorer-error-rate <0..1> to fail CI on a thinned sample)`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (maxScorerErrorRate != null) {
|
|
389
|
+
const reason = evaluateScorerErrorRateGate(comparison, maxScorerErrorRate);
|
|
390
|
+
if (reason) refuseToGate(reason);
|
|
391
|
+
}
|
|
210
392
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
211
393
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
212
394
|
if (hasSignificance) {
|
|
@@ -220,11 +402,18 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
220
402
|
}
|
|
221
403
|
}
|
|
222
404
|
async function runRescore(args, signal) {
|
|
223
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
405
|
+
const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
|
|
224
406
|
if (paths.length < 2) {
|
|
225
407
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
226
408
|
process.exit(1);
|
|
227
409
|
}
|
|
410
|
+
if (scorerNames?.length) {
|
|
411
|
+
console.error(
|
|
412
|
+
"Error: --scorers is not supported with rescore; pass a scorer subset in the eval file instead."
|
|
413
|
+
);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
const itemConcurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY");
|
|
228
417
|
const [resultsPath, evalFilePath] = paths;
|
|
229
418
|
const raw = JSON.parse(await readFileAsync(resultsPath, "utf-8"));
|
|
230
419
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
@@ -245,7 +434,12 @@ async function runRescore(args, signal) {
|
|
|
245
434
|
const rescored = [];
|
|
246
435
|
for (const resultData of results) {
|
|
247
436
|
if (signal.aborted) break;
|
|
248
|
-
rescored.push(
|
|
437
|
+
rescored.push(
|
|
438
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
439
|
+
signal,
|
|
440
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
|
|
441
|
+
})
|
|
442
|
+
);
|
|
249
443
|
}
|
|
250
444
|
for (const r of rescored) {
|
|
251
445
|
console.log("\n" + formatTable(r) + "\n");
|
|
@@ -295,14 +489,15 @@ function formatTable(result) {
|
|
|
295
489
|
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
296
490
|
for (const name of scorerNames) {
|
|
297
491
|
const s = result.summary.scorers[name];
|
|
298
|
-
const
|
|
299
|
-
|
|
492
|
+
const { scored, failed } = scorerSampleCounts(s, name, result.items);
|
|
493
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
494
|
+
if (scored === 0) {
|
|
300
495
|
lines.push(
|
|
301
|
-
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}`
|
|
496
|
+
` ${name.padEnd(maxNameLen)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)} ${"--".padStart(colWidth)}${annotation}`
|
|
302
497
|
);
|
|
303
498
|
} else {
|
|
304
499
|
lines.push(
|
|
305
|
-
` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}`
|
|
500
|
+
` ${name.padEnd(maxNameLen)} ${s.mean.toFixed(2).padStart(colWidth)} ${s.min.toFixed(2).padStart(colWidth)} ${s.max.toFixed(2).padStart(colWidth)} ${s.p50.toFixed(2).padStart(colWidth)} ${s.p95.toFixed(2).padStart(colWidth)}${annotation}`
|
|
306
501
|
);
|
|
307
502
|
}
|
|
308
503
|
}
|
|
@@ -350,8 +545,10 @@ function formatMultiRunTable(summary) {
|
|
|
350
545
|
for (const name of scorerNames) {
|
|
351
546
|
const s = summary.scorers[name];
|
|
352
547
|
const meanStd = `${s.mean.toFixed(3)} \xB1 ${s.std.toFixed(3)}`;
|
|
548
|
+
const { scored, failed } = scorerSampleCounts(s, name);
|
|
549
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
353
550
|
lines.push(
|
|
354
|
-
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}`
|
|
551
|
+
` ${name.padEnd(maxNameLen)} ${meanStd.padStart(colWidth)} ${s.min.toFixed(3).padStart(8)} ${s.max.toFixed(3).padStart(8)}${annotation}`
|
|
355
552
|
);
|
|
356
553
|
}
|
|
357
554
|
if (summary.timing) {
|
|
@@ -365,6 +562,26 @@ function formatMultiRunTable(summary) {
|
|
|
365
562
|
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
366
563
|
return lines.join("\n");
|
|
367
564
|
}
|
|
565
|
+
function reportDegraded(result, label) {
|
|
566
|
+
const d = result.summary.degraded;
|
|
567
|
+
if (!d || d.length === 0) return false;
|
|
568
|
+
for (const x of d) {
|
|
569
|
+
const attempted = x.scored + x.failed;
|
|
570
|
+
const limitStr = x.type === "deterministic" ? "deterministic zero-tolerance" : `${(x.limit * 100).toFixed(1)}% limit`;
|
|
571
|
+
console.error(
|
|
572
|
+
`[axl-eval] DEGRADED: ${label} scorer "${x.scorer}" (${x.type}) failed ${(x.rate * 100).toFixed(1)}% (${x.failed}/${attempted}), over the ${limitStr}.`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
function reportTotalWipeout(result, label) {
|
|
578
|
+
const { count, failures } = result.summary;
|
|
579
|
+
if (count === 0 || failures < count) return false;
|
|
580
|
+
console.error(
|
|
581
|
+
`[axl-eval] FAILED: ${label} \u2014 all ${count} item(s) errored in the workflow (0 succeeded); the eval produced no scorable output.`
|
|
582
|
+
);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
368
585
|
async function resolveRuntimeFromConfig(configPath) {
|
|
369
586
|
try {
|
|
370
587
|
const mod = await importModule(configPath, import.meta.url);
|
|
@@ -425,41 +642,17 @@ async function getRuntime(configArg, conditions) {
|
|
|
425
642
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
426
643
|
return new AxlRuntime();
|
|
427
644
|
}
|
|
428
|
-
function parseEvalArgs(args) {
|
|
429
|
-
let outputPath;
|
|
430
|
-
let configArg;
|
|
431
|
-
let conditions = [];
|
|
432
|
-
let runs = 1;
|
|
433
|
-
let captureTraces = false;
|
|
434
|
-
const paths = [];
|
|
435
|
-
for (let i = 0; i < args.length; i++) {
|
|
436
|
-
const arg = args[i];
|
|
437
|
-
if (arg === "--output" || arg === "--config" || arg === "--conditions" || arg === "--runs") {
|
|
438
|
-
if (i + 1 >= args.length) {
|
|
439
|
-
console.error(`Error: ${arg} requires a value`);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
}
|
|
442
|
-
const value = args[++i];
|
|
443
|
-
if (arg === "--output") outputPath = value;
|
|
444
|
-
else if (arg === "--config") configArg = value;
|
|
445
|
-
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
446
|
-
else
|
|
447
|
-
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
448
|
-
} else if (arg === "--capture-traces") {
|
|
449
|
-
captureTraces = true;
|
|
450
|
-
} else if (arg.startsWith("--")) {
|
|
451
|
-
if (!KNOWN_FLAGS.has(arg)) {
|
|
452
|
-
console.error(`Unknown flag: ${arg}`);
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
paths.push(arg);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return { outputPath, configArg, conditions, runs, captureTraces, paths };
|
|
460
|
-
}
|
|
461
645
|
async function runEvalCommand(args, signal) {
|
|
462
|
-
const {
|
|
646
|
+
const {
|
|
647
|
+
outputPath,
|
|
648
|
+
configArg,
|
|
649
|
+
conditions,
|
|
650
|
+
runs,
|
|
651
|
+
captureTraces,
|
|
652
|
+
concurrency,
|
|
653
|
+
scorerNames,
|
|
654
|
+
paths
|
|
655
|
+
} = parseEvalArgs(args);
|
|
463
656
|
if (paths.length === 0) {
|
|
464
657
|
console.error("Error: No eval file path provided");
|
|
465
658
|
process.exit(1);
|
|
@@ -472,6 +665,10 @@ async function runEvalCommand(args, signal) {
|
|
|
472
665
|
console.error("Error: No eval files found");
|
|
473
666
|
process.exit(1);
|
|
474
667
|
}
|
|
668
|
+
if (scorerNames?.length && evalFiles.length > 1) {
|
|
669
|
+
console.error(`Error: --scorers requires a single eval file (matched ${evalFiles.length}).`);
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
475
672
|
const runtime = await getRuntime(configArg, conditions);
|
|
476
673
|
const results = [];
|
|
477
674
|
let failedFiles = 0;
|
|
@@ -487,6 +684,22 @@ async function runEvalCommand(args, signal) {
|
|
|
487
684
|
failedFiles++;
|
|
488
685
|
continue;
|
|
489
686
|
}
|
|
687
|
+
evalConfig.concurrency = concurrency ?? envInt("AXL_EVAL_CONCURRENCY") ?? evalConfig.concurrency ?? 5;
|
|
688
|
+
if (scorerNames?.length) {
|
|
689
|
+
const available = evalConfig.scorers.map((s) => s.name);
|
|
690
|
+
const unknown = scorerNames.filter((n) => !available.includes(n));
|
|
691
|
+
if (unknown.length) {
|
|
692
|
+
console.error(
|
|
693
|
+
`Error: --scorers: unknown scorer(s): ${unknown.join(", ")}. Available: ${available.join(", ")}`
|
|
694
|
+
);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
evalConfig.scorers = evalConfig.scorers.filter((s) => scorerNames.includes(s.name));
|
|
698
|
+
if (evalConfig.scorers.length === 0) {
|
|
699
|
+
console.error("Error: --scorers matched no scorers.");
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
490
703
|
const exported = pickExport(mod, "executeWorkflow");
|
|
491
704
|
let customExecute;
|
|
492
705
|
if (typeof exported === "function") {
|
|
@@ -543,6 +756,13 @@ async function runEvalCommand(args, signal) {
|
|
|
543
756
|
continue;
|
|
544
757
|
}
|
|
545
758
|
const runOptions = { signal, ...captureTraces ? { captureTraces: true } : {} };
|
|
759
|
+
const scorersRun = scorerNames?.length ? evalConfig.scorers.map((s) => s.name) : void 0;
|
|
760
|
+
const stampFiltered = (result) => {
|
|
761
|
+
if (scorersRun) {
|
|
762
|
+
result.metadata.scorerFiltered = true;
|
|
763
|
+
result.metadata.scorersRun = scorersRun;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
546
766
|
if (runs > 1) {
|
|
547
767
|
const { randomUUID } = await import("crypto");
|
|
548
768
|
const runGroupId = randomUUID();
|
|
@@ -555,6 +775,7 @@ async function runEvalCommand(args, signal) {
|
|
|
555
775
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
556
776
|
result.metadata.runGroupId = runGroupId;
|
|
557
777
|
result.metadata.runIndex = r;
|
|
778
|
+
stampFiltered(result);
|
|
558
779
|
runResults.push(result);
|
|
559
780
|
} catch (err) {
|
|
560
781
|
runFailure = err instanceof Error ? err : new Error(String(err));
|
|
@@ -586,10 +807,21 @@ async function runEvalCommand(args, signal) {
|
|
|
586
807
|
const summary = aggregateRuns(runResults);
|
|
587
808
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
588
809
|
for (const r of runResults) results.push(r);
|
|
810
|
+
let anyFailing = false;
|
|
811
|
+
for (const r of runResults) {
|
|
812
|
+
const wipeout = reportTotalWipeout(r, filePath);
|
|
813
|
+
const degraded = reportDegraded(r, filePath);
|
|
814
|
+
if (wipeout || degraded) anyFailing = true;
|
|
815
|
+
}
|
|
816
|
+
if (anyFailing && !partial) failedFiles++;
|
|
589
817
|
} else {
|
|
590
818
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
819
|
+
stampFiltered(result);
|
|
591
820
|
results.push(result);
|
|
592
821
|
console.log("\n" + formatTable(result) + "\n");
|
|
822
|
+
const wipeout = reportTotalWipeout(result, filePath);
|
|
823
|
+
const degraded = reportDegraded(result, filePath);
|
|
824
|
+
if (wipeout || degraded) failedFiles++;
|
|
593
825
|
}
|
|
594
826
|
} catch (err) {
|
|
595
827
|
console.error(
|