@axlsdk/eval 0.10.3 → 0.11.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 CHANGED
@@ -85,7 +85,7 @@ const qualityJudge = llmScorer({
85
85
  });
86
86
  ```
87
87
 
88
- ### `runEval(config, executeFn, provider?, runtime?)`
88
+ ### `runEval(config, executeFn, provider, runtime)`
89
89
 
90
90
  Run an evaluation:
91
91
 
@@ -103,6 +103,8 @@ const results = await runEval(
103
103
  const output = await runtime.execute('my-workflow', input);
104
104
  return { output };
105
105
  },
106
+ undefined, // provider (only needed for llmScorer)
107
+ runtime,
106
108
  );
107
109
 
108
110
  console.log(results.summary);
@@ -158,11 +160,11 @@ export default defineEval({
158
160
  });
159
161
  ```
160
162
 
161
- By default, the eval runner calls `runtime.execute(workflow, input)` for each dataset item, which requires the workflow to be registered on the runtime.
163
+ When running via `runtime.eval()` or `runtime.runRegisteredEval()`, the runner calls `runtime.execute(workflow, input)` by default, which requires the workflow to be registered on the runtime.
162
164
 
163
- For self-contained evals, export an `executeWorkflow` function alongside the config. This is called instead of `runtime.execute()` for each dataset item.
165
+ When running via the CLI, the eval runner resolves the execution function in order: (1) the exported `executeWorkflow` function, (2) `runtime.execute(workflow, input)` if the workflow is registered on the runtime, or (3) a passthrough that returns the input as the output (with a warning).
164
166
 
165
- The function receives `(input, runtime?)` — the second argument is the `AxlRuntime` instance when running via Studio or `runtime.runRegisteredEval()`. It is `undefined` when running via the CLI. Use it to call agents without needing a registered workflow:
167
+ The function receives `(input, runtime)` — the second argument is the `AxlRuntime` instance, always provided by the CLI, Studio, and `runtime.runRegisteredEval()`. Use it to call agents without needing a registered workflow:
166
168
 
167
169
  ```typescript
168
170
  // evals/qa-quality.eval.ts — calls an agent directly via the runtime
@@ -192,15 +194,14 @@ export default defineEval({
192
194
  scorers: [notEmpty],
193
195
  });
194
196
 
195
- export async function executeWorkflow(input: { question: string }, runtime?: AxlRuntime) {
196
- if (!runtime) throw new Error('This eval requires a runtime — run via Studio or runtime.runRegisteredEval()');
197
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
197
198
  const ctx = runtime.createContext();
198
199
  const output = await ctx.ask(qaAgent, input.question);
199
- return { output };
200
+ return { output }; // cost tracked automatically via runtime.trackCost()
200
201
  }
201
202
  ```
202
203
 
203
- When no runtime is needed (e.g., testing a pure function), just ignore the second argument — these evals work from both the CLI and Studio:
204
+ When no runtime is needed (e.g., testing a pure function), omit the runtime parameter — these evals work from both the CLI and Studio:
204
205
 
205
206
  ```typescript
206
207
  // evals/parser.eval.ts — tests a pure function, no runtime needed
@@ -210,9 +211,81 @@ export async function executeWorkflow(input: { raw: string }) {
210
211
  }
211
212
  ```
212
213
 
213
- > **Note:** The CLI (`npx axl eval`) does not provide a runtime to `executeWorkflow`. Eval files that call agents via `runtime.createContext()` must be run through Studio or `runtime.runRegisteredEval()`.
214
+ > **Note:** The CLI always provides a runtime to `executeWorkflow`. By default it auto-detects `axl.config.*` in the working directory or falls back to a bare `AxlRuntime` (providers resolved from environment variables). Use `--config` for explicit config files.
215
+
216
+ ### Cost Tracking
217
+
218
+ Cost is tracked automatically. You don't need to return `cost` from your `executeWorkflow` — the eval runner wraps each item with `runtime.trackCost()`, which captures cost from all `createContext()` and `execute()` calls.
219
+
220
+ ```typescript
221
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
222
+ const ctx = runtime.createContext();
223
+ const output = await ctx.ask(myAgent, input.question);
224
+ return { output }; // cost captured automatically
225
+ }
226
+ ```
227
+
228
+ To override the automatic cost (e.g., to exclude setup calls), return it explicitly:
229
+
230
+ ```typescript
231
+ return { output, cost: ctx.totalCost };
232
+ ```
233
+
234
+ You can also read `ctx.totalCost` at any point to inspect accumulated cost.
235
+
236
+ ### Common Patterns
237
+
238
+ **Budget per item** — cap cost to prevent runaway evals:
239
+
240
+ ```typescript
241
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
242
+ const ctx = runtime.createContext({ budget: '$0.50' });
243
+ const output = await ctx.ask(myAgent, input.question);
244
+ return { output };
245
+ }
246
+ ```
247
+
248
+ **Timeout per item** — cancel items that take too long:
249
+
250
+ ```typescript
251
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
252
+ const controller = new AbortController();
253
+ setTimeout(() => controller.abort(), 30_000);
254
+ const ctx = runtime.createContext({ signal: controller.signal });
255
+ const output = await ctx.ask(myAgent, input.question);
256
+ return { output };
257
+ }
258
+ ```
259
+
260
+ **Auto-approve tools** — when testing agents that have tools with `requireApproval`:
261
+
262
+ ```typescript
263
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
264
+ const ctx = runtime.createContext({
265
+ awaitHumanHandler: async () => ({ approved: true }),
266
+ });
267
+ const output = await ctx.ask(myAgent, input.question);
268
+ return { output };
269
+ }
270
+ ```
214
271
 
215
- > **Cost tracking:** The default path (no `executeWorkflow` export) tracks cost automatically via runtime trace events. Custom `executeWorkflow` functions must return `{ output, cost }` manually if cost tracking is needed — `createContext()` does not capture cost automatically.
272
+ **Multi-turn evaluation** — test follow-up responses with conversation history:
273
+
274
+ ```typescript
275
+ export async function executeWorkflow(
276
+ input: { setupPrompt: string; setupResponse: string; followUp: string },
277
+ runtime: AxlRuntime,
278
+ ) {
279
+ const ctx = runtime.createContext({
280
+ sessionHistory: [
281
+ { role: 'user', content: input.setupPrompt },
282
+ { role: 'assistant', content: input.setupResponse },
283
+ ],
284
+ });
285
+ const output = await ctx.ask(myAgent, input.followUp);
286
+ return { output };
287
+ }
288
+ ```
216
289
 
217
290
  ## CLI
218
291
 
@@ -220,18 +293,39 @@ Run evaluations from the command line:
220
293
 
221
294
  ```bash
222
295
  # Run an eval file
223
- npx axl eval ./evals/math.eval.ts
296
+ npx axl-eval ./evals/math.eval.ts
224
297
 
225
298
  # Run all evals in a directory
226
- npx axl eval ./evals/
299
+ npx axl-eval ./evals/
227
300
 
228
301
  # Save results to a file
229
- npx axl eval ./evals/math.eval.ts --output ./results/baseline.json
302
+ npx axl-eval ./evals/math.eval.ts --output ./results/baseline.json
303
+
304
+ # Use a config file for runtime access
305
+ npx axl-eval ./evals/ --config ./axl.config.ts
306
+
307
+ # Use custom import conditions (monorepo source exports)
308
+ npx axl-eval ./evals/ --config ./axl.config.ts --conditions development
230
309
 
231
310
  # Compare two results
232
- npx axl eval compare ./results/baseline.json ./results/candidate.json
311
+ npx axl-eval compare ./results/baseline.json ./results/candidate.json
312
+
313
+ # Fail CI if regressions detected
314
+ npx axl-eval compare baseline.json candidate.json --fail-on-regression
233
315
  ```
234
316
 
317
+ TypeScript config and eval files require [tsx](https://github.com/privatenumber/tsx) as a dev dependency (`npm install -D tsx`).
318
+
319
+ ### Runtime Resolution
320
+
321
+ The CLI resolves an `AxlRuntime` to pass to `executeWorkflow`:
322
+
323
+ 1. **`--config <path>`** — use an explicit config file
324
+ 2. **Auto-detect** — search for `axl.config.mts` → `.ts` → `.mjs` → `.js` in cwd
325
+ 3. **Fallback** — create a bare `new AxlRuntime()` (providers resolve from environment variables)
326
+
327
+ When a runtime is available, each `executeWorkflow` call is wrapped with `runtime.trackCost()` for automatic cost attribution.
328
+
235
329
  ## Studio Integration
236
330
 
237
331
  Eval files can also be lazy-loaded by the Studio middleware, enabling the Eval Runner panel without static imports that would create circular dependencies:
@@ -195,4 +195,4 @@ export {
195
195
  evalCompare,
196
196
  runEval
197
197
  };
198
- //# sourceMappingURL=chunk-LMQJNZWT.js.map
198
+ //# sourceMappingURL=chunk-D7KCUS6H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/compare.ts","../src/runner.ts"],"sourcesContent":["import type { EvalResult, EvalComparison, EvalRegression, EvalImprovement } from './types.js';\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\nexport function evalCompare(baseline: EvalResult, candidate: EvalResult): EvalComparison {\n if (baseline.dataset !== candidate.dataset) {\n throw new Error(\n `Cannot compare evals from different datasets: \"${baseline.dataset}\" vs \"${candidate.dataset}\"`,\n );\n }\n\n const baselineScorerNames = Object.keys(baseline.summary.scorers).sort();\n const candidateScorerNames = Object.keys(candidate.summary.scorers).sort();\n if (JSON.stringify(baselineScorerNames) !== JSON.stringify(candidateScorerNames)) {\n throw new Error(`Cannot compare evals with different scorers`);\n }\n\n const scorers: EvalComparison['scorers'] = {};\n for (const name of baselineScorerNames) {\n const bMean = baseline.summary.scorers[name].mean;\n const cMean = candidate.summary.scorers[name].mean;\n const delta = round(cMean - bMean);\n const deltaPercent = bMean > 0 ? round((delta / bMean) * 100) : 0;\n scorers[name] = { baselineMean: bMean, candidateMean: cMean, delta, deltaPercent };\n }\n\n const regressions: EvalRegression[] = [];\n const improvements: EvalImprovement[] = [];\n\n const minLength = Math.min(baseline.items.length, candidate.items.length);\n for (let i = 0; i < minLength; i++) {\n const bItem = baseline.items[i];\n const cItem = candidate.items[i];\n if (bItem.error || cItem.error) continue;\n for (const name of baselineScorerNames) {\n const bScore = bItem.scores[name];\n const cScore = cItem.scores[name];\n if (bScore === undefined || cScore === undefined || bScore < 0 || cScore < 0) continue;\n const delta = round(cScore - bScore);\n if (delta < -0.1)\n regressions.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n else if (delta > 0.1)\n improvements.push({\n input: bItem.input,\n scorer: name,\n baselineScore: bScore,\n candidateScore: cScore,\n delta,\n });\n }\n }\n\n const parts: string[] = [];\n for (const name of baselineScorerNames) {\n const s = scorers[name];\n if (Math.abs(s.delta) > 0.001) {\n const direction = s.delta > 0 ? 'improves' : 'regresses';\n const sign = s.delta > 0 ? '+' : '';\n parts.push(\n `'${name}' ${direction} by ${sign}${s.deltaPercent.toFixed(1)}% (${s.baselineMean.toFixed(2)} -> ${s.candidateMean.toFixed(2)})`,\n );\n }\n }\n const summaryStr = `candidate ${parts.length > 0 ? parts.join(', ') : 'no significant changes'} with ${regressions.length} regressions and ${improvements.length} improvements`;\n\n return {\n baseline: { id: baseline.id, metadata: baseline.metadata },\n candidate: { id: candidate.id, metadata: candidate.metadata },\n scorers,\n regressions,\n improvements,\n summary: summaryStr,\n };\n}\n","import type { AxlRuntime } from '@axlsdk/axl';\nimport type { EvalConfig, EvalResult, EvalItem, EvalSummary } from './types.js';\nimport { randomUUID } from 'node:crypto';\n\nfunction computeStats(scores: number[]): {\n mean: number;\n min: number;\n max: number;\n p50: number;\n p95: number;\n} {\n if (scores.length === 0) return { mean: 0, min: 0, max: 0, p50: 0, p95: 0 };\n const sorted = [...scores].sort((a, b) => a - b);\n const mean = sorted.reduce((a, b) => a + b, 0) / sorted.length;\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n const p50 = sorted[Math.floor(sorted.length * 0.5)];\n const p95 = sorted[Math.floor(sorted.length * 0.95)];\n return { mean: round(mean), min: round(min), max: round(max), p50: round(p50), p95: round(p95) };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\nfunction parseCost(cost: string): number {\n const match = cost.match(/^\\$?([\\d.]+)$/);\n if (!match) throw new Error(`Invalid cost format: \"${cost}\"`);\n return parseFloat(match[1]);\n}\n\n/** Minimal provider interface for LLM scoring (avoids depending on core axl package). */\ntype LlmScorerProvider = {\n chat(\n messages: { role: string; content: string }[],\n options: { model: string; temperature?: number },\n ): Promise<{ content: string }>;\n};\n\nexport async function runEval(\n config: EvalConfig,\n executeWorkflow: (\n input: unknown,\n runtime: AxlRuntime,\n ) => Promise<{ output: unknown; cost?: number }>,\n provider: LlmScorerProvider | undefined,\n runtime: AxlRuntime,\n): Promise<EvalResult> {\n const startTime = Date.now();\n const id = randomUUID();\n const items = await config.dataset.getItems();\n const concurrency = config.concurrency ?? 5;\n const budgetLimit = config.budget ? parseCost(config.budget) : undefined;\n\n if (provider) {\n for (const scorer of config.scorers) {\n if (scorer.isLlm) {\n (scorer as unknown as { _provider: LlmScorerProvider })._provider = provider;\n }\n }\n }\n\n const evalItems: EvalItem[] = [];\n let totalCost = 0;\n let budgetExceeded = false;\n\n async function processItem(item: (typeof items)[0]): Promise<void> {\n if (budgetExceeded) {\n evalItems.push({\n input: item.input,\n annotations: item.annotations,\n output: null,\n error: 'Budget exceeded',\n scores: {},\n });\n return;\n }\n\n const evalItem: EvalItem = {\n input: item.input,\n annotations: item.annotations,\n output: null,\n scores: {},\n };\n try {\n const result = await executeWorkflow(item.input, runtime);\n evalItem.output = result.output;\n if (result.cost != null) {\n totalCost += result.cost;\n }\n } catch (err) {\n evalItem.error = err instanceof Error ? err.message : String(err);\n evalItems.push(evalItem);\n return;\n }\n\n if (budgetLimit != null && totalCost > budgetLimit) {\n budgetExceeded = true;\n }\n\n for (const scorer of config.scorers) {\n try {\n const score = await scorer.score(evalItem.output, item.input, item.annotations);\n if (score < 0 || score > 1) {\n if (!evalItem.errors) evalItem.errors = [];\n evalItem.errors.push(\n `Scorer \"${scorer.name}\" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`,\n );\n evalItem.scores[scorer.name] = -1;\n } else {\n evalItem.scores[scorer.name] = round(score);\n }\n } catch (err) {\n if (!evalItem.errors) evalItem.errors = [];\n evalItem.errors.push(\n `Scorer \"${scorer.name}\" threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n evalItem.scores[scorer.name] = -1;\n }\n }\n evalItems.push(evalItem);\n }\n\n let index = 0;\n async function runNext(): Promise<void> {\n while (index < items.length) {\n const currentIndex = index++;\n await processItem(items[currentIndex]);\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => runNext());\n await Promise.all(workers);\n\n const failures = evalItems.filter((i) => i.error).length;\n const scorerNames = config.scorers.map((s) => s.name);\n const scorerStats: EvalSummary['scorers'] = {};\n for (const name of scorerNames) {\n const scores = evalItems\n .filter((i) => !i.error && i.scores[name] !== undefined && i.scores[name] >= 0)\n .map((i) => i.scores[name]);\n scorerStats[name] = computeStats(scores);\n }\n\n return {\n id,\n workflow: config.workflow,\n dataset: config.dataset.name,\n metadata: config.metadata ?? {},\n timestamp: new Date().toISOString(),\n totalCost,\n duration: Date.now() - startTime,\n items: evalItems,\n summary: { count: items.length, failures, scorers: scorerStats },\n };\n}\n"],"mappings":"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","names":["round"]}
package/dist/cli.cjs CHANGED
@@ -24,9 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_node_fs = require("fs");
27
+ var import_node_fs2 = require("fs");
28
28
  var import_promises = require("fs/promises");
29
29
  var path = __toESM(require("path"), 1);
30
+ var import_node_url2 = require("url");
30
31
 
31
32
  // src/compare.ts
32
33
  function round(n) {
@@ -221,16 +222,110 @@ async function runEval(config, executeWorkflow, provider, runtime) {
221
222
  };
222
223
  }
223
224
 
225
+ // src/cli-utils.ts
226
+ var import_node_path = require("path");
227
+ var import_node_fs = require("fs");
228
+ var import_node_url = require("url");
229
+ var CONFIG_CANDIDATES = [
230
+ "axl.config.mts",
231
+ "axl.config.ts",
232
+ "axl.config.mjs",
233
+ "axl.config.js"
234
+ ];
235
+ function findConfig(cwd) {
236
+ for (const name of CONFIG_CANDIDATES) {
237
+ const p = (0, import_node_path.resolve)(cwd, name);
238
+ if ((0, import_node_fs.existsSync)(p)) return p;
239
+ }
240
+ return void 0;
241
+ }
242
+ function needsEsmForcing(configPath) {
243
+ const ext = (0, import_node_path.extname)(configPath);
244
+ return ext === ".ts" || ext === ".tsx";
245
+ }
246
+ function needsTsxLoader(configPath) {
247
+ return /\.[mc]?tsx?$/.test(configPath);
248
+ }
249
+ function resolveRuntime(mod) {
250
+ const def = mod.default;
251
+ return def?.default ?? def ?? mod.runtime;
252
+ }
253
+ var tsxRegistered = false;
254
+ async function ensureTsxLoader() {
255
+ if (tsxRegistered) return;
256
+ tsxRegistered = true;
257
+ let loaded = false;
258
+ try {
259
+ const tsxEsm = await import("tsx/esm/api");
260
+ tsxEsm.register();
261
+ loaded = true;
262
+ } catch {
263
+ }
264
+ try {
265
+ const tsxCjs = await import("tsx/cjs/api");
266
+ tsxCjs.register();
267
+ loaded = true;
268
+ } catch {
269
+ }
270
+ if (!loaded) {
271
+ console.warn(
272
+ "[axl-eval] Warning: tsx is not installed. TypeScript files require tsx.\n Install it with: npm install -D tsx"
273
+ );
274
+ }
275
+ }
276
+ async function forceEsmForConfig(configPath) {
277
+ try {
278
+ const nodeModule = await import("module");
279
+ const configUrl = (0, import_node_url.pathToFileURL)(configPath).href;
280
+ const hookCode = [
281
+ `export async function resolve(specifier, context, nextResolve) {`,
282
+ ` const result = await nextResolve(specifier, context);`,
283
+ ` if (result.url === ${JSON.stringify(configUrl)}) result.format = 'module';`,
284
+ ` return result;`,
285
+ `}`
286
+ ].join("\n");
287
+ nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
288
+ } catch {
289
+ }
290
+ }
291
+ async function registerConditions(conditions) {
292
+ try {
293
+ const nodeModule = await import("module");
294
+ const hookCode = [
295
+ `const extra = ${JSON.stringify(conditions)};`,
296
+ `export async function resolve(specifier, context, nextResolve) {`,
297
+ ` return nextResolve(specifier, {`,
298
+ ` ...context,`,
299
+ ` conditions: [...new Set([...context.conditions, ...extra])],`,
300
+ ` });`,
301
+ `}`
302
+ ].join("\n");
303
+ nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
304
+ } catch {
305
+ console.warn("[axl-eval] Warning: --conditions requires Node.js 20.6+");
306
+ }
307
+ }
308
+
224
309
  // src/cli.ts
310
+ var KNOWN_FLAGS = /* @__PURE__ */ new Set(["--output", "--config", "--conditions", "--fail-on-regression"]);
225
311
  async function main() {
226
312
  const args = process.argv.slice(2);
227
- if (args.length === 0 || args[0] === "--help") {
313
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
228
314
  console.log(`
229
315
  Usage:
230
- npx axl eval <path> Run eval file(s)
231
- npx axl eval <path> --output <f> Run eval and save results to JSON
232
- npx axl eval compare <a> <b> Compare two eval result files
233
- npx axl eval compare <a> <b> --fail-on-regression Exit 1 if regressions
316
+ axl-eval <path> Run eval file(s)
317
+ axl-eval <path> --output <file> Save results to JSON
318
+ axl-eval <path> --config <file> Use config file for runtime
319
+ axl-eval <path> --conditions <list> Node.js import conditions (comma-separated)
320
+ axl-eval compare <a> <b> Compare two eval result files
321
+ axl-eval compare <a> <b> --fail-on-regression Exit 1 if regressions
322
+
323
+ Config auto-detection (when --config is not specified):
324
+ ${CONFIG_CANDIDATES.join(" -> ")}
325
+
326
+ When a config is found, the exported AxlRuntime is passed to executeWorkflow
327
+ and cost is tracked automatically via runtime.trackCost().
328
+ When no config is found, a bare AxlRuntime is created (providers from env vars).
234
329
  `);
235
330
  process.exit(0);
236
331
  }
@@ -244,7 +339,7 @@ async function runCompare(args) {
244
339
  const failOnRegression = args.includes("--fail-on-regression");
245
340
  const files = args.filter((a) => !a.startsWith("--"));
246
341
  if (files.length !== 2) {
247
- console.error("Usage: npx axl eval compare <baseline.json> <candidate.json>");
342
+ console.error("Usage: axl-eval compare <baseline.json> <candidate.json>");
248
343
  process.exit(1);
249
344
  }
250
345
  const baseline = JSON.parse(await (0, import_promises.readFile)(files[0], "utf-8"));
@@ -282,10 +377,10 @@ Compare: baseline (${comparison.baseline.id.slice(0, 8)}) -> candidate (${compar
282
377
  function collectEvalFiles(p) {
283
378
  const resolved = path.resolve(p);
284
379
  try {
285
- const stat = (0, import_node_fs.statSync)(resolved);
380
+ const stat = (0, import_node_fs2.statSync)(resolved);
286
381
  if (stat.isDirectory()) {
287
- const entries = (0, import_node_fs.readdirSync)(resolved);
288
- return entries.filter((e) => e.endsWith(".eval.ts") || e.endsWith(".eval.js")).map((e) => path.join(resolved, e));
382
+ const entries = (0, import_node_fs2.readdirSync)(resolved);
383
+ return entries.filter((e) => /\.eval\.[mc]?[jt]sx?$/.test(e)).map((e) => path.join(resolved, e));
289
384
  }
290
385
  return [resolved];
291
386
  } catch {
@@ -317,16 +412,104 @@ function formatTable(result) {
317
412
  );
318
413
  return lines.join("\n");
319
414
  }
320
- async function runEvalCommand(args) {
415
+ async function resolveRuntimeFromConfig(configPath) {
416
+ if (needsTsxLoader(configPath)) {
417
+ await ensureTsxLoader();
418
+ }
419
+ if (needsEsmForcing(configPath)) {
420
+ await forceEsmForConfig(configPath);
421
+ }
422
+ try {
423
+ const mod = await import((0, import_node_url2.pathToFileURL)(configPath).href);
424
+ const runtime = resolveRuntime(mod);
425
+ if (!runtime || typeof runtime.execute !== "function") {
426
+ console.error(`Config must export a default AxlRuntime instance.`);
427
+ if (runtime) {
428
+ const keys = Object.keys(runtime).slice(0, 5).join(", ");
429
+ console.error(` Got: ${typeof runtime}${keys ? ` with keys: { ${keys} }` : ""}`);
430
+ }
431
+ console.error(
432
+ `Example:
433
+ import { AxlRuntime } from '@axlsdk/axl';
434
+ export default new AxlRuntime({ ... });`
435
+ );
436
+ process.exit(1);
437
+ }
438
+ return runtime;
439
+ } catch (err) {
440
+ const msg = err instanceof Error ? err.message : String(err);
441
+ if (/Cannot use import statement|Unexpected reserved word|top-level await|exports is not defined/.test(
442
+ msg
443
+ )) {
444
+ const ext = path.extname(configPath);
445
+ console.error(`[axl-eval] Config failed to load due to a CJS/ESM compatibility issue.`);
446
+ if (ext === ".ts" || ext === ".tsx") {
447
+ const mtsPath = configPath.slice(0, -ext.length) + ".mts";
448
+ console.error(
449
+ ` Tip: rename to .mts to force ESM format:
450
+ mv ${configPath} ${mtsPath}`
451
+ );
452
+ } else {
453
+ console.error(` Tip: add "type": "module" to your package.json.`);
454
+ }
455
+ console.error();
456
+ }
457
+ console.error(`Failed to load config:`, err);
458
+ process.exit(1);
459
+ }
460
+ }
461
+ async function getRuntime(configArg, conditions) {
462
+ if (conditions && conditions.length > 0) {
463
+ await registerConditions(conditions);
464
+ }
465
+ if (configArg) {
466
+ const configPath = path.resolve(process.cwd(), configArg);
467
+ const stat = (0, import_node_fs2.statSync)(configPath, { throwIfNoEntry: false });
468
+ if (!stat?.isFile()) {
469
+ console.error(`Config file not found: ${configPath}`);
470
+ process.exit(1);
471
+ }
472
+ console.error(`[axl-eval] Loading config from ${configPath}`);
473
+ return resolveRuntimeFromConfig(configPath);
474
+ }
475
+ const found = findConfig(process.cwd());
476
+ if (found) {
477
+ console.error(`[axl-eval] Auto-detected config: ${found}`);
478
+ return resolveRuntimeFromConfig(found);
479
+ }
480
+ const { AxlRuntime } = await import("@axlsdk/axl");
481
+ return new AxlRuntime();
482
+ }
483
+ function parseEvalArgs(args) {
321
484
  let outputPath;
485
+ let configArg;
486
+ let conditions = [];
322
487
  const paths = [];
323
488
  for (let i = 0; i < args.length; i++) {
324
- if (args[i] === "--output" && i + 1 < args.length) {
325
- outputPath = args[++i];
326
- } else if (!args[i].startsWith("--")) {
327
- paths.push(args[i]);
489
+ const arg = args[i];
490
+ if (arg === "--output" || arg === "--config" || arg === "--conditions") {
491
+ if (i + 1 >= args.length) {
492
+ console.error(`Error: ${arg} requires a value`);
493
+ process.exit(1);
494
+ }
495
+ const value = args[++i];
496
+ if (arg === "--output") outputPath = value;
497
+ else if (arg === "--config") configArg = value;
498
+ else
499
+ conditions = value.split(",").map((c) => c.trim()).filter(Boolean);
500
+ } else if (arg.startsWith("--")) {
501
+ if (!KNOWN_FLAGS.has(arg)) {
502
+ console.error(`Unknown flag: ${arg}`);
503
+ process.exit(1);
504
+ }
505
+ } else {
506
+ paths.push(arg);
328
507
  }
329
508
  }
509
+ return { outputPath, configArg, conditions, paths };
510
+ }
511
+ async function runEvalCommand(args) {
512
+ const { outputPath, configArg, conditions, paths } = parseEvalArgs(args);
330
513
  if (paths.length === 0) {
331
514
  console.error("Error: No eval file path provided");
332
515
  process.exit(1);
@@ -339,33 +522,65 @@ async function runEvalCommand(args) {
339
522
  console.error("Error: No eval files found");
340
523
  process.exit(1);
341
524
  }
525
+ if (evalFiles.some((f) => /\.[mc]?tsx?$/.test(f))) {
526
+ await ensureTsxLoader();
527
+ }
528
+ const runtime = await getRuntime(configArg, conditions);
342
529
  const results = [];
343
- for (const filePath of evalFiles) {
344
- try {
345
- const mod = await import(path.resolve(filePath));
346
- const evalConfig = mod.default ?? mod.config ?? mod;
347
- if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {
530
+ try {
531
+ for (const filePath of evalFiles) {
532
+ try {
533
+ const mod = await import((0, import_node_url2.pathToFileURL)(path.resolve(filePath)).href);
534
+ const evalConfig = mod.default ?? mod.config ?? mod;
535
+ if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {
536
+ console.error(
537
+ `Error: ${filePath} does not export a valid eval config (missing workflow, dataset, or scorers)`
538
+ );
539
+ continue;
540
+ }
541
+ let executeWorkflow;
542
+ if (mod.executeWorkflow) {
543
+ executeWorkflow = async (input, rt) => {
544
+ const { result: result2, cost: trackedCost } = await runtime.trackCost(async () => {
545
+ return mod.executeWorkflow(input, rt);
546
+ });
547
+ return { output: result2.output, cost: result2.cost ?? trackedCost };
548
+ };
549
+ } else if (runtime.getWorkflow(evalConfig.workflow)) {
550
+ executeWorkflow = async (input) => {
551
+ const { result: result2, cost } = await runtime.trackCost(async () => {
552
+ return runtime.execute(evalConfig.workflow, input);
553
+ });
554
+ return { output: result2, cost };
555
+ };
556
+ } else {
557
+ console.warn(
558
+ `[axl-eval] Warning: ${filePath} does not export executeWorkflow \u2014 using input passthrough`
559
+ );
560
+ executeWorkflow = async (input) => ({ output: input });
561
+ }
562
+ const result = await runEval(evalConfig, executeWorkflow, mod.provider, runtime);
563
+ results.push(result);
564
+ console.log("\n" + formatTable(result) + "\n");
565
+ } catch (err) {
348
566
  console.error(
349
- `Error: ${filePath} does not export a valid eval config (missing workflow, dataset, or scorers)`
567
+ `Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
350
568
  );
351
- continue;
352
569
  }
353
- const executeWorkflow = mod.executeWorkflow ?? (async (input) => ({ output: input }));
354
- const result = await runEval(evalConfig, executeWorkflow, mod.provider);
355
- results.push(result);
356
- console.log("\n" + formatTable(result) + "\n");
357
- } catch (err) {
358
- console.error(
359
- `Error running eval ${filePath}: ${err instanceof Error ? err.message : String(err)}`
360
- );
361
570
  }
571
+ if (outputPath && results.length > 0) {
572
+ const output = results.length === 1 ? results[0] : results;
573
+ const outputDir = path.dirname(path.resolve(outputPath));
574
+ await (0, import_promises.mkdir)(outputDir, { recursive: true });
575
+ await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
576
+ console.log(`Results saved to ${outputPath}`);
577
+ }
578
+ } finally {
579
+ await runtime.shutdown().catch(() => {
580
+ });
362
581
  }
363
- if (outputPath && results.length > 0) {
364
- const output = results.length === 1 ? results[0] : results;
365
- const outputDir = path.dirname(path.resolve(outputPath));
366
- await (0, import_promises.mkdir)(outputDir, { recursive: true });
367
- await (0, import_promises.writeFile)(outputPath, JSON.stringify(output, null, 2), "utf-8");
368
- console.log(`Results saved to ${outputPath}`);
582
+ if (results.length === 0) {
583
+ process.exit(1);
369
584
  }
370
585
  }
371
586
  main().catch((err) => {