@axlsdk/eval 0.16.1 → 0.17.1

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/dist/cli.cjs CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_node_fs2 = require("fs");
27
+ var import_node_fs = require("fs");
28
28
  var import_promises = require("fs/promises");
29
29
  var path = __toESM(require("path"), 1);
30
30
 
@@ -114,13 +114,16 @@ function resolveThreshold(scorerName, options, metadata) {
114
114
  return LEGACY_THRESHOLD;
115
115
  }
116
116
  function evalCompare(baseline, candidate, options) {
117
- const baselineRuns = Array.isArray(baseline) ? baseline : [baseline];
118
- const candidateRuns = Array.isArray(candidate) ? candidate : [candidate];
119
- if (baselineRuns.length === 0 || candidateRuns.length === 0) {
117
+ const baselineRunsRaw = Array.isArray(baseline) ? baseline : [baseline];
118
+ const candidateRunsRaw = Array.isArray(candidate) ? candidate : [candidate];
119
+ if (baselineRunsRaw.length === 0 || candidateRunsRaw.length === 0) {
120
120
  throw new Error("Cannot compare empty eval result arrays");
121
121
  }
122
- const baselineRef = baselineRuns[0];
123
- const candidateRef = candidateRuns[0];
122
+ const runCount = Math.min(baselineRunsRaw.length, candidateRunsRaw.length);
123
+ const baselineRuns = baselineRunsRaw.slice(0, runCount);
124
+ const candidateRuns = candidateRunsRaw.slice(0, runCount);
125
+ const baselineRef = baselineRunsRaw[0];
126
+ const candidateRef = candidateRunsRaw[0];
124
127
  if (baselineRef.dataset !== candidateRef.dataset) {
125
128
  throw new Error(
126
129
  `Cannot compare evals from different datasets: "${baselineRef.dataset}" vs "${candidateRef.dataset}"`
@@ -135,7 +138,6 @@ function evalCompare(baseline, candidate, options) {
135
138
  for (const name of baselineScorerNames) {
136
139
  pairedDiffs[name] = [];
137
140
  }
138
- const runCount = Math.min(baselineRuns.length, candidateRuns.length);
139
141
  for (let r = 0; r < runCount; r++) {
140
142
  const bRun = baselineRuns[r];
141
143
  const cRun = candidateRuns[r];
@@ -272,9 +274,21 @@ function evalCompare(baseline, candidate, options) {
272
274
  parts.push(`${Math.abs(cost.deltaPercent).toFixed(0)}% ${dir}`);
273
275
  }
274
276
  const summaryStr = `candidate ${parts.length > 0 ? parts.join(", ") : "no meaningful changes"} with ${regressions.length} regressions and ${improvements.length} improvements`;
277
+ const baselinePartial = detectPartial(baselineRunsRaw);
278
+ const candidatePartial = detectPartial(candidateRunsRaw);
275
279
  return {
276
- baseline: { id: baselineRef.id, metadata: baselineRef.metadata },
277
- candidate: { id: candidateRef.id, metadata: candidateRef.metadata },
280
+ baseline: {
281
+ id: baselineRef.id,
282
+ metadata: baselineRef.metadata,
283
+ runCount,
284
+ ...baselinePartial ? { partial: baselinePartial } : {}
285
+ },
286
+ candidate: {
287
+ id: candidateRef.id,
288
+ metadata: candidateRef.metadata,
289
+ runCount,
290
+ ...candidatePartial ? { partial: candidatePartial } : {}
291
+ },
278
292
  scorers,
279
293
  timing,
280
294
  cost,
@@ -283,6 +297,20 @@ function evalCompare(baseline, candidate, options) {
283
297
  summary: summaryStr
284
298
  };
285
299
  }
300
+ function detectPartial(runs) {
301
+ if (runs.length === 0) return void 0;
302
+ let attempted;
303
+ for (const r of runs) {
304
+ const v = r.metadata?.batchAttempted;
305
+ if (typeof v === "number" && Number.isFinite(v)) {
306
+ attempted = v;
307
+ break;
308
+ }
309
+ }
310
+ if (attempted === void 0) return void 0;
311
+ if (runs.length >= attempted) return void 0;
312
+ return { completed: runs.length, attempted };
313
+ }
286
314
 
287
315
  // src/scorer.ts
288
316
  function normalizeScorerResult(result) {
@@ -786,66 +814,43 @@ function aggregateRuns(runs) {
786
814
  }
787
815
 
788
816
  // src/cli-utils.ts
789
- var import_node_path = require("path");
790
- var import_node_fs = require("fs");
791
- var import_node_url = require("url");
792
- var CONFIG_CANDIDATES = [
793
- "axl.config.mts",
794
- "axl.config.ts",
795
- "axl.config.mjs",
796
- "axl.config.js"
797
- ];
798
- function findConfig(cwd) {
799
- for (const name of CONFIG_CANDIDATES) {
800
- const p = (0, import_node_path.resolve)(cwd, name);
801
- if ((0, import_node_fs.existsSync)(p)) return p;
817
+ var import_axl = require("@axlsdk/axl");
818
+
819
+ // src/cli-validate.ts
820
+ function validateEvalConfig(cfg) {
821
+ if (!cfg || typeof cfg !== "object") {
822
+ return `does not export a valid eval config \u2014 got ${cfg === null ? "null" : typeof cfg}.`;
823
+ }
824
+ const c = cfg;
825
+ const missing = [];
826
+ if (!c.workflow) missing.push("workflow");
827
+ if (!c.dataset) missing.push("dataset");
828
+ if (!c.scorers) missing.push("scorers");
829
+ if (missing.length > 0) {
830
+ const keys = Object.keys(c).slice(0, 10);
831
+ const got = keys.length > 0 ? ` Got: { ${keys.join(", ")} }.` : "";
832
+ return `does not export a valid eval config (missing ${missing.join(", ")}).${got}`;
833
+ }
834
+ if (typeof c.workflow !== "string") {
835
+ return `exports a non-string workflow (got ${typeof c.workflow}) \u2014 workflow must be the registered workflow name.`;
836
+ }
837
+ if (typeof c.dataset !== "object" || Array.isArray(c.dataset)) {
838
+ return `exports a non-object dataset (got ${Array.isArray(c.dataset) ? "array" : typeof c.dataset}) \u2014 use the dataset() factory.`;
839
+ }
840
+ if (typeof c.dataset.getItems !== "function") {
841
+ return `exports a dataset without a getItems() method \u2014 use the dataset() factory.`;
842
+ }
843
+ if (!Array.isArray(c.scorers) || c.scorers.length === 0) {
844
+ return `exports an empty scorers array \u2014 at least one scorer is required.`;
845
+ }
846
+ const badScorerIdx = c.scorers.findIndex(
847
+ (s) => !s || typeof s.score !== "function"
848
+ );
849
+ if (badScorerIdx !== -1) {
850
+ 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).`;
802
851
  }
803
852
  return void 0;
804
853
  }
805
- function needsTsxLoader(filePath) {
806
- return /\.[mc]?tsx?$/.test(filePath);
807
- }
808
- function resolveRuntime(mod) {
809
- const def = mod.default;
810
- return def?.default ?? def ?? mod.runtime;
811
- }
812
- var tsImportFn;
813
- async function importModule(filePath, parentURL) {
814
- if (needsTsxLoader(filePath)) {
815
- if (tsImportFn === void 0) {
816
- try {
817
- const mod = await import("tsx/esm/api");
818
- tsImportFn = mod.tsImport ?? null;
819
- } catch {
820
- tsImportFn = null;
821
- console.warn(
822
- "[axl-eval] Warning: tsx is not installed. TypeScript files require tsx.\n Install it with: npm install -D tsx"
823
- );
824
- }
825
- }
826
- if (tsImportFn) {
827
- return await tsImportFn((0, import_node_url.pathToFileURL)(filePath).href, parentURL);
828
- }
829
- }
830
- return await import((0, import_node_url.pathToFileURL)(filePath).href);
831
- }
832
- async function registerConditions(conditions) {
833
- try {
834
- const nodeModule = await import("module");
835
- const hookCode = [
836
- `const extra = ${JSON.stringify(conditions)};`,
837
- `export async function resolve(specifier, context, nextResolve) {`,
838
- ` return nextResolve(specifier, {`,
839
- ` ...context,`,
840
- ` conditions: [...new Set([...context.conditions, ...extra])],`,
841
- ` });`,
842
- `}`
843
- ].join("\n");
844
- nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
845
- } catch {
846
- console.warn("[axl-eval] Warning: --conditions requires Node.js 20.6+");
847
- }
848
- }
849
854
 
850
855
  // src/cli.ts
851
856
  var import_meta = {};
@@ -878,7 +883,7 @@ Usage:
878
883
  axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
879
884
 
880
885
  Config auto-detection (when --config is not specified):
881
- ${CONFIG_CANDIDATES.join(" -> ")}
886
+ ${import_axl.CONFIG_CANDIDATES.join(" -> ")}
882
887
 
883
888
  When a config is found, the exported AxlRuntime is passed to executeWorkflow
884
889
  and cost is tracked automatically via runtime.trackCost().
@@ -1000,10 +1005,16 @@ async function runRescore(args) {
1000
1005
  const results = Array.isArray(raw) ? raw : [raw];
1001
1006
  const runtime = await getRuntime(configArg, conditions);
1002
1007
  try {
1003
- const mod = await importModule(path.resolve(evalFilePath), import_meta.url);
1004
- const evalConfig = mod.default?.default ?? mod.default ?? mod.config ?? mod;
1005
- if (!evalConfig.scorers || evalConfig.scorers.length === 0) {
1006
- console.error(`Error: ${evalFilePath} does not export scorers`);
1008
+ const mod = await (0, import_axl.importModule)(path.resolve(evalFilePath), import_meta.url);
1009
+ const evalConfig = (0, import_axl.pickDefault)(mod);
1010
+ if (!evalConfig || typeof evalConfig !== "object") {
1011
+ console.error(
1012
+ `Error: ${evalFilePath} does not export an eval config object \u2014 got ${evalConfig === null ? "null" : typeof evalConfig}.`
1013
+ );
1014
+ process.exit(1);
1015
+ }
1016
+ if (!Array.isArray(evalConfig.scorers) || evalConfig.scorers.length === 0) {
1017
+ console.error(`Error: ${evalFilePath} does not export a non-empty scorers array.`);
1007
1018
  process.exit(1);
1008
1019
  }
1009
1020
  const rescored = [];
@@ -1026,11 +1037,12 @@ async function runRescore(args) {
1026
1037
  }
1027
1038
  }
1028
1039
  function collectEvalFiles(p) {
1040
+ if (p.includes("*")) return (0, import_axl.expandGlob)(p, process.cwd());
1029
1041
  const resolved = path.resolve(p);
1030
1042
  try {
1031
- const stat = (0, import_node_fs2.statSync)(resolved);
1043
+ const stat = (0, import_node_fs.statSync)(resolved);
1032
1044
  if (stat.isDirectory()) {
1033
- const entries = (0, import_node_fs2.readdirSync)(resolved);
1045
+ const entries = (0, import_node_fs.readdirSync)(resolved);
1034
1046
  return entries.filter((e) => /\.eval\.[mc]?[jt]sx?$/.test(e)).map((e) => path.join(resolved, e));
1035
1047
  }
1036
1048
  return [resolved];
@@ -1129,8 +1141,8 @@ function formatMultiRunTable(summary) {
1129
1141
  }
1130
1142
  async function resolveRuntimeFromConfig(configPath) {
1131
1143
  try {
1132
- const mod = await importModule(configPath, import_meta.url);
1133
- const runtime = resolveRuntime(mod);
1144
+ const mod = await (0, import_axl.importModule)(configPath, import_meta.url);
1145
+ const runtime = (0, import_axl.resolveRuntime)(mod);
1134
1146
  if (!runtime || typeof runtime.execute !== "function") {
1135
1147
  console.error(`Config must export a default AxlRuntime instance.`);
1136
1148
  if (runtime) {
@@ -1167,11 +1179,11 @@ async function resolveRuntimeFromConfig(configPath) {
1167
1179
  }
1168
1180
  async function getRuntime(configArg, conditions) {
1169
1181
  if (conditions && conditions.length > 0) {
1170
- await registerConditions(conditions);
1182
+ await (0, import_axl.registerConditions)(conditions);
1171
1183
  }
1172
1184
  if (configArg) {
1173
1185
  const configPath = path.resolve(process.cwd(), configArg);
1174
- const stat = (0, import_node_fs2.statSync)(configPath, { throwIfNoEntry: false });
1186
+ const stat = (0, import_node_fs.statSync)(configPath, { throwIfNoEntry: false });
1175
1187
  if (!stat?.isFile()) {
1176
1188
  console.error(`Config file not found: ${configPath}`);
1177
1189
  process.exit(1);
@@ -1179,7 +1191,7 @@ async function getRuntime(configArg, conditions) {
1179
1191
  console.error(`[axl-eval] Loading config from ${configPath}`);
1180
1192
  return resolveRuntimeFromConfig(configPath);
1181
1193
  }
1182
- const found = findConfig(process.cwd());
1194
+ const found = (0, import_axl.findConfig)(process.cwd());
1183
1195
  if (found) {
1184
1196
  console.error(`[axl-eval] Auto-detected config: ${found}`);
1185
1197
  return resolveRuntimeFromConfig(found);
@@ -1236,26 +1248,38 @@ async function runEvalCommand(args) {
1236
1248
  }
1237
1249
  const runtime = await getRuntime(configArg, conditions);
1238
1250
  const results = [];
1251
+ let failedFiles = 0;
1239
1252
  try {
1240
1253
  for (const filePath of evalFiles) {
1241
1254
  try {
1242
- const mod = await importModule(path.resolve(filePath), import_meta.url);
1243
- const evalConfig = mod.default?.default ?? mod.default ?? mod.config ?? mod;
1244
- if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {
1255
+ const mod = await (0, import_axl.importModule)(path.resolve(filePath), import_meta.url);
1256
+ const evalConfig = (0, import_axl.pickDefault)(mod);
1257
+ const validationError = validateEvalConfig(evalConfig);
1258
+ if (validationError) {
1259
+ console.error(`Error: ${filePath} ${validationError}`);
1260
+ failedFiles++;
1261
+ continue;
1262
+ }
1263
+ const exported = (0, import_axl.pickExport)(mod, "executeWorkflow");
1264
+ let customExecute;
1265
+ if (typeof exported === "function") {
1266
+ customExecute = exported;
1267
+ } else if (exported !== void 0) {
1245
1268
  console.error(
1246
- `Error: ${filePath} does not export a valid eval config (missing workflow, dataset, or scorers)`
1269
+ `[axl-eval] Error: ${filePath} exports executeWorkflow but it is ${typeof exported}, not a function.`
1247
1270
  );
1271
+ failedFiles++;
1248
1272
  continue;
1249
1273
  }
1250
1274
  let executeWorkflow;
1251
- if (mod.executeWorkflow) {
1275
+ if (customExecute) {
1252
1276
  executeWorkflow = async (input, rt) => {
1253
1277
  const {
1254
1278
  result,
1255
1279
  cost: trackedCost,
1256
1280
  metadata
1257
1281
  } = await runtime.trackExecution(async () => {
1258
- return mod.executeWorkflow(input, rt);
1282
+ return customExecute(input, rt);
1259
1283
  });
1260
1284
  return {
1261
1285
  output: result.output,
@@ -1271,26 +1295,69 @@ async function runEvalCommand(args) {
1271
1295
  return { output: result, cost, metadata };
1272
1296
  };
1273
1297
  } else {
1274
- console.warn(
1275
- `[axl-eval] Warning: ${filePath} does not export executeWorkflow \u2014 using input passthrough`
1298
+ const exportShape = (() => {
1299
+ const top = Object.keys(mod).filter((k) => k !== "default");
1300
+ const fromDefault = mod.default && typeof mod.default === "object" ? Object.keys(mod.default) : [];
1301
+ const all = [.../* @__PURE__ */ new Set([...top, ...fromDefault])];
1302
+ return all.length > 0 ? `Found exports: [${all.join(", ")}].` : "";
1303
+ })();
1304
+ console.error(
1305
+ `[axl-eval] Error: ${filePath} does not export an executeWorkflow function and no workflow named "${evalConfig.workflow}" is registered on the runtime.
1306
+ ` + (exportShape ? ` ${exportShape}
1307
+ ` : "") + ` Add either:
1308
+ export async function executeWorkflow(input, runtime) { ... }
1309
+ or register the workflow on your runtime via runtime.registerWorkflow(workflow).
1310
+ Tip: if your eval previously worked, your package.json may be missing
1311
+ "type": "module" \u2014 tsx loads .ts as CJS, putting named exports on
1312
+ mod.default.<name>. The CLI handles that interop, but if you renamed
1313
+ or removed the export the file path is your fix.`
1276
1314
  );
1277
- executeWorkflow = async (input) => ({ output: input });
1315
+ failedFiles++;
1316
+ continue;
1278
1317
  }
1279
1318
  const runOptions = captureTraces ? { captureTraces: true } : void 0;
1280
1319
  if (runs > 1) {
1281
1320
  const { randomUUID: randomUUID4 } = await import("crypto");
1282
1321
  const runGroupId = randomUUID4();
1283
1322
  const runResults = [];
1323
+ let runFailure;
1284
1324
  for (let r = 0; r < runs; r++) {
1285
1325
  console.error(`[axl-eval] Run ${r + 1}/${runs}...`);
1286
- const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1287
- result.metadata.runGroupId = runGroupId;
1288
- result.metadata.runIndex = r;
1289
- runResults.push(result);
1290
- results.push(result);
1326
+ try {
1327
+ const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1328
+ result.metadata.runGroupId = runGroupId;
1329
+ result.metadata.runIndex = r;
1330
+ runResults.push(result);
1331
+ } catch (err) {
1332
+ runFailure = err instanceof Error ? err : new Error(String(err));
1333
+ console.error(`[axl-eval] Run ${r + 1}/${runs} failed: ${runFailure.message}`);
1334
+ break;
1335
+ }
1336
+ }
1337
+ if (runResults.length === 0) {
1338
+ console.error(
1339
+ `[axl-eval] Aborting ${filePath}: no runs completed (${runFailure?.message ?? "unknown failure"}).`
1340
+ );
1341
+ failedFiles++;
1342
+ continue;
1343
+ }
1344
+ const partial = runResults.length < runs;
1345
+ if (partial) {
1346
+ const failureMsg = runFailure ? runFailure.message || String(runFailure) || void 0 : void 0;
1347
+ for (const r of runResults) {
1348
+ r.metadata.fromPartialBatch = true;
1349
+ r.metadata.batchCompleted = runResults.length;
1350
+ r.metadata.batchAttempted = runs;
1351
+ if (failureMsg) r.metadata.batchFailure = failureMsg;
1352
+ }
1353
+ console.error(
1354
+ `[axl-eval] PARTIAL: ${runResults.length} of ${runs} runs completed for ${filePath}.`
1355
+ );
1356
+ failedFiles++;
1291
1357
  }
1292
1358
  const summary = aggregateRuns(runResults);
1293
1359
  console.log("\n" + formatMultiRunTable(summary) + "\n");
1360
+ for (const r of runResults) results.push(r);
1294
1361
  } else {
1295
1362
  const result = await runEval(evalConfig, executeWorkflow, runtime, runOptions);
1296
1363
  results.push(result);
@@ -1300,6 +1367,7 @@ async function runEvalCommand(args) {
1300
1367
  console.error(
1301
1368
  `Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
1302
1369
  );
1370
+ failedFiles++;
1303
1371
  }
1304
1372
  }
1305
1373
  if (outputPath && results.length > 0) {
@@ -1313,7 +1381,7 @@ async function runEvalCommand(args) {
1313
1381
  await runtime.shutdown().catch(() => {
1314
1382
  });
1315
1383
  }
1316
- if (results.length === 0) {
1384
+ if (failedFiles > 0 || results.length === 0) {
1317
1385
  process.exit(1);
1318
1386
  }
1319
1387
  }