@graphorin/evals 0.6.0 → 0.7.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/CHANGELOG.md +30 -0
- package/README.md +29 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/loaders/locomo.d.ts.map +1 -1
- package/dist/loaders/locomo.js +24 -2
- package/dist/loaders/locomo.js.map +1 -1
- package/dist/loaders/memory-eval.d.ts +8 -0
- package/dist/loaders/memory-eval.d.ts.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +10 -9
- package/src/cli/index.ts +124 -0
- package/src/index.ts +136 -0
- package/src/loaders/csv.ts +147 -0
- package/src/loaders/dmr.ts +137 -0
- package/src/loaders/from-traces.ts +92 -0
- package/src/loaders/index.ts +46 -0
- package/src/loaders/iterable.ts +22 -0
- package/src/loaders/jsonl.ts +94 -0
- package/src/loaders/locomo.ts +217 -0
- package/src/loaders/longmemeval.ts +164 -0
- package/src/loaders/memory-eval.ts +80 -0
- package/src/regression.ts +133 -0
- package/src/reporters/html.ts +83 -0
- package/src/reporters/index.ts +14 -0
- package/src/reporters/json.ts +18 -0
- package/src/reporters/junit.ts +53 -0
- package/src/reporters/markdown.ts +65 -0
- package/src/reporters/terminal.ts +46 -0
- package/src/runner.ts +233 -0
- package/src/scorers/code/exact-match.ts +95 -0
- package/src/scorers/code/index.ts +10 -0
- package/src/scorers/code/json-path.ts +101 -0
- package/src/scorers/code/predicate.ts +32 -0
- package/src/scorers/code/regex.ts +55 -0
- package/src/scorers/index.ts +15 -0
- package/src/scorers/llm/index.ts +13 -0
- package/src/scorers/llm/judge.ts +170 -0
- package/src/scorers/prebuilt/index.ts +95 -0
- package/src/scorers/trajectory/argument-validity.ts +63 -0
- package/src/scorers/trajectory/correct-tool-selected.ts +61 -0
- package/src/scorers/trajectory/final-state-correct.ts +62 -0
- package/src/scorers/trajectory/index.ts +25 -0
- package/src/scorers/trajectory/recovery-after-error.ts +55 -0
- package/src/scorers/trajectory/redundant-call-detection.ts +58 -0
- package/src/scorers/trajectory/types.ts +44 -0
- package/src/scorers/trajectory/util.ts +81 -0
- package/src/stats.ts +196 -0
- package/src/types.ts +135 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the trajectory scorer family. A {@link Trajectory} is
|
|
3
|
+
* the recorded sequence of tool calls a harness made while attempting a
|
|
4
|
+
* task, plus an optional goal-state snapshot and the final text output.
|
|
5
|
+
*
|
|
6
|
+
* The scorers in this folder are pure functions over a `Trajectory` - no
|
|
7
|
+
* network, no model - so they can gate harness reliability in CI. Build a
|
|
8
|
+
* `Trajectory` by folding an agent's `AgentEvent` stream (correlate
|
|
9
|
+
* `tool.call.start` / `tool.call.end` with `tool.execute.end` /
|
|
10
|
+
* `tool.execute.error` by `toolCallId`).
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One executed tool call as observed on the `AgentEvent` stream.
|
|
17
|
+
*
|
|
18
|
+
* @stable
|
|
19
|
+
*/
|
|
20
|
+
export interface TrajectoryToolCall {
|
|
21
|
+
readonly toolCallId: string;
|
|
22
|
+
readonly toolName: string;
|
|
23
|
+
/** The arguments the model emitted (the resolved `tool.call.end.finalArgs`). */
|
|
24
|
+
readonly args: unknown;
|
|
25
|
+
/** `'ok'` when the call returned; `'error'` when the executor surfaced a `ToolError`. */
|
|
26
|
+
readonly status: 'ok' | 'error';
|
|
27
|
+
/** The tool output, present when `status === 'ok'`. */
|
|
28
|
+
readonly result?: unknown;
|
|
29
|
+
/** The surfaced error, present when `status === 'error'`. */
|
|
30
|
+
readonly error?: { readonly kind?: string; readonly message?: string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The full record of a single harness attempt at a task.
|
|
35
|
+
*
|
|
36
|
+
* @stable
|
|
37
|
+
*/
|
|
38
|
+
export interface Trajectory {
|
|
39
|
+
readonly calls: ReadonlyArray<TrajectoryToolCall>;
|
|
40
|
+
/** Goal-state snapshot compared by {@link finalStateCorrect}. */
|
|
41
|
+
readonly finalState?: unknown;
|
|
42
|
+
/** The assistant's final text, when the run completed. */
|
|
43
|
+
readonly finalOutput?: string;
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared by the trajectory scorers: structural deep
|
|
3
|
+
* equality, dot-path reads, and an order-insensitive canonical key used to
|
|
4
|
+
* de-duplicate tool calls by `(name, args)`. Private to the family - not
|
|
5
|
+
* exported from the package surface.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Structural deep-equality with order-insensitive object key comparison. */
|
|
11
|
+
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
12
|
+
if (a === b) return true;
|
|
13
|
+
if (typeof a !== typeof b) return false;
|
|
14
|
+
if (a === null || b === null) return a === b;
|
|
15
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
16
|
+
if (a.length !== b.length) return false;
|
|
17
|
+
for (let i = 0; i < a.length; i++) {
|
|
18
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
23
|
+
const aObj = a as Record<string, unknown>;
|
|
24
|
+
const bObj = b as Record<string, unknown>;
|
|
25
|
+
const aKeys = Object.keys(aObj).sort();
|
|
26
|
+
const bKeys = Object.keys(bObj).sort();
|
|
27
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
28
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
29
|
+
const key = aKeys[i];
|
|
30
|
+
if (key === undefined || key !== bKeys[i]) return false;
|
|
31
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Read a dot-separated path (`'orders.0.status'`) out of a value. */
|
|
39
|
+
export function readPath(root: unknown, path: string): unknown {
|
|
40
|
+
if (path.length === 0) return root;
|
|
41
|
+
let cursor: unknown = root;
|
|
42
|
+
for (const segment of path.split('.')) {
|
|
43
|
+
if (cursor === null || cursor === undefined) return undefined;
|
|
44
|
+
if (Array.isArray(cursor)) {
|
|
45
|
+
const idx = Number.parseInt(segment, 10);
|
|
46
|
+
if (!Number.isFinite(idx)) return undefined;
|
|
47
|
+
cursor = cursor[idx];
|
|
48
|
+
} else if (typeof cursor === 'object') {
|
|
49
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
50
|
+
} else {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return cursor;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Order-insensitive canonical JSON, so `{a,b}` and `{b,a}` key alike. */
|
|
58
|
+
export function canonicalize(value: unknown): string {
|
|
59
|
+
if (value === null || typeof value !== 'object') {
|
|
60
|
+
return JSON.stringify(value) ?? 'null';
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return `[${value.map(canonicalize).join(',')}]`;
|
|
64
|
+
}
|
|
65
|
+
const obj = value as Record<string, unknown>;
|
|
66
|
+
const keys = Object.keys(obj).sort();
|
|
67
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',')}}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function truncate(s: string, max = 80): string {
|
|
71
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function stringifySafe(value: unknown): string {
|
|
75
|
+
if (value === undefined) return 'undefined';
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(value) ?? String(value);
|
|
78
|
+
} catch {
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/stats.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared eval statistics (audit 2026-07-04, E8; closes the evals-05 /
|
|
3
|
+
* evals-08 gap): confidence intervals, pass^k, and paired significance
|
|
4
|
+
* for benchmark deltas. Everything here is pure and dependency-free so
|
|
5
|
+
* the runner, the regression gate, and the benchmarks can share one
|
|
6
|
+
* implementation instead of each inlining its own mean/stddev.
|
|
7
|
+
*
|
|
8
|
+
* Statistical choices, briefly:
|
|
9
|
+
* - **Wilson score interval** for pass-rate CIs. Eval suites are small
|
|
10
|
+
* (n in the tens) and pass rates hug 0/1, exactly where the normal
|
|
11
|
+
* approximation interval collapses; Wilson stays sane there.
|
|
12
|
+
* - **pass^k** ("pass-hat-k", the fraction of base cases that pass in
|
|
13
|
+
* ALL k repeat iterations) as the stability metric for `iterations>1`
|
|
14
|
+
* runs - a mean pass rate hides a case that flips every other run.
|
|
15
|
+
* - **McNemar's test** (continuity-corrected normal approximation) for
|
|
16
|
+
* "did the pass rate really change vs the baseline": the same cases
|
|
17
|
+
* ran in both reports, so a PAIRED test on the discordant cases has
|
|
18
|
+
* far more power than comparing two independent proportions - and it
|
|
19
|
+
* is sample-size aware, which the fixed-tolerance regression gate is
|
|
20
|
+
* not.
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Arithmetic mean; `0` for an empty list.
|
|
27
|
+
*
|
|
28
|
+
* @stable
|
|
29
|
+
*/
|
|
30
|
+
export function mean(values: ReadonlyArray<number>): number {
|
|
31
|
+
if (values.length === 0) return 0;
|
|
32
|
+
let sum = 0;
|
|
33
|
+
for (const v of values) sum += v;
|
|
34
|
+
return sum / values.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sample standard deviation (n-1 denominator); `0` for n < 2.
|
|
39
|
+
*
|
|
40
|
+
* @stable
|
|
41
|
+
*/
|
|
42
|
+
export function sampleStddev(values: ReadonlyArray<number>): number {
|
|
43
|
+
if (values.length < 2) return 0;
|
|
44
|
+
const m = mean(values);
|
|
45
|
+
let acc = 0;
|
|
46
|
+
for (const v of values) acc += (v - m) * (v - m);
|
|
47
|
+
return Math.sqrt(acc / (values.length - 1));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Wilson score interval for a binomial proportion.
|
|
52
|
+
*
|
|
53
|
+
* @param passed number of successes
|
|
54
|
+
* @param total number of trials
|
|
55
|
+
* @param z normal quantile (default 1.96 = 95% two-sided)
|
|
56
|
+
* @returns `{ lo, hi }` clamped to [0, 1]; `{ lo: 0, hi: 1 }` when `total` is 0
|
|
57
|
+
*
|
|
58
|
+
* @stable
|
|
59
|
+
*/
|
|
60
|
+
export function wilsonInterval(
|
|
61
|
+
passed: number,
|
|
62
|
+
total: number,
|
|
63
|
+
z = 1.96,
|
|
64
|
+
): { readonly lo: number; readonly hi: number } {
|
|
65
|
+
if (total <= 0) return { lo: 0, hi: 1 };
|
|
66
|
+
const p = passed / total;
|
|
67
|
+
const z2 = z * z;
|
|
68
|
+
const denom = 1 + z2 / total;
|
|
69
|
+
const centre = p + z2 / (2 * total);
|
|
70
|
+
const margin = z * Math.sqrt((p * (1 - p)) / total + z2 / (4 * total * total));
|
|
71
|
+
return {
|
|
72
|
+
lo: Math.max(0, (centre - margin) / denom),
|
|
73
|
+
hi: Math.min(1, (centre + margin) / denom),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ITERATION_SUFFIX_RE = /-iter-\d+$/;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Strip the `-iter-N` disambiguation suffix the runner appends under
|
|
81
|
+
* `iterations > 1` (EB-6), recovering the base case id.
|
|
82
|
+
*
|
|
83
|
+
* @stable
|
|
84
|
+
*/
|
|
85
|
+
export function stripIterationSuffix(caseId: string): string {
|
|
86
|
+
return caseId.replace(ITERATION_SUFFIX_RE, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* pass^k over per-iteration case outcomes: group by base case id and
|
|
91
|
+
* report the fraction of base cases whose EVERY iteration passed.
|
|
92
|
+
*
|
|
93
|
+
* @param outcomes one entry per executed case iteration
|
|
94
|
+
* @returns `k` = the largest group size observed, `baseCases` = number of
|
|
95
|
+
* distinct base cases, `value` = the pass^k fraction (0 when no cases)
|
|
96
|
+
*
|
|
97
|
+
* @stable
|
|
98
|
+
*/
|
|
99
|
+
export function passHatK(
|
|
100
|
+
outcomes: ReadonlyArray<{ readonly caseId: string; readonly pass: boolean }>,
|
|
101
|
+
): { readonly k: number; readonly baseCases: number; readonly value: number } {
|
|
102
|
+
const groups = new Map<string, { total: number; passed: number }>();
|
|
103
|
+
for (const o of outcomes) {
|
|
104
|
+
const base = stripIterationSuffix(o.caseId);
|
|
105
|
+
const g = groups.get(base) ?? { total: 0, passed: 0 };
|
|
106
|
+
g.total += 1;
|
|
107
|
+
if (o.pass) g.passed += 1;
|
|
108
|
+
groups.set(base, g);
|
|
109
|
+
}
|
|
110
|
+
if (groups.size === 0) return { k: 0, baseCases: 0, value: 0 };
|
|
111
|
+
let k = 0;
|
|
112
|
+
let allPass = 0;
|
|
113
|
+
for (const g of groups.values()) {
|
|
114
|
+
if (g.total > k) k = g.total;
|
|
115
|
+
if (g.passed === g.total) allPass += 1;
|
|
116
|
+
}
|
|
117
|
+
return { k, baseCases: groups.size, value: allPass / groups.size };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Result of {@link pairedPassSignificance}.
|
|
122
|
+
*
|
|
123
|
+
* @stable
|
|
124
|
+
*/
|
|
125
|
+
export interface PairedSignificance {
|
|
126
|
+
/** Cases present in BOTH runs (the paired sample). */
|
|
127
|
+
readonly pairs: number;
|
|
128
|
+
/** baseline-pass -> current-fail count (the regressions). */
|
|
129
|
+
readonly regressed: number;
|
|
130
|
+
/** baseline-fail -> current-pass count (the improvements). */
|
|
131
|
+
readonly improved: number;
|
|
132
|
+
/**
|
|
133
|
+
* Two-sided p-value from McNemar's test (continuity-corrected normal
|
|
134
|
+
* approximation over the discordant pairs). `1` when there are no
|
|
135
|
+
* discordant pairs - identical outcomes are never "significant".
|
|
136
|
+
*/
|
|
137
|
+
readonly pValue: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* McNemar's paired test on per-case pass outcomes of two eval runs.
|
|
142
|
+
* Only cases present in both maps are compared (by base case id).
|
|
143
|
+
*
|
|
144
|
+
* @stable
|
|
145
|
+
*/
|
|
146
|
+
export function pairedPassSignificance(
|
|
147
|
+
current: ReadonlyMap<string, boolean>,
|
|
148
|
+
baseline: ReadonlyMap<string, boolean>,
|
|
149
|
+
): PairedSignificance {
|
|
150
|
+
let pairs = 0;
|
|
151
|
+
let regressed = 0;
|
|
152
|
+
let improved = 0;
|
|
153
|
+
for (const [caseId, basePass] of baseline) {
|
|
154
|
+
const curPass = current.get(caseId);
|
|
155
|
+
if (curPass === undefined) continue;
|
|
156
|
+
pairs += 1;
|
|
157
|
+
if (basePass && !curPass) regressed += 1;
|
|
158
|
+
else if (!basePass && curPass) improved += 1;
|
|
159
|
+
}
|
|
160
|
+
const discordant = regressed + improved;
|
|
161
|
+
if (discordant === 0) return { pairs, regressed, improved, pValue: 1 };
|
|
162
|
+
// Continuity-corrected normal approximation to the binomial(discordant, 0.5).
|
|
163
|
+
const z = (Math.abs(regressed - improved) - 1) / Math.sqrt(discordant);
|
|
164
|
+
const pValue = z <= 0 ? 1 : Math.min(1, 2 * (1 - normalCdf(z)));
|
|
165
|
+
return { pairs, regressed, improved, pValue };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Collapse per-iteration outcomes to per-base-case pass (a base case
|
|
170
|
+
* passes when EVERY iteration passed) - the paired unit for
|
|
171
|
+
* {@link pairedPassSignificance}.
|
|
172
|
+
*
|
|
173
|
+
* @stable
|
|
174
|
+
*/
|
|
175
|
+
export function passByBaseCase(
|
|
176
|
+
outcomes: ReadonlyArray<{ readonly caseId: string; readonly pass: boolean }>,
|
|
177
|
+
): ReadonlyMap<string, boolean> {
|
|
178
|
+
const out = new Map<string, boolean>();
|
|
179
|
+
for (const o of outcomes) {
|
|
180
|
+
const base = stripIterationSuffix(o.caseId);
|
|
181
|
+
const prev = out.get(base);
|
|
182
|
+
out.set(base, prev === undefined ? o.pass : prev && o.pass);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Standard normal CDF via the Abramowitz-Stegun 7.1.26 erf approximation. */
|
|
188
|
+
function normalCdf(x: number): number {
|
|
189
|
+
const t = 1 / (1 + 0.3275911 * (Math.abs(x) / Math.SQRT2));
|
|
190
|
+
const erf =
|
|
191
|
+
1 -
|
|
192
|
+
((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) *
|
|
193
|
+
t *
|
|
194
|
+
Math.exp(-(x * x) / 2);
|
|
195
|
+
return x >= 0 ? 0.5 * (1 + erf) : 0.5 * (1 - erf);
|
|
196
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for the full eval framework. Re-exports the
|
|
3
|
+
* {@link Case} / {@link Dataset} / {@link EvalReport} primitives from
|
|
4
|
+
* `@graphorin/observability/eval` so consumers do not need to depend
|
|
5
|
+
* on both packages, then layers on the framework-specific concepts:
|
|
6
|
+
* agent shape, parallel runner config, regression policy, baseline
|
|
7
|
+
* loaders.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
Case,
|
|
14
|
+
Dataset,
|
|
15
|
+
EvalCaseResult,
|
|
16
|
+
EvalReport,
|
|
17
|
+
RunEvalOptions,
|
|
18
|
+
ScoreResult,
|
|
19
|
+
Scorer,
|
|
20
|
+
} from '@graphorin/observability/eval';
|
|
21
|
+
|
|
22
|
+
import type { EvalReport, Scorer } from '@graphorin/observability/eval';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Agent shape consumed by the runner. Anything with a `run(input)`
|
|
26
|
+
* method satisfies the contract - the framework's own `Agent` type
|
|
27
|
+
* matches by structural typing.
|
|
28
|
+
*
|
|
29
|
+
* @stable
|
|
30
|
+
*/
|
|
31
|
+
export interface AgentLike<I, O> {
|
|
32
|
+
run(input: I, ctx?: { signal?: AbortSignal }): Promise<O>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options accepted by the parallel runner.
|
|
37
|
+
*
|
|
38
|
+
* @stable
|
|
39
|
+
*/
|
|
40
|
+
export interface RunOptions<I, O> {
|
|
41
|
+
readonly agent: AgentLike<I, O>;
|
|
42
|
+
readonly dataset: {
|
|
43
|
+
readonly cases: ReadonlyArray<{
|
|
44
|
+
readonly id?: string;
|
|
45
|
+
readonly input: I;
|
|
46
|
+
readonly expected?: O;
|
|
47
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
readonly scorers: ReadonlyArray<Scorer<I, O>>;
|
|
51
|
+
/** Default `1`. */
|
|
52
|
+
readonly iterations?: number;
|
|
53
|
+
/** Default `1` (sequential). Set higher for parallel evaluation. */
|
|
54
|
+
readonly concurrency?: number;
|
|
55
|
+
readonly signal?: AbortSignal;
|
|
56
|
+
/**
|
|
57
|
+
* How to handle `signal` abort. Default (`false`): stop dispatching and
|
|
58
|
+
* resolve with a **partial** {@link EvalReport} (`aborted: true`) covering
|
|
59
|
+
* the cases that finished - a long judged run shouldn't lose all completed
|
|
60
|
+
* work to a Ctrl+C. Set `true` to throw the abort reason instead.
|
|
61
|
+
*/
|
|
62
|
+
readonly throwOnAbort?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Optional progress hook invoked after every case. Useful for
|
|
65
|
+
* terminal reporters that want a per-case heartbeat.
|
|
66
|
+
*/
|
|
67
|
+
readonly onProgress?: (event: ProgressEvent) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @stable */
|
|
71
|
+
export interface ProgressEvent {
|
|
72
|
+
readonly index: number;
|
|
73
|
+
readonly total: number;
|
|
74
|
+
readonly caseId: string;
|
|
75
|
+
readonly durationMs: number;
|
|
76
|
+
readonly passed: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Regression-detection options.
|
|
81
|
+
*
|
|
82
|
+
* @stable
|
|
83
|
+
*/
|
|
84
|
+
export interface RegressionOptions {
|
|
85
|
+
/** Minimum drop in pass-rate (in percentage points) that counts as a regression. */
|
|
86
|
+
readonly maxPassRateDropPct?: number;
|
|
87
|
+
/** Minimum drop in average score per scorer that counts as a regression. */
|
|
88
|
+
readonly maxAvgScoreDrop?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Maximum allowed increase in `avgDurationMs` before it counts as a
|
|
91
|
+
* regression. **Opt-in: defaults to `Infinity` (gate off)** because absolute
|
|
92
|
+
* wall-clock budgets are environment-sensitive (workstation baseline vs CI
|
|
93
|
+
* runner, real LLM-latency jitter). Pass a finite ms budget to enable an
|
|
94
|
+
* absolute duration gate; leave unset to ignore duration entirely.
|
|
95
|
+
*/
|
|
96
|
+
readonly maxAvgDurationIncreaseMs?: number;
|
|
97
|
+
/**
|
|
98
|
+
* E8 (evals-05/08): when `true`, a `pass-rate-drop` finding is only kept if
|
|
99
|
+
* McNemar's paired test over the shared cases rejects "no real change" at
|
|
100
|
+
* {@link significanceAlpha} - a fixed percentage tolerance is blind to
|
|
101
|
+
* sample size (a 5pp drop is one case in a 20-case suite). Off by default
|
|
102
|
+
* so existing gates keep their exact behavior; the computed `pValue` is
|
|
103
|
+
* attached to the finding either way whenever both reports carry per-case
|
|
104
|
+
* results.
|
|
105
|
+
*/
|
|
106
|
+
readonly requireSignificance?: boolean;
|
|
107
|
+
/** Significance level for {@link requireSignificance}. Default `0.05`. */
|
|
108
|
+
readonly significanceAlpha?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Result of {@link detectRegressions}.
|
|
113
|
+
*
|
|
114
|
+
* @stable
|
|
115
|
+
*/
|
|
116
|
+
export interface RegressionReport<I, O> {
|
|
117
|
+
readonly hasRegressions: boolean;
|
|
118
|
+
readonly findings: ReadonlyArray<RegressionFinding>;
|
|
119
|
+
readonly current: EvalReport<I, O>;
|
|
120
|
+
readonly baseline: EvalReport<I, O>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @stable */
|
|
124
|
+
export interface RegressionFinding {
|
|
125
|
+
readonly kind: 'pass-rate-drop' | 'avg-score-drop' | 'avg-duration-increase' | 'scorer-removed';
|
|
126
|
+
readonly scorer?: string;
|
|
127
|
+
readonly message: string;
|
|
128
|
+
readonly delta: number;
|
|
129
|
+
/**
|
|
130
|
+
* Two-sided McNemar p-value over the cases shared by both reports
|
|
131
|
+
* (E8; only on `pass-rate-drop` findings, and only when both reports
|
|
132
|
+
* carry per-case results to pair on).
|
|
133
|
+
*/
|
|
134
|
+
readonly pValue?: number;
|
|
135
|
+
}
|