@axlsdk/eval 0.13.0 → 0.13.2

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
@@ -10,63 +10,120 @@ Evaluation framework for [Axl](https://github.com/axl-sdk/axl) agentic workflows
10
10
  npm install @axlsdk/eval
11
11
  ```
12
12
 
13
- ## API
13
+ TypeScript eval files require [tsx](https://github.com/privatenumber/tsx) as a dev dependency (`npm install -D tsx`).
14
14
 
15
- ### `dataset(config)`
15
+ ## Quick Start
16
16
 
17
- Define evaluation datasets:
17
+ An eval file defines what to test (dataset), how to run it (execution function), and how to score it (scorers):
18
+
19
+ ```typescript
20
+ // evals/qa.eval.ts
21
+ import { defineEval, dataset, scorer, llmScorer } from '@axlsdk/eval';
22
+ import type { AxlRuntime } from '@axlsdk/axl';
23
+ import { z } from 'zod';
24
+ import { qaAgent } from '../src/agents/qa.js';
25
+
26
+ export default defineEval({
27
+ workflow: 'qa-eval', // label for results (used in output table and comparisons)
28
+ dataset: dataset({
29
+ name: 'qa-basics',
30
+ schema: z.object({ question: z.string() }),
31
+ items: [
32
+ { input: { question: 'What is TypeScript?' } },
33
+ { input: { question: 'Explain closures.' } },
34
+ ],
35
+ }),
36
+ scorers: [
37
+ // Deterministic — runs in-process, no LLM call
38
+ scorer({
39
+ name: 'not-empty',
40
+ description: 'Output is non-empty',
41
+ score: (output) => (String(output).length > 10 ? 1 : 0),
42
+ }),
43
+ // LLM-as-judge — calls an LLM to evaluate quality
44
+ llmScorer({
45
+ name: 'relevance',
46
+ description: 'Is the answer relevant to the question?',
47
+ model: 'openai:gpt-4o',
48
+ system: 'Rate whether the answer is relevant to the question asked.',
49
+ schema: z.object({ score: z.number(), reasoning: z.string() }),
50
+ }),
51
+ ],
52
+ });
53
+
54
+ // How to produce output for each dataset item
55
+ export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
56
+ const ctx = runtime.createContext();
57
+ return { output: await ctx.ask(qaAgent, input.question) };
58
+ }
59
+ ```
60
+
61
+ Run it:
62
+
63
+ ```bash
64
+ OPENAI_API_KEY=sk-... npx axl-eval ./evals/qa.eval.ts
65
+ ```
66
+
67
+ ```
68
+ Eval: qa-eval x qa-basics (2 items)
69
+ Scorer Mean Min Max p50 p95
70
+ ─────────────────────────────────────────────────────
71
+ not-empty 1.00 1.00 1.00 1.00 1.00
72
+ relevance 0.90 0.80 1.00 0.90 1.00
73
+
74
+ Failures: 0/2 | Cost: $0.01 | Duration: 3.2s
75
+ ```
76
+
77
+ If an LLM scorer fails (wrong API key, provider down, invalid response), you'll see:
78
+
79
+ ```
80
+ relevance -- -- -- -- --
81
+
82
+ Scorer errors (2/2 items affected):
83
+ - Scorer "relevance" threw: OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey in options.
84
+ ```
85
+
86
+ ## Datasets
18
87
 
19
88
  ```typescript
20
89
  import { dataset } from '@axlsdk/eval';
21
90
  import { z } from 'zod';
22
91
 
23
- // Inline items with schema and annotations
24
92
  const ds = dataset({
25
93
  name: 'math-basics',
26
94
  schema: z.object({ question: z.string() }),
27
- annotations: z.object({ answer: z.number() }),
95
+ annotations: z.object({ answer: z.number() }), // optional ground truth
28
96
  items: [
29
97
  { input: { question: '2+2' }, annotations: { answer: 4 } },
30
98
  { input: { question: '3*5' }, annotations: { answer: 15 } },
31
- { input: { question: '10/2' }, annotations: { answer: 5 } },
32
99
  ],
33
100
  });
34
-
35
- // Load from file
36
- const ds = dataset({
37
- name: 'math-advanced',
38
- schema: z.object({ question: z.string() }),
39
- file: './datasets/math.json',
40
- });
41
101
  ```
42
102
 
43
- ### `scorer(config)`
103
+ You can also load from a file: `dataset({ name: 'large', schema, file: './data.json' })`.
104
+
105
+ ## Scorers
44
106
 
45
- Define deterministic scoring functions. The `score` callback receives `(output, input, annotations?)`:
107
+ Scorers rate each output on a 0-1 scale. You can mix deterministic and LLM scorers in the same eval.
108
+
109
+ ### Deterministic scorers
110
+
111
+ Pure functions — fast, free, deterministic. The `score` callback receives `(output, input, annotations?)`:
46
112
 
47
113
  ```typescript
48
114
  import { scorer } from '@axlsdk/eval';
49
115
 
50
- const exactMatch = scorer({
51
- name: 'exact-match',
52
- description: 'Checks if the output answer exactly matches the expected answer',
53
- score: (output, input, annotations) => output.answer === annotations?.answer ? 1 : 0,
54
- });
55
-
56
- const partialMatch = scorer({
57
- name: 'partial',
58
- description: 'Checks if the output contains the expected answer',
59
- score: (output, input, annotations) => {
60
- const outputStr = String(output).toLowerCase();
61
- const expected = String(annotations?.answer).toLowerCase();
62
- return outputStr.includes(expected) ? 1 : 0;
63
- },
116
+ const containsAnswer = scorer({
117
+ name: 'contains-answer',
118
+ description: 'Output contains the expected numeric answer',
119
+ score: (output, _input, annotations) =>
120
+ String(output).includes(String(annotations?.answer)) ? 1 : 0,
64
121
  });
65
122
  ```
66
123
 
67
- ### `llmScorer(config)`
124
+ ### LLM scorers
68
125
 
69
- Use an LLM as a judge. The scorer automatically constructs a prompt from the input, output, and annotations, then validates the response against your Zod schema:
126
+ Use an LLM as a judge. The scorer constructs a prompt from the input, output, and annotations, calls the LLM, and validates the response against your Zod schema:
70
127
 
71
128
  ```typescript
72
129
  import { llmScorer } from '@axlsdk/eval';
@@ -74,285 +131,216 @@ import { z } from 'zod';
74
131
 
75
132
  const qualityJudge = llmScorer({
76
133
  name: 'quality',
77
- description: 'Rates output quality on a 0-1 scale',
78
- model: 'openai-responses:gpt-5.4',
79
- system: 'You are an expert evaluator. Rate the quality of AI outputs.',
134
+ description: 'Rates overall output quality',
135
+ model: 'openai:gpt-4o', // provider:model URI
136
+ system: 'Rate the quality of the AI output.',
80
137
  schema: z.object({
81
138
  score: z.number().min(0).max(1),
82
139
  reasoning: z.string(),
83
140
  }),
84
- temperature: 0.2,
141
+ temperature: 0.2, // default: 0.2 (low for consistency)
85
142
  });
86
143
  ```
87
144
 
88
- ### `runEval(config, executeFn, runtime)`
145
+ The `model` field uses a `provider:model` URI. The provider is resolved automatically at eval time — just set the right API key:
89
146
 
90
- Run an evaluation. LLM scorer providers are automatically resolved from the runtime's provider registry using each scorer's `model` URI (e.g., `google:gemini-3.1-flash`). Ensure the relevant API key environment variable is set (e.g., `OPENAI_API_KEY`, `GOOGLE_AI_API_KEY`) or register providers via `runtime.registerProvider()`:
147
+ | Provider | URI prefix | Env var |
148
+ |----------|-----------|---------|
149
+ | OpenAI (Chat Completions) | `openai:` | `OPENAI_API_KEY` |
150
+ | OpenAI (Responses API) | `openai-responses:` | `OPENAI_API_KEY` |
151
+ | Anthropic | `anthropic:` | `ANTHROPIC_API_KEY` |
152
+ | Google Gemini | `google:` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` |
153
+ | Custom | `your-name:` | Via `runtime.registerProvider('your-name', provider)` |
91
154
 
92
- ```typescript
93
- import { runEval } from '@axlsdk/eval';
155
+ Different LLM scorers can use different providers — each resolves independently:
94
156
 
95
- const results = await runEval(
96
- {
97
- workflow: 'my-workflow', // label stored in results for comparison
98
- dataset: ds,
99
- scorers: [exactMatch, qualityJudge],
100
- concurrency: 5,
101
- },
102
- async (input) => {
103
- const output = await runtime.execute('my-workflow', input);
104
- return { output };
105
- },
106
- runtime,
107
- );
108
-
109
- console.log(results.summary);
110
- // { count: 3, failures: 0, scorers: { 'exact-match': { mean: 0.85, min: 0, max: 1, p50: 1, p95: 1 }, ... } }
157
+ ```typescript
158
+ const qualityJudge = llmScorer({ name: 'quality', model: 'openai:gpt-4o', ... });
159
+ const safetyJudge = llmScorer({ name: 'safety', model: 'anthropic:claude-sonnet-4-5-20250514', ... });
111
160
  ```
112
161
 
113
- ### `evalCompare(baseline, candidate)`
114
-
115
- Compare two evaluation runs:
162
+ ## Running Evals
116
163
 
117
- ```typescript
118
- import { evalCompare } from '@axlsdk/eval';
164
+ ### CLI
119
165
 
120
- const comparison = evalCompare(baselineResults, candidateResults);
166
+ The most common way to run evals:
121
167
 
122
- console.log(comparison.regressions);
123
- // [{ input: ..., scorer: 'exact-match', baselineScore: 1, candidateScore: 0, delta: -1 }]
124
-
125
- console.log(comparison.improvements);
126
- // [{ input: ..., scorer: 'quality', baselineScore: 0.5, candidateScore: 0.9, delta: 0.4 }]
168
+ ```bash
169
+ npx axl-eval ./evals/qa.eval.ts # run a single file
170
+ npx axl-eval ./evals/ # run all *.eval.* files in a directory
171
+ npx axl-eval ./evals/ --output ./results/v1.json # save results to JSON
172
+ npx axl-eval ./evals/ --config ./axl.config.ts # use a specific runtime config
127
173
  ```
128
174
 
129
- ### `defineEval(config)`
175
+ The CLI resolves a runtime automatically: `--config <path>` > auto-detect `axl.config.*` > bare `new AxlRuntime()` (providers from env vars).
130
176
 
131
- Define an eval file for discovery by the CLI or Studio middleware:
177
+ ### Programmatic
178
+
179
+ **`runtime.eval()`** — when you have a workflow registered on the runtime. The `workflow` field must match the registered name:
132
180
 
133
181
  ```typescript
134
- // evals/math.eval.ts
135
- import { defineEval, dataset, scorer } from '@axlsdk/eval';
182
+ import { AxlRuntime, workflow, agent } from '@axlsdk/axl';
183
+ import { dataset, scorer } from '@axlsdk/eval';
136
184
  import { z } from 'zod';
137
185
 
138
- const mathDataset = dataset({
139
- name: 'math-basics',
140
- schema: z.object({ question: z.string() }),
141
- annotations: z.object({ answer: z.number() }),
142
- items: [
143
- { input: { question: '2+2' }, annotations: { answer: 4 } },
144
- { input: { question: '3*5' }, annotations: { answer: 15 } },
145
- ],
186
+ const qaAgent = agent({ name: 'qa', model: 'openai:gpt-4o', system: 'Answer questions.' });
187
+ const qaWorkflow = workflow({
188
+ name: 'qa-workflow',
189
+ input: z.object({ question: z.string() }),
190
+ handler: async (ctx) => ctx.ask(qaAgent, ctx.input.question),
146
191
  });
147
192
 
148
- const correctAnswer = scorer({
149
- name: 'correct-answer',
150
- description: 'Checks if the output contains the expected numeric answer',
151
- score: (output, _input, annotations) =>
152
- String(output).includes(String(annotations?.answer)) ? 1 : 0,
153
- });
193
+ const runtime = new AxlRuntime();
194
+ runtime.register(qaWorkflow);
154
195
 
155
- export default defineEval({
156
- workflow: 'math-workflow',
157
- dataset: mathDataset,
158
- scorers: [correctAnswer],
196
+ const results = await runtime.eval({
197
+ workflow: 'qa-workflow', // must match the registered workflow name
198
+ dataset: ds,
199
+ scorers: [containsAnswer, qualityJudge],
159
200
  });
160
201
  ```
161
202
 
162
- 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.
203
+ **`runEval()`** — when you want full control. The `workflow` field is just a label; the second argument is the function that produces output:
163
204
 
164
- 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).
205
+ ```typescript
206
+ import { runEval } from '@axlsdk/eval';
165
207
 
166
- 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:
208
+ const results = await runEval(
209
+ { workflow: 'my-eval', dataset: ds, scorers: [containsAnswer, qualityJudge] },
210
+ async (input, runtime) => {
211
+ const ctx = runtime.createContext();
212
+ return { output: await ctx.ask(qaAgent, input.question) };
213
+ },
214
+ runtime,
215
+ );
216
+ ```
167
217
 
168
- ```typescript
169
- // evals/qa-quality.eval.ts — calls an agent directly via the runtime
170
- import { defineEval, dataset, scorer } from '@axlsdk/eval';
171
- import type { AxlRuntime } from '@axlsdk/axl';
172
- import { z } from 'zod';
173
- import { qaAgent } from '../src/agents/qa.js'; // import your agent definition
218
+ ### Studio
174
219
 
175
- const qaDataset = dataset({
176
- name: 'qa-pairs',
177
- schema: z.object({ question: z.string() }),
178
- items: [
179
- { input: { question: 'What is TypeScript?' } },
180
- { input: { question: 'How do promises work?' } },
181
- ],
182
- });
220
+ Eval files can be lazy-loaded by the Studio middleware for the Eval Runner panel:
183
221
 
184
- const notEmpty = scorer({
185
- name: 'not-empty',
186
- description: 'Output is non-empty',
187
- score: (output) => (String(output).length > 0 ? 1 : 0),
188
- });
222
+ ```typescript
223
+ import { createStudioMiddleware } from '@axlsdk/studio/middleware';
189
224
 
190
- export default defineEval({
191
- workflow: 'qa-quality', // label for results (not a runtime lookup)
192
- dataset: qaDataset,
193
- scorers: [notEmpty],
225
+ const studio = createStudioMiddleware({
226
+ runtime,
227
+ evals: 'evals/**/*.eval.ts',
194
228
  });
195
-
196
- export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
197
- const ctx = runtime.createContext();
198
- const output = await ctx.ask(qaAgent, input.question);
199
- return { output }; // cost tracked automatically via runtime.trackCost()
200
- }
201
229
  ```
202
230
 
203
- When no runtime is needed (e.g., testing a pure function), omit the runtime parameter — these evals work from both the CLI and Studio:
231
+ See the [@axlsdk/studio README](../axl-studio/README.md#lazy-eval-loading) for details.
204
232
 
205
- ```typescript
206
- // evals/parser.eval.ts — tests a pure function, no runtime needed
207
- export async function executeWorkflow(input: { raw: string }) {
208
- const result = parseDocument(input.raw);
209
- return { output: result };
210
- }
211
- ```
233
+ ## Understanding Results
212
234
 
213
- > **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.
235
+ Each eval run returns an `EvalResult` with per-item scores and aggregate statistics.
214
236
 
215
- ### Cost Tracking
237
+ **Scores** are `number | null` per scorer per item:
238
+ - `0` to `1` — valid score from the scorer
239
+ - `null` — the scorer failed (see `item.scorerErrors` for details)
216
240
 
217
- 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.
241
+ Summary statistics (mean, p50, p95, etc.) exclude `null` scores. If all scores for a scorer are `null`, the CLI shows `--` instead of misleading `0.00`.
218
242
 
219
243
  ```typescript
220
- export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
221
- const ctx = runtime.createContext();
222
- const output = await ctx.ask(myAgent, input.question);
223
- return { output }; // cost captured automatically
244
+ const results = await runtime.eval({ ... });
245
+
246
+ // Aggregate stats
247
+ console.log(results.summary.scorers['quality'].mean); // 0.85
248
+ console.log(results.summary.count); // 50 items
249
+ console.log(results.summary.failures); // 2 workflow errors
250
+
251
+ // Per-item inspection
252
+ for (const item of results.items) {
253
+ if (item.error) continue; // workflow threw
254
+ if (item.scores['quality'] === null) {
255
+ console.log('Scorer failed:', item.scorerErrors); // e.g., ["Scorer "quality" threw: ..."]
256
+ }
224
257
  }
225
- ```
226
-
227
- To override the automatic cost (e.g., to exclude setup calls), return it explicitly:
228
258
 
229
- ```typescript
230
- return { output, cost: ctx.totalCost };
259
+ // Cost includes both workflow and LLM scorer calls
260
+ console.log(results.totalCost); // 0.42
231
261
  ```
232
262
 
233
- You can also read `ctx.totalCost` at any point to inspect accumulated cost.
263
+ ## Comparing Results
234
264
 
235
- ### Common Patterns
265
+ Compare two runs to detect regressions and improvements:
236
266
 
237
- **Budget per item** — cap cost to prevent runaway evals:
267
+ ```bash
268
+ npx axl-eval compare ./results/v1.json ./results/v2.json
269
+ npx axl-eval compare v1.json v2.json --fail-on-regression # exit 1 if worse
270
+ ```
238
271
 
239
272
  ```typescript
240
- export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
241
- const ctx = runtime.createContext({ budget: '$0.50' });
242
- const output = await ctx.ask(myAgent, input.question);
243
- return { output };
244
- }
273
+ import { evalCompare } from '@axlsdk/eval';
274
+
275
+ const comparison = evalCompare(v1Results, v2Results);
276
+ console.log(comparison.regressions); // items that got worse
277
+ console.log(comparison.improvements); // items that got better
245
278
  ```
246
279
 
247
- **Timeout per item** — cancel items that take too long:
280
+ ## Eval Files in Detail
248
281
 
249
- ```typescript
250
- export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
251
- const controller = new AbortController();
252
- setTimeout(() => controller.abort(), 30_000);
253
- const ctx = runtime.createContext({ signal: controller.signal });
254
- const output = await ctx.ask(myAgent, input.question);
255
- return { output };
256
- }
257
- ```
282
+ ### Execution function
258
283
 
259
- **Auto-approve tools** — when testing agents that have tools with `requireApproval`:
284
+ By default, the runner calls `runtime.execute(workflow, input)` for each item. Export `executeWorkflow` to override:
260
285
 
261
286
  ```typescript
287
+ // evals/qa.eval.ts
288
+ import { defineEval, dataset, scorer } from '@axlsdk/eval';
289
+ import type { AxlRuntime } from '@axlsdk/axl';
290
+ import { qaAgent } from '../src/agents/qa.js';
291
+
292
+ export default defineEval({
293
+ workflow: 'qa-eval',
294
+ dataset: dataset({ ... }),
295
+ scorers: [scorer({ ... })],
296
+ });
297
+
262
298
  export async function executeWorkflow(input: { question: string }, runtime: AxlRuntime) {
263
- const ctx = runtime.createContext({
264
- awaitHumanHandler: async () => ({ approved: true }),
265
- });
266
- const output = await ctx.ask(myAgent, input.question);
267
- return { output };
299
+ const ctx = runtime.createContext();
300
+ return { output: await ctx.ask(qaAgent, input.question) };
268
301
  }
269
302
  ```
270
303
 
271
- **Multi-turn evaluation** — test follow-up responses with conversation history:
304
+ When no runtime is needed (e.g., testing a pure function):
272
305
 
273
306
  ```typescript
274
- export async function executeWorkflow(
275
- input: { setupPrompt: string; setupResponse: string; followUp: string },
276
- runtime: AxlRuntime,
277
- ) {
278
- const ctx = runtime.createContext({
279
- sessionHistory: [
280
- { role: 'user', content: input.setupPrompt },
281
- { role: 'assistant', content: input.setupResponse },
282
- ],
283
- });
284
- const output = await ctx.ask(myAgent, input.followUp);
285
- return { output };
307
+ export async function executeWorkflow(input: { raw: string }) {
308
+ return { output: parseDocument(input.raw) };
286
309
  }
287
310
  ```
288
311
 
289
- ## CLI
290
-
291
- Run evaluations from the command line:
292
-
293
- ```bash
294
- # Run an eval file
295
- npx axl-eval ./evals/math.eval.ts
312
+ ### Cost tracking
296
313
 
297
- # Run all evals in a directory
298
- npx axl-eval ./evals/
314
+ Cost is tracked automatically — the runner wraps each item with `runtime.trackCost()`. LLM scorer costs are also included in `totalCost` and count toward the `budget` limit.
299
315
 
300
- # Save results to a file
301
- npx axl-eval ./evals/math.eval.ts --output ./results/baseline.json
316
+ To override (e.g., exclude setup calls), return cost explicitly:
302
317
 
303
- # Use a config file for runtime access
304
- npx axl-eval ./evals/ --config ./axl.config.ts
305
-
306
- # Use custom import conditions (monorepo source exports)
307
- npx axl-eval ./evals/ --config ./axl.config.ts --conditions development
308
-
309
- # Compare two results
310
- npx axl-eval compare ./results/baseline.json ./results/candidate.json
311
-
312
- # Fail CI if regressions detected
313
- npx axl-eval compare baseline.json candidate.json --fail-on-regression
318
+ ```typescript
319
+ return { output, cost: ctx.totalCost };
314
320
  ```
315
321
 
316
- TypeScript config and eval files require [tsx](https://github.com/privatenumber/tsx) as a dev dependency (`npm install -D tsx`).
317
-
318
- ### Runtime Resolution
319
-
320
- The CLI resolves an `AxlRuntime` to pass to `executeWorkflow`:
321
-
322
- 1. **`--config <path>`** — use an explicit config file
323
- 2. **Auto-detect** — search for `axl.config.mts` → `.ts` → `.mjs` → `.js` in cwd
324
- 3. **Fallback** — create a bare `new AxlRuntime()` (providers resolve from environment variables)
325
-
326
- When a runtime is available, each `executeWorkflow` call is wrapped with `runtime.trackCost()` for automatic cost attribution.
322
+ ### Common patterns
327
323
 
328
- ## Studio Integration
324
+ **Budget** — cap cost per item with `runtime.createContext({ budget: '$0.50' })`.
329
325
 
330
- Eval files can also be lazy-loaded by the Studio middleware, enabling the Eval Runner panel without static imports that would create circular dependencies:
326
+ **Timeout** — abort slow items:
331
327
 
332
328
  ```typescript
333
- import { createStudioMiddleware } from '@axlsdk/studio/middleware';
334
-
335
- const studio = createStudioMiddleware({
336
- runtime,
337
- evals: 'evals/**/*.eval.ts',
338
- });
329
+ const controller = new AbortController();
330
+ setTimeout(() => controller.abort(), 30_000);
331
+ const ctx = runtime.createContext({ signal: controller.signal });
339
332
  ```
340
333
 
341
- See the [@axlsdk/studio README](../axl-studio/README.md#lazy-eval-loading) for details.
334
+ **Auto-approve tools** — skip human approval in evals: `runtime.createContext({ awaitHumanHandler: async () => ({ approved: true }) })`.
342
335
 
343
- ## Integration with AxlRuntime
336
+ **Multi-turn** — provide conversation history:
344
337
 
345
338
  ```typescript
346
- import { AxlRuntime } from '@axlsdk/axl';
347
-
348
- const runtime = new AxlRuntime();
349
- runtime.register(myWorkflow);
350
-
351
- const results = await runtime.eval({
352
- workflow: 'my-workflow',
353
- dataset: ds,
354
- scorers: [exactMatch],
355
- concurrency: 3,
339
+ const ctx = runtime.createContext({
340
+ sessionHistory: [
341
+ { role: 'user', content: input.setupPrompt },
342
+ { role: 'assistant', content: input.setupResponse },
343
+ ],
356
344
  });
357
345
  ```
358
346
 
@@ -158,8 +158,8 @@ async function runEval(config, executeWorkflow, runtime) {
158
158
  }
159
159
  }
160
160
  if (score < 0 || score > 1) {
161
- if (!evalItem.errors) evalItem.errors = [];
162
- evalItem.errors.push(
161
+ if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
162
+ evalItem.scorerErrors.push(
163
163
  `Scorer "${scorer.name}" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`
164
164
  );
165
165
  evalItem.scores[scorer.name] = null;
@@ -173,8 +173,8 @@ async function runEval(config, executeWorkflow, runtime) {
173
173
  totalCost += lastCost;
174
174
  }
175
175
  }
176
- if (!evalItem.errors) evalItem.errors = [];
177
- evalItem.errors.push(
176
+ if (!evalItem.scorerErrors) evalItem.scorerErrors = [];
177
+ evalItem.scorerErrors.push(
178
178
  `Scorer "${scorer.name}" threw: ${err instanceof Error ? err.message : String(err)}`
179
179
  );
180
180
  evalItem.scores[scorer.name] = null;
@@ -215,4 +215,4 @@ export {
215
215
  evalCompare,
216
216
  runEval
217
217
  };
218
- //# sourceMappingURL=chunk-7B5NFD5T.js.map
218
+ //# sourceMappingURL=chunk-K73SNPVV.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 == null || cScore == null) 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 type { ScorerContext } from './scorer.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\nexport async function runEval(\n config: EvalConfig,\n executeWorkflow: (\n input: unknown,\n runtime: AxlRuntime,\n ) => Promise<{ output: unknown; cost?: number }>,\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 // Create a scorer context that LLM scorers use to resolve providers.\n const scorerContext: ScorerContext = {\n resolveProvider: (uri: string) => {\n if (typeof runtime.resolveProvider !== 'function') {\n throw new Error(\n `LLM scorers require a runtime with resolveProvider(). ` +\n `Ensure you are using a real AxlRuntime instance, not a mock.`,\n );\n }\n return runtime.resolveProvider(uri);\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(\n evalItem.output,\n item.input,\n item.annotations,\n scorerContext,\n );\n // Accumulate LLM scorer cost if available\n if (scorer.isLlm) {\n const lastCost = (scorer as unknown as { _lastCost?: number })._lastCost;\n if (lastCost != null) {\n totalCost += lastCost;\n }\n }\n if (score < 0 || score > 1) {\n if (!evalItem.scorerErrors) evalItem.scorerErrors = [];\n evalItem.scorerErrors.push(\n `Scorer \"${scorer.name}\" returned out-of-range score ${score} for input ${JSON.stringify(item.input)}`,\n );\n evalItem.scores[scorer.name] = null;\n } else {\n evalItem.scores[scorer.name] = round(score);\n }\n } catch (err) {\n // Accumulate cost even on failure — the LLM call may have succeeded\n // before a downstream error (e.g., invalid JSON, schema validation)\n if (scorer.isLlm) {\n const lastCost = (scorer as unknown as { _lastCost?: number })._lastCost;\n if (lastCost != null) {\n totalCost += lastCost;\n }\n }\n if (!evalItem.scorerErrors) evalItem.scorerErrors = [];\n evalItem.scorerErrors.push(\n `Scorer \"${scorer.name}\" threw: ${err instanceof Error ? err.message : String(err)}`,\n );\n evalItem.scores[scorer.name] = null;\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] != null)\n .map((i) => i.scores[name] as number);\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"]}