@axlsdk/eval 0.17.8 → 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 +74 -7
- package/dist/{chunk-NPKIP4SB.js → chunk-BWBX5YVQ.js} +245 -164
- package/dist/chunk-BWBX5YVQ.js.map +1 -0
- package/dist/cli.cjs +537 -214
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +305 -52
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +327 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +199 -18
- package/dist/index.d.ts +199 -18
- package/dist/index.js +91 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-NPKIP4SB.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,113 @@ 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"
|
|
88
|
+
]);
|
|
89
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
90
|
+
"--output",
|
|
91
|
+
"--config",
|
|
92
|
+
"--conditions",
|
|
93
|
+
"--runs",
|
|
94
|
+
"--concurrency",
|
|
95
|
+
"--scorers"
|
|
72
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
|
+
}
|
|
176
|
+
function installAbortHandler(controller) {
|
|
177
|
+
let signalled = false;
|
|
178
|
+
const onSignal = (sig) => {
|
|
179
|
+
if (signalled) {
|
|
180
|
+
console.error(`
|
|
181
|
+
[axl-eval] Second ${sig} \u2014 forcing exit.`);
|
|
182
|
+
process.exit(130);
|
|
183
|
+
}
|
|
184
|
+
signalled = true;
|
|
185
|
+
console.error(`
|
|
186
|
+
[axl-eval] ${sig} received \u2014 cancelling eval (press again to force exit)...`);
|
|
187
|
+
controller.abort();
|
|
188
|
+
};
|
|
189
|
+
process.on("SIGINT", onSignal);
|
|
190
|
+
process.on("SIGTERM", onSignal);
|
|
191
|
+
}
|
|
73
192
|
async function main() {
|
|
74
193
|
const args = process.argv.slice(2);
|
|
75
194
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
@@ -80,6 +199,15 @@ Usage:
|
|
|
80
199
|
axl-eval <path> --output <file> Save results to JSON
|
|
81
200
|
axl-eval <path> --config <file> Use config file for runtime
|
|
82
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.
|
|
83
211
|
axl-eval <path> --capture-traces Populate EvalItem.traces on every item
|
|
84
212
|
(success + failure). Adds memory
|
|
85
213
|
overhead proportional to dataset size
|
|
@@ -88,6 +216,12 @@ Usage:
|
|
|
88
216
|
axl-eval compare <a> <b> Compare two eval result files
|
|
89
217
|
axl-eval compare <a> <b> --threshold <v> Set regression threshold (global or per-scorer)
|
|
90
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.
|
|
91
225
|
|
|
92
226
|
Config auto-detection (when --config is not specified):
|
|
93
227
|
${CONFIG_CANDIDATES.join(" -> ")}
|
|
@@ -102,11 +236,13 @@ When no config is found, a bare AxlRuntime is created (providers from env vars).
|
|
|
102
236
|
await runCompare(args.slice(1));
|
|
103
237
|
return;
|
|
104
238
|
}
|
|
239
|
+
const controller = new AbortController();
|
|
240
|
+
installAbortHandler(controller);
|
|
105
241
|
if (args[0] === "rescore") {
|
|
106
|
-
await runRescore(args.slice(1));
|
|
242
|
+
await runRescore(args.slice(1), controller.signal);
|
|
107
243
|
return;
|
|
108
244
|
}
|
|
109
|
-
await runEvalCommand(args);
|
|
245
|
+
await runEvalCommand(args, controller.signal);
|
|
110
246
|
}
|
|
111
247
|
function parseThresholdArg(args) {
|
|
112
248
|
const idx = args.indexOf("--threshold");
|
|
@@ -120,20 +256,61 @@ function parseThresholdArg(args) {
|
|
|
120
256
|
}
|
|
121
257
|
return Object.keys(map).length > 0 ? map : void 0;
|
|
122
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
|
+
}
|
|
123
280
|
async function runCompare(args) {
|
|
124
281
|
const failOnRegression = args.includes("--fail-on-regression");
|
|
125
282
|
const thresholds = parseThresholdArg(args);
|
|
283
|
+
const maxScorerErrorRate = parseMaxScorerErrorRate(args);
|
|
284
|
+
const valueFlags = /* @__PURE__ */ new Set(["--threshold", "--max-scorer-error-rate"]);
|
|
126
285
|
const files = args.filter(
|
|
127
|
-
(a, i) => !a.startsWith("--") && !(i > 0 && args[i - 1]
|
|
286
|
+
(a, i) => !a.startsWith("--") && !(i > 0 && valueFlags.has(args[i - 1]))
|
|
128
287
|
);
|
|
129
288
|
if (files.length !== 2) {
|
|
130
|
-
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
|
+
);
|
|
131
292
|
process.exit(1);
|
|
132
293
|
}
|
|
133
294
|
const baselineRaw = JSON.parse(await readFileAsync(files[0], "utf-8"));
|
|
134
295
|
const candidateRaw = JSON.parse(await readFileAsync(files[1], "utf-8"));
|
|
135
296
|
const baseline = baselineRaw;
|
|
136
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
|
+
}
|
|
137
314
|
const compareOptions = thresholds != null ? { thresholds } : void 0;
|
|
138
315
|
const comparison = evalCompare(baseline, candidate, compareOptions);
|
|
139
316
|
console.log(
|
|
@@ -189,6 +366,29 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
189
366
|
Regressions: ${comparison.regressions.length} | Improvements: ${comparison.improvements.length} | Stable: ${stable}
|
|
190
367
|
`
|
|
191
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
|
+
}
|
|
192
392
|
if (failOnRegression && comparison.regressions.length > 0) {
|
|
193
393
|
const hasSignificance = scorerNames.some((n) => comparison.scorers[n].significant != null);
|
|
194
394
|
if (hasSignificance) {
|
|
@@ -201,12 +401,19 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
|
|
|
201
401
|
}
|
|
202
402
|
}
|
|
203
403
|
}
|
|
204
|
-
async function runRescore(args) {
|
|
205
|
-
const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
|
|
404
|
+
async function runRescore(args, signal) {
|
|
405
|
+
const { outputPath, configArg, conditions, concurrency, scorerNames, paths } = parseEvalArgs(args);
|
|
206
406
|
if (paths.length < 2) {
|
|
207
407
|
console.error("Usage: axl-eval rescore <results.json> <eval-file> [--output <file>]");
|
|
208
408
|
process.exit(1);
|
|
209
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");
|
|
210
417
|
const [resultsPath, evalFilePath] = paths;
|
|
211
418
|
const raw = JSON.parse(await readFileAsync(resultsPath, "utf-8"));
|
|
212
419
|
const results = Array.isArray(raw) ? raw : [raw];
|
|
@@ -226,7 +433,13 @@ async function runRescore(args) {
|
|
|
226
433
|
}
|
|
227
434
|
const rescored = [];
|
|
228
435
|
for (const resultData of results) {
|
|
229
|
-
|
|
436
|
+
if (signal.aborted) break;
|
|
437
|
+
rescored.push(
|
|
438
|
+
await rescore(resultData, evalConfig.scorers, runtime, {
|
|
439
|
+
signal,
|
|
440
|
+
...itemConcurrency != null ? { concurrency: itemConcurrency } : {}
|
|
441
|
+
})
|
|
442
|
+
);
|
|
230
443
|
}
|
|
231
444
|
for (const r of rescored) {
|
|
232
445
|
console.log("\n" + formatTable(r) + "\n");
|
|
@@ -276,14 +489,15 @@ function formatTable(result) {
|
|
|
276
489
|
lines.push(" " + "\u2500".repeat(ruleLen));
|
|
277
490
|
for (const name of scorerNames) {
|
|
278
491
|
const s = result.summary.scorers[name];
|
|
279
|
-
const
|
|
280
|
-
|
|
492
|
+
const { scored, failed } = scorerSampleCounts(s, name, result.items);
|
|
493
|
+
const annotation = failed > 0 ? ` (${scored}/${scored + failed} scored, ${failed} failed)` : "";
|
|
494
|
+
if (scored === 0) {
|
|
281
495
|
lines.push(
|
|
282
|
-
` ${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}`
|
|
283
497
|
);
|
|
284
498
|
} else {
|
|
285
499
|
lines.push(
|
|
286
|
-
` ${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}`
|
|
287
501
|
);
|
|
288
502
|
}
|
|
289
503
|
}
|
|
@@ -331,8 +545,10 @@ function formatMultiRunTable(summary) {
|
|
|
331
545
|
for (const name of scorerNames) {
|
|
332
546
|
const s = summary.scorers[name];
|
|
333
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)` : "";
|
|
334
550
|
lines.push(
|
|
335
|
-
` ${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}`
|
|
336
552
|
);
|
|
337
553
|
}
|
|
338
554
|
if (summary.timing) {
|
|
@@ -346,6 +562,26 @@ function formatMultiRunTable(summary) {
|
|
|
346
562
|
lines.push(` Total Cost: ${costStr} | Total Duration: ${durationStr}s`);
|
|
347
563
|
return lines.join("\n");
|
|
348
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
|
+
}
|
|
349
585
|
async function resolveRuntimeFromConfig(configPath) {
|
|
350
586
|
try {
|
|
351
587
|
const mod = await importModule(configPath, import.meta.url);
|
|
@@ -406,41 +642,17 @@ async function getRuntime(configArg, conditions) {
|
|
|
406
642
|
const { AxlRuntime } = await import("@axlsdk/axl");
|
|
407
643
|
return new AxlRuntime();
|
|
408
644
|
}
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
console.error(`Error: ${arg} requires a value`);
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
423
|
-
const value = args[++i];
|
|
424
|
-
if (arg === "--output") outputPath = value;
|
|
425
|
-
else if (arg === "--config") configArg = value;
|
|
426
|
-
else if (arg === "--runs") runs = Math.max(1, parseInt(value, 10) || 1);
|
|
427
|
-
else
|
|
428
|
-
conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
|
|
429
|
-
} else if (arg === "--capture-traces") {
|
|
430
|
-
captureTraces = true;
|
|
431
|
-
} else if (arg.startsWith("--")) {
|
|
432
|
-
if (!KNOWN_FLAGS.has(arg)) {
|
|
433
|
-
console.error(`Unknown flag: ${arg}`);
|
|
434
|
-
process.exit(1);
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
paths.push(arg);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return { outputPath, configArg, conditions, runs, captureTraces, paths };
|
|
441
|
-
}
|
|
442
|
-
async function runEvalCommand(args) {
|
|
443
|
-
const { outputPath, configArg, conditions, runs, captureTraces, paths } = parseEvalArgs(args);
|
|
645
|
+
async function runEvalCommand(args, signal) {
|
|
646
|
+
const {
|
|
647
|
+
outputPath,
|
|
648
|
+
configArg,
|
|
649
|
+
conditions,
|
|
650
|
+
runs,
|
|
651
|
+
captureTraces,
|
|
652
|
+
concurrency,
|
|
653
|
+
scorerNames,
|
|
654
|
+
paths
|
|
655
|
+
} = parseEvalArgs(args);
|
|
444
656
|
if (paths.length === 0) {
|
|
445
657
|
console.error("Error: No eval file path provided");
|
|
446
658
|
process.exit(1);
|
|
@@ -453,11 +665,16 @@ async function runEvalCommand(args) {
|
|
|
453
665
|
console.error("Error: No eval files found");
|
|
454
666
|
process.exit(1);
|
|
455
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
|
+
}
|
|
456
672
|
const runtime = await getRuntime(configArg, conditions);
|
|
457
673
|
const results = [];
|
|
458
674
|
let failedFiles = 0;
|
|
459
675
|
try {
|
|
460
676
|
for (const filePath of evalFiles) {
|
|
677
|
+
if (signal.aborted) break;
|
|
461
678
|
try {
|
|
462
679
|
const mod = await importModule(path.resolve(filePath), import.meta.url);
|
|
463
680
|
const evalConfig = pickDefault(mod);
|
|
@@ -467,6 +684,22 @@ async function runEvalCommand(args) {
|
|
|
467
684
|
failedFiles++;
|
|
468
685
|
continue;
|
|
469
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
|
+
}
|
|
470
703
|
const exported = pickExport(mod, "executeWorkflow");
|
|
471
704
|
let customExecute;
|
|
472
705
|
if (typeof exported === "function") {
|
|
@@ -522,18 +755,27 @@ async function runEvalCommand(args) {
|
|
|
522
755
|
failedFiles++;
|
|
523
756
|
continue;
|
|
524
757
|
}
|
|
525
|
-
const runOptions = captureTraces ? { captureTraces: true } :
|
|
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
|
+
};
|
|
526
766
|
if (runs > 1) {
|
|
527
767
|
const { randomUUID } = await import("crypto");
|
|
528
768
|
const runGroupId = randomUUID();
|
|
529
769
|
const runResults = [];
|
|
530
770
|
let runFailure;
|
|
531
771
|
for (let r = 0; r < runs; r++) {
|
|
772
|
+
if (signal.aborted) break;
|
|
532
773
|
console.error(`[axl-eval] Run ${r + 1}/${runs}...`);
|
|
533
774
|
try {
|
|
534
775
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
535
776
|
result.metadata.runGroupId = runGroupId;
|
|
536
777
|
result.metadata.runIndex = r;
|
|
778
|
+
stampFiltered(result);
|
|
537
779
|
runResults.push(result);
|
|
538
780
|
} catch (err) {
|
|
539
781
|
runFailure = err instanceof Error ? err : new Error(String(err));
|
|
@@ -565,10 +807,21 @@ async function runEvalCommand(args) {
|
|
|
565
807
|
const summary = aggregateRuns(runResults);
|
|
566
808
|
console.log("\n" + formatMultiRunTable(summary) + "\n");
|
|
567
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++;
|
|
568
817
|
} else {
|
|
569
818
|
const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
|
|
819
|
+
stampFiltered(result);
|
|
570
820
|
results.push(result);
|
|
571
821
|
console.log("\n" + formatTable(result) + "\n");
|
|
822
|
+
const wipeout = reportTotalWipeout(result, filePath);
|
|
823
|
+
const degraded = reportDegraded(result, filePath);
|
|
824
|
+
if (wipeout || degraded) failedFiles++;
|
|
572
825
|
}
|
|
573
826
|
} catch (err) {
|
|
574
827
|
console.error(
|