@namzu/sdk 62.11.0 → 62.13.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 +29 -0
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +96 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +488 -13
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/tasks.d.ts +40 -0
- package/dist/connector/mcp/tasks.d.ts.map +1 -0
- package/dist/connector/mcp/tasks.js +55 -0
- package/dist/connector/mcp/tasks.js.map +1 -0
- package/dist/constants/mcp/index.d.ts +1 -1
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +5 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/eval/bootstrap.d.ts +122 -0
- package/dist/eval/bootstrap.d.ts.map +1 -0
- package/dist/eval/bootstrap.js +227 -0
- package/dist/eval/bootstrap.js.map +1 -0
- package/dist/eval/compare.d.ts +109 -0
- package/dist/eval/compare.d.ts.map +1 -0
- package/dist/eval/compare.js +324 -0
- package/dist/eval/compare.js.map +1 -0
- package/dist/eval/disclosure.d.ts +70 -0
- package/dist/eval/disclosure.d.ts.map +1 -0
- package/dist/eval/disclosure.js +64 -0
- package/dist/eval/disclosure.js.map +1 -0
- package/dist/eval/experiment.d.ts +23 -0
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +87 -15
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/from-turn.d.ts.map +1 -1
- package/dist/eval/from-turn.js +5 -0
- package/dist/eval/from-turn.js.map +1 -1
- package/dist/eval/index.d.ts +13 -2
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +6 -1
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/report-stats.d.ts +77 -0
- package/dist/eval/report-stats.d.ts.map +1 -0
- package/dist/eval/report-stats.js +184 -0
- package/dist/eval/report-stats.js.map +1 -0
- package/dist/eval/run-options.d.ts +30 -0
- package/dist/eval/run-options.d.ts.map +1 -0
- package/dist/eval/run-options.js +20 -0
- package/dist/eval/run-options.js.map +1 -0
- package/dist/eval/run-summary.d.ts +36 -0
- package/dist/eval/run-summary.d.ts.map +1 -0
- package/dist/eval/run-summary.js +57 -0
- package/dist/eval/run-summary.js.map +1 -0
- package/dist/eval/scorers.d.ts +32 -0
- package/dist/eval/scorers.d.ts.map +1 -1
- package/dist/eval/scorers.js +77 -0
- package/dist/eval/scorers.js.map +1 -1
- package/dist/eval/types.d.ts +74 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +3 -0
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +20 -5
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/public-runtime.d.ts +5 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +3 -0
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +76 -5
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/query/abandon-turn.d.ts.map +1 -1
- package/dist/runtime/query/abandon-turn.js +2 -2
- package/dist/runtime/query/abandon-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +15 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +18 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +80 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
- package/dist/runtime/query/prelude-lease.js +2 -2
- package/dist/runtime/query/prelude-lease.js.map +1 -1
- package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
- package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
- package/dist/runtime/query/turn/answer-claims.js +348 -0
- package/dist/runtime/query/turn/answer-claims.js.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.js +13 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
- package/dist/store/goal/index.d.ts +3 -1
- package/dist/store/goal/index.d.ts.map +1 -1
- package/dist/store/goal/index.js +6 -0
- package/dist/store/goal/index.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -0
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -0
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/store/session-log/turn-holder.d.ts +26 -0
- package/dist/store/session-log/turn-holder.d.ts.map +1 -0
- package/dist/store/session-log/turn-holder.js +79 -0
- package/dist/store/session-log/turn-holder.js.map +1 -0
- package/dist/telemetry/runtime-accessors.d.ts +14 -0
- package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
- package/dist/telemetry/runtime-accessors.js +32 -0
- package/dist/telemetry/runtime-accessors.js.map +1 -1
- package/dist/tools/goal/confirmation.d.ts +154 -0
- package/dist/tools/goal/confirmation.d.ts.map +1 -0
- package/dist/tools/goal/confirmation.js +290 -0
- package/dist/tools/goal/confirmation.js.map +1 -0
- package/dist/tools/goal/index.d.ts +4 -1
- package/dist/tools/goal/index.d.ts.map +1 -1
- package/dist/tools/goal/index.js +27 -2
- package/dist/tools/goal/index.js.map +1 -1
- package/dist/tools/hand-back/index.d.ts +48 -0
- package/dist/tools/hand-back/index.d.ts.map +1 -0
- package/dist/tools/hand-back/index.js +129 -0
- package/dist/tools/hand-back/index.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +15 -1
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/schedules/types.d.ts +10 -0
- package/dist/tools/schedules/types.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +18 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/goal/index.d.ts +12 -0
- package/dist/types/goal/index.d.ts.map +1 -1
- package/dist/types/session/config.d.ts +8 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/hand-back.d.ts +29 -0
- package/dist/types/session/hand-back.d.ts.map +1 -0
- package/dist/types/session/hand-back.js +11 -0
- package/dist/types/session/hand-back.js.map +1 -0
- package/dist/types/session/outcome.d.ts +37 -0
- package/dist/types/session/outcome.d.ts.map +1 -1
- package/dist/types/session/outcome.js.map +1 -1
- package/dist/types/session/turn.d.ts +19 -1
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/package.json +1 -1
- package/src/connector/index.ts +1 -0
- package/src/connector/mcp/client.ts +531 -14
- package/src/connector/mcp/index.ts +1 -0
- package/src/connector/mcp/tasks.ts +81 -0
- package/src/constants/mcp/index.ts +6 -1
- package/src/eval/bootstrap.ts +314 -0
- package/src/eval/compare.ts +473 -0
- package/src/eval/disclosure.ts +136 -0
- package/src/eval/experiment.ts +127 -15
- package/src/eval/from-turn.ts +5 -0
- package/src/eval/index.ts +51 -0
- package/src/eval/report-stats.ts +257 -0
- package/src/eval/run-options.ts +49 -0
- package/src/eval/run-summary.ts +104 -0
- package/src/eval/scorers.ts +83 -0
- package/src/eval/types.ts +76 -0
- package/src/manager/session/turn-recorder.ts +25 -4
- package/src/public-runtime.ts +53 -0
- package/src/public-types.ts +9 -0
- package/src/registry/command/kernel-commands.ts +94 -6
- package/src/runtime/query/abandon-turn.ts +2 -1
- package/src/runtime/query/index.ts +18 -0
- package/src/runtime/query/iteration/index.ts +85 -1
- package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
- package/src/runtime/query/prelude-lease.ts +7 -2
- package/src/runtime/query/turn/answer-claims.ts +413 -0
- package/src/runtime/query/turn/outcome-ledger.ts +12 -0
- package/src/store/goal/index.ts +11 -0
- package/src/store/session-log/index.ts +8 -0
- package/src/store/session-log/turn-holder.ts +95 -0
- package/src/telemetry/runtime-accessors.ts +29 -0
- package/src/tools/goal/confirmation.ts +375 -0
- package/src/tools/goal/index.ts +37 -1
- package/src/tools/hand-back/index.ts +150 -0
- package/src/tools/schedules/schedule-tool.ts +19 -1
- package/src/tools/schedules/types.ts +10 -0
- package/src/types/connector/mcp.ts +18 -0
- package/src/types/goal/index.ts +13 -0
- package/src/types/session/config.ts +8 -0
- package/src/types/session/hand-back.ts +31 -0
- package/src/types/session/outcome.ts +42 -0
- package/src/types/session/turn.ts +19 -1
package/src/eval/experiment.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { EvalUsageError, validateSeed } from './bootstrap.js'
|
|
2
|
+
import { type SuiteFacts, buildDisclosure, describeDisclosure } from './disclosure.js'
|
|
3
|
+
import {
|
|
4
|
+
describeIntervals,
|
|
5
|
+
describeSplits,
|
|
6
|
+
duplicateMessage,
|
|
7
|
+
duplicateName,
|
|
8
|
+
groupRuns,
|
|
9
|
+
statsOver,
|
|
10
|
+
} from './report-stats.js'
|
|
11
|
+
import { currentEvalRunOptions } from './run-options.js'
|
|
1
12
|
import type {
|
|
2
13
|
CaseResult,
|
|
3
14
|
CaseStatus,
|
|
@@ -10,6 +21,8 @@ import type {
|
|
|
10
21
|
import type { ScoreUncertainty } from './uncertainty.js'
|
|
11
22
|
import { describeUncertainty, uncertaintyOf } from './uncertainty.js'
|
|
12
23
|
|
|
24
|
+
const MAX_REPEATS = 100
|
|
25
|
+
|
|
13
26
|
const MAX_CASE_TIMEOUT_MS = 2_147_483_647
|
|
14
27
|
|
|
15
28
|
export interface ExperimentConfig<TInput = unknown> {
|
|
@@ -42,7 +55,29 @@ export interface ExperimentConfig<TInput = unknown> {
|
|
|
42
55
|
passThreshold?: number
|
|
43
56
|
/** Cases to run at once. Default 1 — deterministic ordering by default. */
|
|
44
57
|
concurrency?: number
|
|
58
|
+
/** Called once per run of a case; with repeats, once per repeat. */
|
|
45
59
|
onCaseFinish?: (result: CaseResult) => void
|
|
60
|
+
/**
|
|
61
|
+
* Times every case runs, from 1 to 100. Default 1.
|
|
62
|
+
*
|
|
63
|
+
* One run of a case says little about a model that does not answer the
|
|
64
|
+
* same way twice. The report keeps every run and says per case how many
|
|
65
|
+
* passed, and per suite how many cases passed every time. An
|
|
66
|
+
* `EvalRunOptions` scope opened by `namzu eval --repeat` wins over this.
|
|
67
|
+
*/
|
|
68
|
+
repeats?: number
|
|
69
|
+
/**
|
|
70
|
+
* Seed for the bootstrap intervals, an integer from 0 to 4294967295.
|
|
71
|
+
* Default 1, so the same cases give the same interval on every machine.
|
|
72
|
+
* Written into the report.
|
|
73
|
+
*/
|
|
74
|
+
seed?: number
|
|
75
|
+
/**
|
|
76
|
+
* What the suite knows about how it runs: scripted or a real model, which
|
|
77
|
+
* provider and model fill each role, routing, reasoning effort. Whatever is
|
|
78
|
+
* left out is written `unknown` in the report, never guessed.
|
|
79
|
+
*/
|
|
80
|
+
disclosure?: SuiteFacts
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
/**
|
|
@@ -64,23 +99,33 @@ export async function runExperiment<TInput>(
|
|
|
64
99
|
const threshold = config.passThreshold ?? 1
|
|
65
100
|
const concurrency = Math.max(1, config.concurrency ?? 1)
|
|
66
101
|
const timeoutMs = validateCaseTimeoutMs(config.timeoutMs)
|
|
102
|
+
const options = currentEvalRunOptions()
|
|
103
|
+
const repeats = validateRepeats(options.repeats ?? config.repeats)
|
|
104
|
+
const seed = validateSeed(options.seed ?? config.seed)
|
|
105
|
+
for (const evalCase of config.cases) validateSplit(evalCase)
|
|
106
|
+
const alike = duplicateName(config.cases.map((c) => c.name))
|
|
107
|
+
if (alike !== undefined) throw new Error(duplicateMessage(config.name, alike))
|
|
67
108
|
|
|
68
|
-
|
|
109
|
+
// Case-major, so a case's repeats sit together in the report.
|
|
110
|
+
const total = config.cases.length * repeats
|
|
111
|
+
const results: CaseResult[] = new Array(total)
|
|
69
112
|
let cursor = 0
|
|
70
113
|
|
|
71
114
|
const worker = async (): Promise<void> => {
|
|
72
115
|
for (;;) {
|
|
73
116
|
const index = cursor++
|
|
74
|
-
|
|
75
|
-
|
|
117
|
+
if (index >= total) return
|
|
118
|
+
const evalCase = config.cases[Math.floor(index / repeats)] as EvalCase<TInput>
|
|
76
119
|
|
|
77
120
|
const result = await evaluateCase(config, evalCase, threshold, timeoutMs)
|
|
121
|
+
result.repeat = (index % repeats) + 1
|
|
122
|
+
if (evalCase.split) result.split = evalCase.split
|
|
78
123
|
results[index] = result
|
|
79
124
|
config.onCaseFinish?.(result)
|
|
80
125
|
}
|
|
81
126
|
}
|
|
82
127
|
|
|
83
|
-
await Promise.all(Array.from({ length: Math.min(concurrency,
|
|
128
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, total) }, worker))
|
|
84
129
|
|
|
85
130
|
const settled = results.filter(Boolean)
|
|
86
131
|
// An inconclusive case has no mean to contribute; averaging its zero in
|
|
@@ -88,6 +133,11 @@ export async function runExperiment<TInput>(
|
|
|
88
133
|
const scored = settled.filter((r) => r.status !== 'inconclusive')
|
|
89
134
|
const mean = scored.length === 0 ? 0 : scored.reduce((sum, r) => sum + r.mean, 0) / scored.length
|
|
90
135
|
|
|
136
|
+
const stats = statsOver(groupRuns(settled), seed)
|
|
137
|
+
// One number per CASE, so a case's repeats are not counted as independent
|
|
138
|
+
// evidence about the suite.
|
|
139
|
+
const caseMeans = stats.caseSummaries.flatMap((c) => (c.mean === null ? [] : [c.mean]))
|
|
140
|
+
|
|
91
141
|
return {
|
|
92
142
|
name: config.name,
|
|
93
143
|
cases: settled,
|
|
@@ -95,12 +145,43 @@ export async function runExperiment<TInput>(
|
|
|
95
145
|
// Over the same cases the mean is over. Computing spread across a
|
|
96
146
|
// different denominator than the average it qualifies would produce
|
|
97
147
|
// an interval that does not belong to the number beside it.
|
|
98
|
-
uncertainty: uncertaintyOf(
|
|
148
|
+
uncertainty: uncertaintyOf(caseMeans),
|
|
99
149
|
passed: settled.filter((r) => r.status === 'passed').length,
|
|
100
150
|
failed: settled.filter((r) => r.status === 'failed').length,
|
|
101
151
|
inconclusive: settled.filter((r) => r.status === 'inconclusive').length,
|
|
102
152
|
byScorer: meanByScorer(settled),
|
|
103
153
|
durationMs: Date.now() - startedAt,
|
|
154
|
+
repeats,
|
|
155
|
+
seed,
|
|
156
|
+
...(stats.passRate === null ? {} : { passRate: stats.passRate }),
|
|
157
|
+
intervals: stats.intervals,
|
|
158
|
+
...(repeats > 1 ? { caseSummaries: stats.caseSummaries, reliability: stats.reliability } : {}),
|
|
159
|
+
splits: stats.splits,
|
|
160
|
+
disclosure: buildDisclosure(
|
|
161
|
+
config.disclosure,
|
|
162
|
+
{ cases: config.cases.length, repeats, seed },
|
|
163
|
+
options,
|
|
164
|
+
new Date().toISOString(),
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateRepeats(value: number | undefined): number {
|
|
170
|
+
if (value === undefined) return 1
|
|
171
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_REPEATS) {
|
|
172
|
+
throw new EvalUsageError(
|
|
173
|
+
`repeats must be an integer from 1 to ${MAX_REPEATS}, or omitted; received ${String(value)}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
return value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateSplit(evalCase: EvalCase<unknown>): void {
|
|
180
|
+
const split = evalCase.split as unknown
|
|
181
|
+
if (split !== undefined && split !== 'development' && split !== 'held-out') {
|
|
182
|
+
throw new RangeError(
|
|
183
|
+
`case "${evalCase.name}" declares split ${JSON.stringify(split)}; a split is "development" or "held-out", or left out`,
|
|
184
|
+
)
|
|
104
185
|
}
|
|
105
186
|
}
|
|
106
187
|
|
|
@@ -351,17 +432,46 @@ function derivedUncertainty(report: ExperimentReport): ScoreUncertainty {
|
|
|
351
432
|
}
|
|
352
433
|
|
|
353
434
|
export function formatReport(report: ExperimentReport): string {
|
|
435
|
+
const repeats = report.repeats ?? 1
|
|
436
|
+
const caseCount = repeats > 1 ? report.cases.length / repeats : report.cases.length
|
|
354
437
|
const lines: string[] = [
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// over-read: two experiments three points apart look like a difference, and
|
|
359
|
-
// at the n a hand-built suite has they are usually the same experiment
|
|
360
|
-
// twice. Computing the interval and not showing it would leave the
|
|
361
|
-
// reader exactly where they started.
|
|
362
|
-
` ${describeUncertainty(report.mean, report.uncertainty ?? derivedUncertainty(report))}`,
|
|
363
|
-
'',
|
|
438
|
+
repeats > 1
|
|
439
|
+
? `${report.name}: ${report.passed}/${report.cases.length} runs passed (${caseCount} cases x ${repeats} repeats, mean ${report.mean.toFixed(2)}) in ${report.durationMs}ms`
|
|
440
|
+
: `${report.name}: ${report.passed}/${report.cases.length} passed (mean ${report.mean.toFixed(2)}) in ${report.durationMs}ms`,
|
|
364
441
|
]
|
|
442
|
+
if (report.intervals) {
|
|
443
|
+
// The interval sits next to the number it belongs to. A mean printed
|
|
444
|
+
// alone is the thing that has been over-read: two experiments three
|
|
445
|
+
// points apart look like a difference, and at the n a hand-built
|
|
446
|
+
// suite has they are usually the same experiment twice.
|
|
447
|
+
for (const line of describeIntervals({
|
|
448
|
+
mean: report.cases.some((c) => c.status !== 'inconclusive') ? report.mean : null,
|
|
449
|
+
passRate: report.passRate ?? null,
|
|
450
|
+
intervals: report.intervals,
|
|
451
|
+
})) {
|
|
452
|
+
lines.push(` ${line}`)
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
// A report from before bootstrap intervals, or one built by hand: the
|
|
456
|
+
// older interval, still never omitted.
|
|
457
|
+
lines.push(
|
|
458
|
+
` ${describeUncertainty(report.mean, report.uncertainty ?? derivedUncertainty(report))}`,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
if (report.reliability && repeats > 1) {
|
|
462
|
+
const r = report.reliability
|
|
463
|
+
lines.push(
|
|
464
|
+
` reliability across repeats: ${r.passedEveryRepeat} of ${r.cases} cases passed in every one of ${repeats} repeats${r.fraction === null ? '' : ` (${(r.fraction * 100).toFixed(1)}%)`}`,
|
|
465
|
+
)
|
|
466
|
+
for (const c of report.caseSummaries ?? []) {
|
|
467
|
+
if (!c.passedEveryRepeat) lines.push(` ${c.case}: ${c.passed} of ${c.repeats} passed`)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (report.splits) {
|
|
471
|
+
for (const line of describeSplits(report.splits)) lines.push(` ${line}`)
|
|
472
|
+
}
|
|
473
|
+
if (report.disclosure) lines.push(` ${describeDisclosure(report.disclosure)}`)
|
|
474
|
+
lines.push('')
|
|
365
475
|
|
|
366
476
|
// Surfaced above the failures, because an inconclusive case means the
|
|
367
477
|
// harness is broken and every number below it is measured over less
|
|
@@ -381,7 +491,9 @@ export function formatReport(report: ExperimentReport): string {
|
|
|
381
491
|
if (failures.length > 0) {
|
|
382
492
|
lines.push('', 'Failures:')
|
|
383
493
|
for (const failure of failures) {
|
|
384
|
-
lines.push(
|
|
494
|
+
lines.push(
|
|
495
|
+
` ✗ ${failure.case}${repeats > 1 && failure.repeat ? ` [repeat ${failure.repeat} of ${repeats}]` : ''} (${failure.mean.toFixed(2)})`,
|
|
496
|
+
)
|
|
385
497
|
if (failure.failedGates && failure.failedGates.length > 0) {
|
|
386
498
|
// First line under the case, because it says WHY this failed.
|
|
387
499
|
// A mean of 0.75 next to four scores leaves the reader
|
package/src/eval/from-turn.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareAnswerToOutcome } from '../runtime/query/turn/answer-claims.js'
|
|
1
2
|
import type { Turn } from '../types/session/turn.js'
|
|
2
3
|
import type { EvalTurn } from './types.js'
|
|
3
4
|
|
|
@@ -12,6 +13,7 @@ import type { EvalTurn } from './types.js'
|
|
|
12
13
|
*/
|
|
13
14
|
export function evalTurnFromTurn(turn: Turn, opts: { durationMs?: number } = {}): EvalTurn {
|
|
14
15
|
const steps = turn.steps ?? []
|
|
16
|
+
const claimMismatch = compareAnswerToOutcome(turn.result, turn.outcome)
|
|
15
17
|
return {
|
|
16
18
|
output: turn.result ?? null,
|
|
17
19
|
...(turn.structuredOutput !== undefined ? { structuredOutput: turn.structuredOutput } : {}),
|
|
@@ -23,6 +25,9 @@ export function evalTurnFromTurn(turn: Turn, opts: { durationMs?: number } = {})
|
|
|
23
25
|
totalCostUsd: turn.costInfo.totalCost,
|
|
24
26
|
durationMs: opts.durationMs ?? (turn.endedAt ?? Date.now()) - turn.startedAt,
|
|
25
27
|
...(turn.lastError ? { error: turn.lastError } : {}),
|
|
28
|
+
...(turn.outcome ? { outcome: turn.outcome } : {}),
|
|
29
|
+
...(claimMismatch ? { claimMismatch } : {}),
|
|
30
|
+
...(turn.handedBack ? { handedBack: turn.handedBack } : {}),
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
package/src/eval/index.ts
CHANGED
|
@@ -15,16 +15,67 @@ export type { ExperimentConfig } from './experiment.js'
|
|
|
15
15
|
export { judgeScorer } from './judge.js'
|
|
16
16
|
export type { JudgeScorerConfig } from './judge.js'
|
|
17
17
|
export {
|
|
18
|
+
claimMismatchScorer,
|
|
19
|
+
handedBackScorer,
|
|
18
20
|
completionScorer,
|
|
19
21
|
containsScorer,
|
|
20
22
|
customScorer,
|
|
23
|
+
outcomeScorer,
|
|
21
24
|
stepBudgetScorer,
|
|
22
25
|
trajectoryScorer,
|
|
23
26
|
} from './scorers.js'
|
|
27
|
+
export {
|
|
28
|
+
BOOTSTRAP_RESAMPLES,
|
|
29
|
+
DEFAULT_EVAL_SEED,
|
|
30
|
+
EvalUsageError,
|
|
31
|
+
INTERVAL_LEVEL,
|
|
32
|
+
MIN_CASES_FOR_INTERVAL,
|
|
33
|
+
bootstrapCases,
|
|
34
|
+
bootstrapPairedDifference,
|
|
35
|
+
createSeededRandom,
|
|
36
|
+
exactSignTestP,
|
|
37
|
+
isEvalUsageError,
|
|
38
|
+
} from './bootstrap.js'
|
|
39
|
+
export type {
|
|
40
|
+
BootstrapMethod,
|
|
41
|
+
CaseIntervals,
|
|
42
|
+
CaseTally,
|
|
43
|
+
DifferenceIntervals,
|
|
44
|
+
Interval,
|
|
45
|
+
PairedTally,
|
|
46
|
+
} from './bootstrap.js'
|
|
47
|
+
export type {
|
|
48
|
+
ReportDisclosure,
|
|
49
|
+
RoleDisclosure,
|
|
50
|
+
RoutingDisclosure,
|
|
51
|
+
RunMode,
|
|
52
|
+
SuiteFacts,
|
|
53
|
+
} from './disclosure.js'
|
|
54
|
+
export { sdkVersion } from './disclosure.js'
|
|
55
|
+
export { currentEvalRunOptions, withEvalRunOptions } from './run-options.js'
|
|
56
|
+
export type { EvalRunOptions } from './run-options.js'
|
|
57
|
+
export { formatRunSummary, summarizeRun } from './run-summary.js'
|
|
58
|
+
export type { RunDisclosure, RunSummary } from './run-summary.js'
|
|
59
|
+
export type { RepeatReliability, SplitSummary } from './report-stats.js'
|
|
60
|
+
export { compareEvalReports, formatEvalComparison, loadEvalReportFile } from './compare.js'
|
|
61
|
+
export type {
|
|
62
|
+
CaseRef,
|
|
63
|
+
DisclosureComparison,
|
|
64
|
+
DisclosureDifference,
|
|
65
|
+
DisclosureDimension,
|
|
66
|
+
DisclosureUnknown,
|
|
67
|
+
EvalComparison,
|
|
68
|
+
Flips,
|
|
69
|
+
LoadedReportFile,
|
|
70
|
+
LoadedSuite,
|
|
71
|
+
SideNumbers,
|
|
72
|
+
} from './compare.js'
|
|
24
73
|
export { describeUncertainty, uncertaintyOf } from './uncertainty.js'
|
|
25
74
|
export type { ScoreUncertainty } from './uncertainty.js'
|
|
26
75
|
export type {
|
|
76
|
+
CaseRepeatSummary,
|
|
27
77
|
CaseResult,
|
|
78
|
+
EvalSplit,
|
|
28
79
|
CaseStatus,
|
|
29
80
|
EvalCase,
|
|
30
81
|
EvalTurn,
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { type CaseIntervals, type CaseTally, bootstrapCases } from './bootstrap.js'
|
|
2
|
+
import type { CaseRepeatSummary, CaseResult, EvalSplit } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The numbers a report carries about a set of runs, whether that set is one
|
|
6
|
+
* suite, one split of it, or every suite in a run.
|
|
7
|
+
*
|
|
8
|
+
* Cases are the unit. A case with several repeats is one case with several
|
|
9
|
+
* runs: the pass rate pools the runs, the intervals resample the cases, and
|
|
10
|
+
* reliability asks of each case whether it held up every time.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface RepeatReliability {
|
|
14
|
+
/** Cases that ran. */
|
|
15
|
+
cases: number
|
|
16
|
+
/** Cases whose every repeat passed. */
|
|
17
|
+
passedEveryRepeat: number
|
|
18
|
+
/** `passedEveryRepeat / cases`; null when no case ran. */
|
|
19
|
+
fraction: number | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SplitSummary {
|
|
23
|
+
cases: number
|
|
24
|
+
runs: number
|
|
25
|
+
passed: number
|
|
26
|
+
failed: number
|
|
27
|
+
inconclusive: number
|
|
28
|
+
passRate: number | null
|
|
29
|
+
mean: number | null
|
|
30
|
+
intervals: CaseIntervals
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The runs of one case, with the key that pairs it with the same case elsewhere. */
|
|
34
|
+
export interface CaseRuns {
|
|
35
|
+
readonly key: string
|
|
36
|
+
readonly name: string
|
|
37
|
+
readonly split?: EvalSplit
|
|
38
|
+
readonly runs: readonly CaseResult[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RunStats {
|
|
42
|
+
cases: number
|
|
43
|
+
runs: number
|
|
44
|
+
passed: number
|
|
45
|
+
failed: number
|
|
46
|
+
inconclusive: number
|
|
47
|
+
passRate: number | null
|
|
48
|
+
mean: number | null
|
|
49
|
+
intervals: CaseIntervals
|
|
50
|
+
caseSummaries: CaseRepeatSummary[]
|
|
51
|
+
reliability: RepeatReliability
|
|
52
|
+
splits: Partial<Record<EvalSplit | 'unassigned', SplitSummary>>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const SPLIT_ORDER = ['development', 'held-out', 'unassigned'] as const
|
|
56
|
+
|
|
57
|
+
/** Group a flat list of runs by case name, in the order the cases first appear. */
|
|
58
|
+
export function groupRuns(
|
|
59
|
+
results: readonly CaseResult[],
|
|
60
|
+
keyOf: (name: string) => string = (name) => name,
|
|
61
|
+
): CaseRuns[] {
|
|
62
|
+
const groups = new Map<string, { name: string; split?: EvalSplit; runs: CaseResult[] }>()
|
|
63
|
+
for (const run of results) {
|
|
64
|
+
const key = keyOf(run.case)
|
|
65
|
+
let group = groups.get(key)
|
|
66
|
+
if (!group) {
|
|
67
|
+
group = { name: run.case, ...(run.split ? { split: run.split } : {}), runs: [] }
|
|
68
|
+
groups.set(key, group)
|
|
69
|
+
}
|
|
70
|
+
group.runs.push(run)
|
|
71
|
+
}
|
|
72
|
+
return [...groups].map(([key, g]) => ({ key, ...g }))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The first name that appears more than once, or undefined. */
|
|
76
|
+
export function duplicateName(names: readonly string[]): string | undefined {
|
|
77
|
+
const seen = new Set<string>()
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
if (seen.has(name)) return name
|
|
80
|
+
seen.add(name)
|
|
81
|
+
}
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function duplicateMessage(suite: string, name: string): string {
|
|
86
|
+
return `suite "${suite}" has the case "${name}" more than once; a case is named by its id, and two cases sharing one would be merged into one in every count and interval. Give each case its own name.`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The first case in a list of runs that appears twice for the same repeat: a
|
|
91
|
+
* second run of a case is a repeat only when it says it is a later one.
|
|
92
|
+
*/
|
|
93
|
+
export function duplicateRun(
|
|
94
|
+
runs: readonly Pick<CaseResult, 'case' | 'repeat'>[],
|
|
95
|
+
): string | undefined {
|
|
96
|
+
const seen = new Set<string>()
|
|
97
|
+
for (const run of runs) {
|
|
98
|
+
const key = `${run.case}\u0000${run.repeat ?? 1}`
|
|
99
|
+
if (seen.has(key)) return run.case
|
|
100
|
+
seen.add(key)
|
|
101
|
+
}
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Status of a run, tolerating a report old enough to carry only `passed`. */
|
|
106
|
+
export function statusOf(run: Pick<CaseResult, 'status' | 'passed'>): CaseResult['status'] {
|
|
107
|
+
if (run.status === 'passed' || run.status === 'failed' || run.status === 'inconclusive') {
|
|
108
|
+
return run.status
|
|
109
|
+
}
|
|
110
|
+
return run.passed === true ? 'passed' : 'failed'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function tallyOf(runs: readonly CaseResult[]): CaseTally | null {
|
|
114
|
+
let scoreSum = 0
|
|
115
|
+
let passes = 0
|
|
116
|
+
let judged = 0
|
|
117
|
+
for (const run of runs) {
|
|
118
|
+
const status = statusOf(run)
|
|
119
|
+
if (status === 'inconclusive') continue
|
|
120
|
+
judged++
|
|
121
|
+
scoreSum += run.mean
|
|
122
|
+
if (status === 'passed') passes++
|
|
123
|
+
}
|
|
124
|
+
return judged === 0 ? null : { scoreSum, passes, judged }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True when the case has at least one judged run and every run passed. */
|
|
128
|
+
export function passedEveryRun(runs: readonly CaseResult[]): boolean {
|
|
129
|
+
return runs.length > 0 && runs.every((run) => statusOf(run) === 'passed')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function summaryOf(runs: readonly CaseResult[]) {
|
|
133
|
+
let passed = 0
|
|
134
|
+
let failed = 0
|
|
135
|
+
let inconclusive = 0
|
|
136
|
+
for (const run of runs) {
|
|
137
|
+
const status = statusOf(run)
|
|
138
|
+
if (status === 'passed') passed++
|
|
139
|
+
else if (status === 'failed') failed++
|
|
140
|
+
else inconclusive++
|
|
141
|
+
}
|
|
142
|
+
return { passed, failed, inconclusive }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function numbersOver(cases: readonly CaseRuns[], seed: number) {
|
|
146
|
+
const tallies: CaseTally[] = []
|
|
147
|
+
let runs = 0
|
|
148
|
+
for (const c of cases) {
|
|
149
|
+
runs += c.runs.length
|
|
150
|
+
const tally = tallyOf(c.runs)
|
|
151
|
+
if (tally) tallies.push(tally)
|
|
152
|
+
}
|
|
153
|
+
const counts = summaryOf(cases.flatMap((c) => c.runs))
|
|
154
|
+
const judged = counts.passed + counts.failed
|
|
155
|
+
const scoreSum = tallies.reduce((sum, t) => sum + t.scoreSum, 0)
|
|
156
|
+
return {
|
|
157
|
+
runs,
|
|
158
|
+
...counts,
|
|
159
|
+
passRate: judged === 0 ? null : counts.passed / judged,
|
|
160
|
+
mean: judged === 0 ? null : scoreSum / judged,
|
|
161
|
+
intervals: bootstrapCases(tallies, seed),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function statsOver(cases: readonly CaseRuns[], seed: number): RunStats {
|
|
166
|
+
const whole = numbersOver(cases, seed)
|
|
167
|
+
const caseSummaries: CaseRepeatSummary[] = cases.map((c) => {
|
|
168
|
+
const counts = summaryOf(c.runs)
|
|
169
|
+
const tally = tallyOf(c.runs)
|
|
170
|
+
return {
|
|
171
|
+
case: c.name,
|
|
172
|
+
...(c.split ? { split: c.split } : {}),
|
|
173
|
+
repeats: c.runs.length,
|
|
174
|
+
...counts,
|
|
175
|
+
mean: tally ? tally.scoreSum / tally.judged : null,
|
|
176
|
+
passedEveryRepeat: passedEveryRun(c.runs),
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
const everyRepeat = caseSummaries.filter((c) => c.passedEveryRepeat).length
|
|
180
|
+
|
|
181
|
+
const splits: RunStats['splits'] = {}
|
|
182
|
+
for (const name of SPLIT_ORDER) {
|
|
183
|
+
const members = cases.filter((c) => (c.split ?? 'unassigned') === name)
|
|
184
|
+
if (members.length === 0) continue
|
|
185
|
+
const part = numbersOver(members, seed)
|
|
186
|
+
splits[name] = { cases: members.length, ...part }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
cases: cases.length,
|
|
191
|
+
...whole,
|
|
192
|
+
caseSummaries,
|
|
193
|
+
reliability: {
|
|
194
|
+
cases: cases.length,
|
|
195
|
+
passedEveryRepeat: everyRepeat,
|
|
196
|
+
fraction: cases.length === 0 ? null : everyRepeat / cases.length,
|
|
197
|
+
},
|
|
198
|
+
splits,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const percent = (value: number): string => `${(value * 100).toFixed(1)}%`
|
|
203
|
+
const score = (value: number): string => value.toFixed(3)
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The intervals, as lines a person reads next to the numbers. A number whose
|
|
207
|
+
* interval is not computed is printed with the reason, never alone.
|
|
208
|
+
*/
|
|
209
|
+
export function describeIntervals(args: {
|
|
210
|
+
mean: number | null
|
|
211
|
+
passRate: number | null
|
|
212
|
+
intervals: CaseIntervals
|
|
213
|
+
}): string[] {
|
|
214
|
+
const { intervals } = args
|
|
215
|
+
const line = (
|
|
216
|
+
label: string,
|
|
217
|
+
estimate: number | null,
|
|
218
|
+
interval: CaseIntervals['mean'],
|
|
219
|
+
show: (n: number) => string,
|
|
220
|
+
): string => {
|
|
221
|
+
if (estimate === null) return `${label}: none, no run was judged`
|
|
222
|
+
if (!interval.computed) return `${label} ${show(estimate)}, no interval: ${interval.reason}`
|
|
223
|
+
return `${label} ${show(estimate)}, 95% interval ${show(interval.low)} to ${show(interval.high)}`
|
|
224
|
+
}
|
|
225
|
+
const lines = [
|
|
226
|
+
line('mean score', args.mean, intervals.mean, score),
|
|
227
|
+
line('pass rate', args.passRate, intervals.passRate, percent),
|
|
228
|
+
]
|
|
229
|
+
if (intervals.mean.computed || intervals.passRate.computed) {
|
|
230
|
+
lines.push(
|
|
231
|
+
`intervals: ${intervals.method.kind} (${intervals.method.cases} cases, ${intervals.method.resamples} resamples), seed ${intervals.method.seed}`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
return lines
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The line that says which splits a suite has, and says so plainly when none is held out. */
|
|
238
|
+
export function describeSplits(splits: RunStats['splits']): string[] {
|
|
239
|
+
const lines: string[] = []
|
|
240
|
+
// A suite that declares no split has one bucket and nothing to break down.
|
|
241
|
+
const declared = SPLIT_ORDER.some((name) => name !== 'unassigned' && splits[name])
|
|
242
|
+
for (const name of SPLIT_ORDER) {
|
|
243
|
+
const part = splits[name]
|
|
244
|
+
if (!part || !declared) continue
|
|
245
|
+
const rate = part.passRate === null ? 'no judged run' : `pass rate ${percent(part.passRate)}`
|
|
246
|
+
const interval = part.intervals.passRate.computed
|
|
247
|
+
? `, 95% interval ${percent(part.intervals.passRate.low)} to ${percent(part.intervals.passRate.high)}`
|
|
248
|
+
: ''
|
|
249
|
+
lines.push(`${name}: ${part.cases} case${part.cases === 1 ? '' : 's'}, ${rate}${interval}`)
|
|
250
|
+
}
|
|
251
|
+
if (!splits['held-out']) {
|
|
252
|
+
lines.push(
|
|
253
|
+
'no held-out cases: every number here is measured on cases the suite was developed against',
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
return lines
|
|
257
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Settings an operator gives a whole eval run, handed to every
|
|
5
|
+
* `runExperiment` that executes inside it.
|
|
6
|
+
*
|
|
7
|
+
* A suite file calls `runExperiment` itself and returns the report, so
|
|
8
|
+
* `namzu eval --repeat 5` has no argument to pass the five through. Rather
|
|
9
|
+
* than ask every suite to forward it, the runner opens a scope with
|
|
10
|
+
* {@link withEvalRunOptions} and `runExperiment` reads the scope. A suite
|
|
11
|
+
* that was written before the option existed repeats like any other.
|
|
12
|
+
*
|
|
13
|
+
* An option given here wins over the same option in the suite's own config:
|
|
14
|
+
* the person running the command asked for it.
|
|
15
|
+
*/
|
|
16
|
+
export interface EvalRunOptions {
|
|
17
|
+
/** Times every case runs. */
|
|
18
|
+
readonly repeats?: number
|
|
19
|
+
/** Seed for the bootstrap. */
|
|
20
|
+
readonly seed?: number
|
|
21
|
+
/** Version of the program that is running the suites, for the disclosure. */
|
|
22
|
+
readonly harnessVersion?: string
|
|
23
|
+
/** Source revision the suites came from, when known. */
|
|
24
|
+
readonly commit?: string
|
|
25
|
+
/** Version of the suite being run, when known. */
|
|
26
|
+
readonly suiteVersion?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STORE_KEY = Symbol.for('namzu.eval.runOptions')
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One store per process, found through a global symbol, so a suite that
|
|
33
|
+
* resolves its own copy of the SDK still sees the scope the runner opened.
|
|
34
|
+
*/
|
|
35
|
+
function store(): AsyncLocalStorage<EvalRunOptions> {
|
|
36
|
+
const holder = globalThis as { [STORE_KEY]?: AsyncLocalStorage<EvalRunOptions> }
|
|
37
|
+
holder[STORE_KEY] ??= new AsyncLocalStorage<EvalRunOptions>()
|
|
38
|
+
return holder[STORE_KEY]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Run `work` with `options` in force for every experiment it starts. */
|
|
42
|
+
export function withEvalRunOptions<T>(options: EvalRunOptions, work: () => T): T {
|
|
43
|
+
return store().run(options, work)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The options in force here, or an empty set outside any scope. */
|
|
47
|
+
export function currentEvalRunOptions(): EvalRunOptions {
|
|
48
|
+
return store().getStore() ?? {}
|
|
49
|
+
}
|