@cogitator-ai/evals 0.8.0 → 0.10.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.
Files changed (116) hide show
  1. package/README.md +255 -54
  2. package/dist/assertions/error-rate.d.ts +8 -0
  3. package/dist/assertions/error-rate.d.ts.map +1 -0
  4. package/dist/assertions/error-rate.js +27 -0
  5. package/dist/assertions/error-rate.js.map +1 -0
  6. package/dist/assertions/index.d.ts +3 -0
  7. package/dist/assertions/index.d.ts.map +1 -1
  8. package/dist/assertions/index.js +1 -0
  9. package/dist/assertions/index.js.map +1 -1
  10. package/dist/assertions/regression.d.ts.map +1 -1
  11. package/dist/assertions/regression.js +3 -1
  12. package/dist/assertions/regression.js.map +1 -1
  13. package/dist/assertions/threshold.d.ts.map +1 -1
  14. package/dist/assertions/threshold.js +4 -1
  15. package/dist/assertions/threshold.js.map +1 -1
  16. package/dist/datasets/csv-loader.d.ts +5 -0
  17. package/dist/datasets/csv-loader.d.ts.map +1 -1
  18. package/dist/datasets/csv-loader.js +89 -7
  19. package/dist/datasets/csv-loader.js.map +1 -1
  20. package/dist/datasets/dataset.d.ts +11 -2
  21. package/dist/datasets/dataset.d.ts.map +1 -1
  22. package/dist/datasets/dataset.js +42 -13
  23. package/dist/datasets/dataset.js.map +1 -1
  24. package/dist/datasets/issues.d.ts +4 -0
  25. package/dist/datasets/issues.d.ts.map +1 -0
  26. package/dist/datasets/issues.js +7 -0
  27. package/dist/datasets/issues.js.map +1 -0
  28. package/dist/datasets/jsonl-loader.d.ts.map +1 -1
  29. package/dist/datasets/jsonl-loader.js +6 -1
  30. package/dist/datasets/jsonl-loader.js.map +1 -1
  31. package/dist/eval-builder.d.ts +3 -0
  32. package/dist/eval-builder.d.ts.map +1 -1
  33. package/dist/eval-builder.js +7 -0
  34. package/dist/eval-builder.js.map +1 -1
  35. package/dist/eval-comparison.d.ts +7 -0
  36. package/dist/eval-comparison.d.ts.map +1 -1
  37. package/dist/eval-comparison.js +42 -12
  38. package/dist/eval-comparison.js.map +1 -1
  39. package/dist/eval-suite.d.ts +55 -8
  40. package/dist/eval-suite.d.ts.map +1 -1
  41. package/dist/eval-suite.js +254 -53
  42. package/dist/eval-suite.js.map +1 -1
  43. package/dist/index.d.ts +14 -8
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +6 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/matchers.d.ts +68 -0
  48. package/dist/matchers.d.ts.map +1 -0
  49. package/dist/matchers.js +112 -0
  50. package/dist/matchers.js.map +1 -0
  51. package/dist/metrics/custom.d.ts +19 -7
  52. package/dist/metrics/custom.d.ts.map +1 -1
  53. package/dist/metrics/custom.js +5 -0
  54. package/dist/metrics/custom.js.map +1 -1
  55. package/dist/metrics/index.d.ts +8 -4
  56. package/dist/metrics/index.d.ts.map +1 -1
  57. package/dist/metrics/index.js +4 -2
  58. package/dist/metrics/index.js.map +1 -1
  59. package/dist/metrics/llm-judge.d.ts +20 -1
  60. package/dist/metrics/llm-judge.d.ts.map +1 -1
  61. package/dist/metrics/llm-judge.js +162 -20
  62. package/dist/metrics/llm-judge.js.map +1 -1
  63. package/dist/metrics/matching.d.ts +28 -0
  64. package/dist/metrics/matching.d.ts.map +1 -0
  65. package/dist/metrics/matching.js +124 -0
  66. package/dist/metrics/matching.js.map +1 -0
  67. package/dist/metrics/retrieval.d.ts +21 -0
  68. package/dist/metrics/retrieval.d.ts.map +1 -0
  69. package/dist/metrics/retrieval.js +108 -0
  70. package/dist/metrics/retrieval.js.map +1 -0
  71. package/dist/metrics/statistical.d.ts +28 -0
  72. package/dist/metrics/statistical.d.ts.map +1 -1
  73. package/dist/metrics/statistical.js +61 -0
  74. package/dist/metrics/statistical.js.map +1 -1
  75. package/dist/metrics/tool-calls.d.ts +43 -0
  76. package/dist/metrics/tool-calls.d.ts.map +1 -0
  77. package/dist/metrics/tool-calls.js +106 -0
  78. package/dist/metrics/tool-calls.js.map +1 -0
  79. package/dist/metrics/types.d.ts +53 -5
  80. package/dist/metrics/types.d.ts.map +1 -1
  81. package/dist/metrics/types.js.map +1 -1
  82. package/dist/reporters/ci.d.ts +4 -0
  83. package/dist/reporters/ci.d.ts.map +1 -1
  84. package/dist/reporters/ci.js +5 -1
  85. package/dist/reporters/ci.js.map +1 -1
  86. package/dist/reporters/console.d.ts.map +1 -1
  87. package/dist/reporters/console.js +11 -4
  88. package/dist/reporters/console.js.map +1 -1
  89. package/dist/reporters/csv.d.ts.map +1 -1
  90. package/dist/reporters/csv.js +5 -2
  91. package/dist/reporters/csv.js.map +1 -1
  92. package/dist/reporters/format.d.ts +19 -0
  93. package/dist/reporters/format.d.ts.map +1 -0
  94. package/dist/reporters/format.js +31 -0
  95. package/dist/reporters/format.js.map +1 -0
  96. package/dist/reporters/github.d.ts +11 -0
  97. package/dist/reporters/github.d.ts.map +1 -0
  98. package/dist/reporters/github.js +95 -0
  99. package/dist/reporters/github.js.map +1 -0
  100. package/dist/reporters/index.d.ts +44 -20
  101. package/dist/reporters/index.d.ts.map +1 -1
  102. package/dist/reporters/index.js +14 -3
  103. package/dist/reporters/index.js.map +1 -1
  104. package/dist/reporters/junit.d.ts +8 -0
  105. package/dist/reporters/junit.d.ts.map +1 -0
  106. package/dist/reporters/junit.js +107 -0
  107. package/dist/reporters/junit.js.map +1 -0
  108. package/dist/schema.d.ts +25 -0
  109. package/dist/schema.d.ts.map +1 -1
  110. package/dist/schema.js +29 -0
  111. package/dist/schema.js.map +1 -1
  112. package/dist/vitest.d.ts +35 -0
  113. package/dist/vitest.d.ts.map +1 -0
  114. package/dist/vitest.js +85 -0
  115. package/dist/vitest.js.map +1 -0
  116. package/package.json +17 -3
@@ -1,10 +1,15 @@
1
1
  import { Dataset } from './datasets/index.js';
2
2
  import type { EvalCase } from './schema.js';
3
3
  import type { JudgeConfigInput } from './schema.js';
4
- import type { MetricFn, MetricScore, EvalCaseResult, StatisticalMetricFn } from './metrics/types.js';
4
+ import type { MetricFn, Passage, ScoredCaseResult, StatisticalMetricFn } from './metrics/types.js';
5
5
  import type { LLMMetricFn } from './metrics/llm-judge.js';
6
6
  import type { AssertionFn, AssertionResult, AggregatedMetric, EvalStats } from './assertions/index.js';
7
7
  import type { ReporterType, ReporterOptions } from './reporters/index.js';
8
+ /** A turn of a conversation: what the user said and what the target answered */
9
+ export interface EvalTurn {
10
+ input: string;
11
+ output: string;
12
+ }
8
13
  /** What a function target gets besides the input */
9
14
  export interface EvalTargetContext {
10
15
  /**
@@ -17,11 +22,27 @@ export interface EvalTargetContext {
17
22
  case: EvalCase;
18
23
  /** 0 for the first attempt, then 1, 2 and so on for retries */
19
24
  attempt: number;
25
+ /** 0 for the case's `input`, then 1, 2 and so on for each of its further `turns` */
26
+ turn: number;
27
+ /** The turns of the case before this one, with the answers the target gave */
28
+ history: readonly EvalTurn[];
29
+ }
30
+ /** A tool result as the `retrieval` of a target reads it */
31
+ export interface RetrievalToolResult {
32
+ name: string;
33
+ result: unknown;
20
34
  }
21
35
  export interface EvalTarget {
22
36
  agent?: unknown;
23
37
  cogitator?: unknown;
38
+ /** Called once per turn of a case: with `input`, then with each of its further `turns` */
24
39
  fn?: (input: string, context: EvalTargetContext) => Promise<string>;
40
+ /**
41
+ * Reads the passages a tool result of an agent target surfaced, into the case's `retrieved`,
42
+ * undefined for a result that holds none. Default: `ragSearchPassages`, which reads the
43
+ * results of the `rag_search` tool of `@cogitator-ai/rag`
44
+ */
45
+ retrieval?: (toolResult: RetrievalToolResult) => Passage[] | undefined;
25
46
  }
26
47
  export interface EvalProgress {
27
48
  completed: number;
@@ -38,7 +59,19 @@ export interface EvalSuiteOptions {
38
59
  concurrency?: number;
39
60
  timeout?: number;
40
61
  retries?: number;
41
- /** Called after each case. A callback that throws is reported as a process warning */
62
+ /**
63
+ * Runs of each case, a positive integer, 1 by default. Each trial is scored on its own, the
64
+ * case result is the first trial that answered (the first when all errored) with every trial in
65
+ * `trials`, and the aggregate of a metric
66
+ * takes the mean of a case's scored trials as the case's score. A case counts as errored in
67
+ * `stats.errors` when every trial errored, and `latency()`, `cost()` and `tokenUsage()` see
68
+ * every trial. Use `passAtK()` to score how often a case passes
69
+ */
70
+ trials?: number;
71
+ /**
72
+ * Called after each case, once all its trials ran. A callback that throws is reported as a
73
+ * process warning
74
+ */
42
75
  onProgress?: (progress: EvalProgress) => void;
43
76
  }
44
77
  export interface EvalRunOptions {
@@ -53,9 +86,11 @@ export interface EvalRunOptions {
53
86
  /** The run-wide figures of a finished suite run, every field filled in */
54
87
  export type EvalSuiteStats = Required<EvalStats>;
55
88
  export interface EvalSuiteResult {
56
- results: Array<EvalCaseResult & {
57
- scores: MetricScore[];
58
- }>;
89
+ /**
90
+ * One result per case, in dataset order. With several trials it is the first trial that
91
+ * answered, or the first when every trial errored, with every trial in its `trials`
92
+ */
93
+ results: ScoredCaseResult[];
59
94
  aggregated: Record<string, AggregatedMetric>;
60
95
  assertions: AssertionResult[];
61
96
  stats: EvalSuiteStats;
@@ -73,11 +108,16 @@ export declare class EvalSuite {
73
108
  private readonly concurrency;
74
109
  private readonly timeout;
75
110
  private readonly retries;
111
+ private readonly trials;
76
112
  private readonly onProgress?;
77
113
  constructor(opts: EvalSuiteOptions);
78
114
  private validateTarget;
115
+ /** A `passAtK()` metric needs the metric it reads and at least `k` trials */
116
+ private validatePassAtK;
79
117
  /** Two metrics with one name would be aggregated, compared and reported as one */
80
118
  private validateMetricNames;
119
+ /** The cases a run with `maxCases` goes through, in order */
120
+ cases(maxCases?: number): readonly EvalCase[];
81
121
  run(options?: EvalRunOptions): Promise<EvalSuiteResult>;
82
122
  private reportProgress;
83
123
  private runCase;
@@ -89,14 +129,21 @@ export declare class EvalSuite {
89
129
  */
90
130
  private executeCaseAttempt;
91
131
  private executeFnTarget;
132
+ /**
133
+ * Runs the agent on each turn of the case. A case with further `turns` runs them all on one
134
+ * thread of its own, so the Cogitator's memory carries the conversation from turn to turn
135
+ */
92
136
  private executeAgentTarget;
93
137
  private evaluateCaseMetrics;
94
138
  /**
95
- * A metric's score for one case. A metric that throws, or whose score is not a finite number,
96
- * scores 0 with the reason in `error`, so one bad metric neither drops the case nor poisons
97
- * the aggregate with NaN.
139
+ * A metric's score for one case. Only cases with an output are scored: a case whose every
140
+ * attempt failed is left unscored, so its empty output neither costs a judge call nor counts as
141
+ * a score of 0 (`stats.errors` and the `maxErrorRate` assertion catch those). A metric that
142
+ * throws, or whose score is not a finite number, scores 0 with the reason in `error`, so one
143
+ * bad metric neither drops the case nor poisons the aggregate with NaN.
98
144
  */
99
145
  private scoreMetric;
146
+ /** One score per case and metric: the mean over the case's trials the metric scored */
100
147
  private aggregateScores;
101
148
  }
102
149
  //# sourceMappingURL=eval-suite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eval-suite.d.ts","sourceRoot":"","sources":["../src/eval-suite.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"eval-suite.d.ts","sourceRoot":"","sources":["../src/eval-suite.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,30 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { writeFileSync } from 'node:fs';
2
3
  import { EvalSuiteConfigSchema, JudgeConfigSchema } from './schema.js';
3
4
  import { nonFiniteScoreError } from './metrics/types.js';
5
+ import { isPassAtKMetric } from './metrics/statistical.js';
6
+ import { ragSearchPassages } from './metrics/retrieval.js';
4
7
  import { bindJudgeContext, judgeContextFor } from './metrics/llm-judge.js';
5
- import { aggregate } from './stats/index.js';
8
+ import { aggregate, mean } from './stats/index.js';
6
9
  import { report } from './reporters/index.js';
10
+ /**
11
+ * A turn of a multi-turn case failed: the error of the turn, with the turns run so far (the
12
+ * failed one last) and what they spent
13
+ */
14
+ class TurnFailure extends Error {
15
+ turns;
16
+ usage;
17
+ constructor(cause, turns, usage) {
18
+ super(errorMessage(cause), { cause });
19
+ this.turns = turns;
20
+ this.usage = usage;
21
+ this.name = 'TurnFailure';
22
+ }
23
+ }
24
+ /** What the user says to the target, turn by turn: the case's `input`, then its `turns` */
25
+ function turnInputs(evalCase) {
26
+ return [evalCase.input, ...(evalCase.turns ?? [])];
27
+ }
7
28
  export function isLLMMetric(m) {
8
29
  return 'requiresJudge' in m && m.requiresJudge === true;
9
30
  }
@@ -67,6 +88,25 @@ async function waitAtMost(promise, ms) {
67
88
  clearTimeout(timer);
68
89
  }
69
90
  }
91
+ /**
92
+ * The aggregate of a metric no case was scored on, every case having errored: its figures are
93
+ * NaN, so a threshold, a regression check or a baseline over it fails instead of passing on
94
+ * nothing
95
+ */
96
+ function unscored(name) {
97
+ return {
98
+ name,
99
+ mean: Number.NaN,
100
+ median: Number.NaN,
101
+ min: Number.NaN,
102
+ max: Number.NaN,
103
+ stdDev: Number.NaN,
104
+ p50: Number.NaN,
105
+ p95: Number.NaN,
106
+ p99: Number.NaN,
107
+ count: 0,
108
+ };
109
+ }
70
110
  export class EvalSuite {
71
111
  dataset;
72
112
  target;
@@ -76,12 +116,14 @@ export class EvalSuite {
76
116
  concurrency;
77
117
  timeout;
78
118
  retries;
119
+ trials;
79
120
  onProgress;
80
121
  constructor(opts) {
81
122
  const config = EvalSuiteConfigSchema.parse({
82
123
  concurrency: opts.concurrency,
83
124
  timeout: opts.timeout,
84
125
  retries: opts.retries,
126
+ trials: opts.trials,
85
127
  });
86
128
  this.dataset = opts.dataset;
87
129
  this.target = opts.target;
@@ -90,10 +132,12 @@ export class EvalSuite {
90
132
  this.concurrency = config.concurrency;
91
133
  this.timeout = config.timeout;
92
134
  this.retries = config.retries;
135
+ this.trials = config.trials;
93
136
  this.onProgress = opts.onProgress;
94
137
  this.validateTarget();
95
138
  const rawMetrics = opts.metrics ?? [];
96
139
  this.validateMetricNames(rawMetrics);
140
+ this.validatePassAtK(rawMetrics);
97
141
  const hasLLMMetrics = rawMetrics.some(isLLMMetric);
98
142
  if (hasLLMMetrics && !opts.judge) {
99
143
  throw new Error('LLM metrics require a judge config');
@@ -123,6 +167,24 @@ export class EvalSuite {
123
167
  if (hasCogitator && !hasAgent) {
124
168
  throw new Error('Cogitator target requires agent instance');
125
169
  }
170
+ if (hasFn && this.target.retrieval !== undefined) {
171
+ throw new Error('Target retrieval reads tool results, so it needs an agent target, not fn');
172
+ }
173
+ }
174
+ /** A `passAtK()` metric needs the metric it reads and at least `k` trials */
175
+ validatePassAtK(metrics) {
176
+ const names = new Set(metrics.map((m) => m.metricName));
177
+ for (const statMetric of this.statisticalMetrics) {
178
+ if (!isPassAtKMetric(statMetric))
179
+ continue;
180
+ const { metric, k } = statMetric.passAtK;
181
+ if (!names.has(metric)) {
182
+ throw new Error(`${statMetric.metricName} reads the metric '${metric}', which the suite does not have: add it to metrics`);
183
+ }
184
+ if (k > this.trials) {
185
+ throw new Error(`${statMetric.metricName} needs at least ${k} trials per case, the suite runs ${this.trials}: set trials to ${k} or more`);
186
+ }
187
+ }
126
188
  }
127
189
  /** Two metrics with one name would be aggregated, compared and reported as one */
128
190
  validateMetricNames(metrics) {
@@ -137,6 +199,10 @@ export class EvalSuite {
137
199
  seen.add(name);
138
200
  }
139
201
  }
202
+ /** The cases a run with `maxCases` goes through, in order */
203
+ cases(maxCases) {
204
+ return this.dataset.cases.slice(0, maxCases);
205
+ }
140
206
  async run(options = {}) {
141
207
  const { maxCases, signal } = options;
142
208
  if (maxCases !== undefined && (!Number.isInteger(maxCases) || maxCases < 1)) {
@@ -144,37 +210,45 @@ export class EvalSuite {
144
210
  }
145
211
  signal?.throwIfAborted();
146
212
  const suiteStart = Date.now();
147
- const cases = this.dataset.cases.slice(0, maxCases);
213
+ const cases = this.cases(maxCases);
148
214
  const total = cases.length;
149
- const orderedResults = new Array(total);
150
- let nextIdx = 0;
215
+ const trials = this.trials;
216
+ const trialResults = cases.map(() => new Array(trials));
217
+ const trialsLeft = cases.map(() => trials);
218
+ const runs = total * trials;
219
+ let nextRun = 0;
151
220
  let completed = 0;
152
221
  const worker = async () => {
153
- while (nextIdx < total && !signal?.aborted) {
154
- const i = nextIdx++;
222
+ while (nextRun < runs && !signal?.aborted) {
223
+ const run = nextRun++;
224
+ const i = Math.floor(run / trials);
155
225
  const evalCase = cases[i];
156
- orderedResults[i] = await this.runCase(evalCase, signal);
226
+ trialResults[i][run % trials] = await this.runCase(evalCase, signal);
227
+ if (--trialsLeft[i] > 0)
228
+ continue;
157
229
  completed++;
158
230
  this.reportProgress({ completed, total, currentCase: evalCase });
159
231
  }
160
232
  };
161
- await Promise.all(Array.from({ length: Math.min(this.concurrency, total) }, worker));
233
+ await Promise.all(Array.from({ length: Math.min(this.concurrency, runs) }, worker));
162
234
  signal?.throwIfAborted();
235
+ const orderedResults = trialResults.map((caseTrials) => trials === 1 ? caseTrials[0] : { ...leadTrial(caseTrials), trials: caseTrials });
236
+ const allTrials = orderedResults.flatMap(trialsOf);
163
237
  const aggregated = this.aggregateScores(orderedResults);
164
238
  for (const statMetric of this.statisticalMetrics) {
165
- const score = statMetric(orderedResults);
239
+ const score = statMetric(isPassAtKMetric(statMetric) ? orderedResults : allTrials);
166
240
  aggregated[score.name] = {
167
241
  name: score.name,
168
242
  ...aggregate(score.values ?? [score.score]),
169
243
  ...(score.metadata && { metadata: score.metadata }),
170
244
  };
171
245
  }
172
- const targetCost = orderedResults.reduce((sum, r) => sum + (r.usage?.cost ?? 0), 0);
173
- const judgeCost = orderedResults.reduce((sum, r) => sum + r.scores.reduce((s, score) => s + (score.usage?.cost ?? 0), 0), 0);
246
+ const targetCost = allTrials.reduce((sum, r) => sum + (r.usage?.cost ?? 0), 0);
247
+ const judgeCost = allTrials.reduce((sum, r) => sum + r.scores.reduce((s, score) => s + (score.usage?.cost ?? 0), 0), 0);
174
248
  const stats = {
175
249
  total,
176
250
  errors: orderedResults.filter((r) => r.error !== undefined).length,
177
- metricErrors: orderedResults.reduce((n, r) => n + r.scores.filter((s) => s.error !== undefined).length, 0),
251
+ metricErrors: allTrials.reduce((n, r) => n + r.scores.filter((s) => s.error !== undefined).length, 0),
178
252
  duration: Date.now() - suiteStart,
179
253
  cost: targetCost + judgeCost,
180
254
  targetCost,
@@ -188,13 +262,7 @@ export class EvalSuite {
188
262
  stats,
189
263
  report: (type, options) => {
190
264
  report({
191
- results: orderedResults.map((r) => ({
192
- case: { input: r.case.input, expected: r.case.expected },
193
- output: r.output,
194
- duration: r.duration,
195
- ...(r.error !== undefined && { error: r.error }),
196
- scores: r.scores,
197
- })),
265
+ results: orderedResults.map(reportedCase),
198
266
  aggregated,
199
267
  assertions: assertionResults,
200
268
  stats,
@@ -234,13 +302,14 @@ export class EvalSuite {
234
302
  }
235
303
  async runCase(evalCase, signal) {
236
304
  const caseResult = await this.executeCase(evalCase, signal);
237
- if (signal?.aborted)
305
+ if (signal?.aborted || caseResult.error !== undefined)
238
306
  return { ...caseResult, scores: [] };
239
307
  return { ...caseResult, scores: await this.evaluateCaseMetrics(caseResult) };
240
308
  }
241
309
  async executeCase(evalCase, signal) {
242
310
  const start = Date.now();
243
311
  let lastError;
312
+ let lastTurns;
244
313
  let usage;
245
314
  let attempts = 0;
246
315
  for (let attempt = 0; attempt <= this.retries && !signal?.aborted; attempt++) {
@@ -252,6 +321,7 @@ export class EvalSuite {
252
321
  }
253
322
  usage = addUsage(usage, outcome.usage);
254
323
  lastError = outcome.error;
324
+ lastTurns = outcome.turns;
255
325
  }
256
326
  return {
257
327
  case: evalCase,
@@ -260,6 +330,7 @@ export class EvalSuite {
260
330
  error: errorMessage(signal?.aborted ? signal.reason : lastError),
261
331
  attempts,
262
332
  ...(usage && { usage }),
333
+ ...(lastTurns && { turns: lastTurns }),
263
334
  };
264
335
  }
265
336
  /**
@@ -280,10 +351,12 @@ export class EvalSuite {
280
351
  try {
281
352
  const first = await Promise.race([settled, abort.aborted]);
282
353
  if (first !== 'aborted' && (first.ok || !signal.aborted))
283
- return first;
354
+ return failureDetails(first);
284
355
  const late = await waitAtMost(settled, this.timeout);
285
- const usage = late?.ok ? late.result.usage : undefined;
286
- return { ok: false, error: signal.reason, ...(usage && { usage }) };
356
+ const lateOutcome = late && failureDetails(late);
357
+ const usage = lateOutcome?.ok ? lateOutcome.result.usage : lateOutcome?.usage;
358
+ const turns = lateOutcome && !lateOutcome.ok ? lateOutcome.turns : undefined;
359
+ return { ok: false, error: signal.reason, ...(usage && { usage }), ...(turns && { turns }) };
287
360
  }
288
361
  finally {
289
362
  clearTimeout(timer);
@@ -292,31 +365,105 @@ export class EvalSuite {
292
365
  }
293
366
  async executeFnTarget(evalCase, context) {
294
367
  const start = Date.now();
295
- const output = await this.target.fn(evalCase.input, context);
296
- return { case: evalCase, output, duration: Date.now() - start };
368
+ const inputs = turnInputs(evalCase);
369
+ const multiTurn = inputs.length > 1;
370
+ const turns = [];
371
+ for (const [turn, input] of inputs.entries()) {
372
+ if (turn > 0 && context.signal.aborted) {
373
+ throw new TurnFailure(context.signal.reason, [...turns], undefined);
374
+ }
375
+ const history = turns.map(({ input, output }) => ({ input, output }));
376
+ try {
377
+ const output = await this.target.fn(input, { ...context, turn, history });
378
+ turns.push({ input, output });
379
+ }
380
+ catch (error) {
381
+ if (!multiTurn)
382
+ throw error;
383
+ throw new TurnFailure(error, [...turns, { input, output: '', error: errorMessage(error) }], undefined);
384
+ }
385
+ }
386
+ return {
387
+ case: evalCase,
388
+ output: turns[turns.length - 1].output,
389
+ duration: Date.now() - start,
390
+ ...(multiTurn && { turns }),
391
+ };
297
392
  }
393
+ /**
394
+ * Runs the agent on each turn of the case. A case with further `turns` runs them all on one
395
+ * thread of its own, so the Cogitator's memory carries the conversation from turn to turn
396
+ */
298
397
  async executeAgentTarget(evalCase, signal) {
299
398
  const start = Date.now();
300
399
  const cogitator = this.target.cogitator;
301
- const runResult = await cogitator.run(this.target.agent, {
302
- input: evalCase.input,
303
- context: evalCase.context,
304
- signal,
305
- });
306
- const duration = Date.now() - start;
307
- const result = {
400
+ const retrieval = this.target.retrieval ?? ragSearchPassages;
401
+ const inputs = turnInputs(evalCase);
402
+ const threadId = inputs.length > 1 ? `eval-${randomUUID()}` : undefined;
403
+ const turns = [];
404
+ const toolCalls = [];
405
+ const toolResults = [];
406
+ const retrieved = [];
407
+ let reportsToolCalls = false;
408
+ let usage;
409
+ const flags = {};
410
+ for (const input of inputs) {
411
+ if (turns.length > 0 && signal.aborted) {
412
+ throw new TurnFailure(signal.reason, [...turns], usage);
413
+ }
414
+ const turnResults = [];
415
+ let runResult;
416
+ try {
417
+ runResult = await cogitator.run(this.target.agent, {
418
+ input,
419
+ context: evalCase.context,
420
+ signal,
421
+ ...(threadId !== undefined && { threadId }),
422
+ onToolResult: (toolResult) => turnResults.push(toolResultOf(toolResult)),
423
+ });
424
+ }
425
+ catch (error) {
426
+ if (threadId === undefined)
427
+ throw error;
428
+ throw new TurnFailure(error, [...turns, { input, output: '', error: errorMessage(error) }], usage);
429
+ }
430
+ const output = typeof runResult.output === 'string' ? runResult.output : '';
431
+ const turnCalls = Array.isArray(runResult.toolCalls)
432
+ ? runResult.toolCalls
433
+ : undefined;
434
+ if (turnCalls)
435
+ reportsToolCalls = true;
436
+ toolCalls.push(...(turnCalls ?? []));
437
+ toolResults.push(...turnResults);
438
+ for (const toolResult of turnResults) {
439
+ if (toolResult.error !== undefined)
440
+ continue;
441
+ retrieved.push(...(retrieval({ name: toolResult.name, result: toolResult.result }) ?? []));
442
+ }
443
+ usage = addUsage(usage, targetUsageOf(runResult.usage));
444
+ if (runResult.truncated === true)
445
+ flags.truncated = true;
446
+ if (runResult.iterationLimitReached === true)
447
+ flags.iterationLimitReached = true;
448
+ if (runResult.status === 'paused')
449
+ flags.paused = true;
450
+ turns.push({
451
+ input,
452
+ output,
453
+ ...(turnCalls && turnCalls.length > 0 && { toolCalls: turnCalls }),
454
+ });
455
+ }
456
+ return {
308
457
  case: evalCase,
309
- output: runResult.output ?? '',
310
- duration,
458
+ output: turns[turns.length - 1].output,
459
+ duration: Date.now() - start,
460
+ ...(usage && { usage }),
461
+ ...(reportsToolCalls && { toolCalls }),
462
+ ...(toolResults.length > 0 && { toolResults }),
463
+ ...(retrieved.length > 0 && { retrieved }),
464
+ ...(threadId !== undefined && { turns }),
465
+ ...flags,
311
466
  };
312
- const usage = targetUsageOf(runResult.usage);
313
- if (usage) {
314
- result.usage = usage;
315
- }
316
- if (runResult.toolCalls) {
317
- result.toolCalls = runResult.toolCalls;
318
- }
319
- return result;
320
467
  }
321
468
  async evaluateCaseMetrics(result) {
322
469
  if (this.boundMetrics.length === 0)
@@ -324,9 +471,11 @@ export class EvalSuite {
324
471
  return Promise.all(this.boundMetrics.map((m) => this.scoreMetric(m, result)));
325
472
  }
326
473
  /**
327
- * A metric's score for one case. A metric that throws, or whose score is not a finite number,
328
- * scores 0 with the reason in `error`, so one bad metric neither drops the case nor poisons
329
- * the aggregate with NaN.
474
+ * A metric's score for one case. Only cases with an output are scored: a case whose every
475
+ * attempt failed is left unscored, so its empty output neither costs a judge call nor counts as
476
+ * a score of 0 (`stats.errors` and the `maxErrorRate` assertion catch those). A metric that
477
+ * throws, or whose score is not a finite number, scores 0 with the reason in `error`, so one
478
+ * bad metric neither drops the case nor poisons the aggregate with NaN.
330
479
  */
331
480
  async scoreMetric(metric, result) {
332
481
  try {
@@ -352,23 +501,75 @@ export class EvalSuite {
352
501
  return { name: metric.metricName, score: 0, details: `metric error: ${error}`, error };
353
502
  }
354
503
  }
504
+ /** One score per case and metric: the mean over the case's trials the metric scored */
355
505
  aggregateScores(results) {
356
506
  const scoresByMetric = new Map();
507
+ const push = (map, name, value) => {
508
+ const values = map.get(name);
509
+ if (values)
510
+ values.push(value);
511
+ else
512
+ map.set(name, [value]);
513
+ };
357
514
  for (const r of results) {
358
- for (const s of r.scores) {
359
- let arr = scoresByMetric.get(s.name);
360
- if (!arr) {
361
- arr = [];
362
- scoresByMetric.set(s.name, arr);
363
- }
364
- arr.push(s.score);
515
+ const caseScores = new Map();
516
+ for (const trial of trialsOf(r)) {
517
+ for (const s of trial.scores)
518
+ push(caseScores, s.name, s.score);
365
519
  }
520
+ for (const [name, values] of caseScores)
521
+ push(scoresByMetric, name, mean(values));
366
522
  }
367
523
  const aggregated = {};
368
524
  for (const [name, values] of scoresByMetric) {
369
- aggregated[name] = { name, ...aggregate(values) };
525
+ aggregated[name] = { name, ...aggregate(values), count: values.length };
526
+ }
527
+ if (results.length > 0 && results.flatMap(trialsOf).every((r) => r.error !== undefined)) {
528
+ for (const metric of this.boundMetrics) {
529
+ aggregated[metric.metricName] ??= unscored(metric.metricName);
530
+ }
370
531
  }
371
532
  return aggregated;
372
533
  }
373
534
  }
535
+ function trialsOf(result) {
536
+ return result.trials ?? [result];
537
+ }
538
+ function toolResultOf(toolResult) {
539
+ return {
540
+ callId: toolResult.callId,
541
+ name: toolResult.name,
542
+ ...(toolResult.result !== undefined && { result: toolResult.result }),
543
+ ...(toolResult.error !== undefined && { error: toolResult.error }),
544
+ };
545
+ }
546
+ /** The turns and usage a failed attempt leaves, when a turn of a multi-turn case failed */
547
+ function failureDetails(settled) {
548
+ if (settled.ok || !(settled.error instanceof TurnFailure))
549
+ return settled;
550
+ const { turns, usage } = settled.error;
551
+ return { ok: false, error: settled.error, turns, ...(usage && { usage }) };
552
+ }
553
+ /** A case result as the reporters get it, the agent's data and the trials kept */
554
+ /** The trial a case is reported by: the first that answered, the first when every trial errored */
555
+ function leadTrial(trials) {
556
+ return trials.find((trial) => trial.error === undefined) ?? trials[0];
557
+ }
558
+ function reportedCase(r) {
559
+ return {
560
+ case: { input: r.case.input, expected: r.case.expected },
561
+ output: r.output,
562
+ duration: r.duration,
563
+ ...(r.error !== undefined && { error: r.error }),
564
+ ...(r.truncated && { truncated: true }),
565
+ ...(r.iterationLimitReached && { iterationLimitReached: true }),
566
+ ...(r.paused && { paused: true }),
567
+ ...(r.toolCalls && { toolCalls: r.toolCalls }),
568
+ ...(r.toolResults && { toolResults: r.toolResults }),
569
+ ...(r.retrieved && { retrieved: r.retrieved }),
570
+ ...(r.turns && { turns: r.turns }),
571
+ scores: r.scores,
572
+ ...(r.trials && { trials: r.trials.map(reportedCase) }),
573
+ };
574
+ }
374
575
  //# sourceMappingURL=eval-suite.js.map