@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/connector/index.d.ts +1 -1
  3. package/dist/connector/index.d.ts.map +1 -1
  4. package/dist/connector/index.js +1 -1
  5. package/dist/connector/index.js.map +1 -1
  6. package/dist/connector/mcp/client.d.ts +96 -0
  7. package/dist/connector/mcp/client.d.ts.map +1 -1
  8. package/dist/connector/mcp/client.js +488 -13
  9. package/dist/connector/mcp/client.js.map +1 -1
  10. package/dist/connector/mcp/index.d.ts +1 -0
  11. package/dist/connector/mcp/index.d.ts.map +1 -1
  12. package/dist/connector/mcp/index.js +1 -0
  13. package/dist/connector/mcp/index.js.map +1 -1
  14. package/dist/connector/mcp/tasks.d.ts +40 -0
  15. package/dist/connector/mcp/tasks.d.ts.map +1 -0
  16. package/dist/connector/mcp/tasks.js +55 -0
  17. package/dist/connector/mcp/tasks.js.map +1 -0
  18. package/dist/constants/mcp/index.d.ts +1 -1
  19. package/dist/constants/mcp/index.d.ts.map +1 -1
  20. package/dist/constants/mcp/index.js +5 -0
  21. package/dist/constants/mcp/index.js.map +1 -1
  22. package/dist/eval/bootstrap.d.ts +122 -0
  23. package/dist/eval/bootstrap.d.ts.map +1 -0
  24. package/dist/eval/bootstrap.js +227 -0
  25. package/dist/eval/bootstrap.js.map +1 -0
  26. package/dist/eval/compare.d.ts +109 -0
  27. package/dist/eval/compare.d.ts.map +1 -0
  28. package/dist/eval/compare.js +324 -0
  29. package/dist/eval/compare.js.map +1 -0
  30. package/dist/eval/disclosure.d.ts +70 -0
  31. package/dist/eval/disclosure.d.ts.map +1 -0
  32. package/dist/eval/disclosure.js +64 -0
  33. package/dist/eval/disclosure.js.map +1 -0
  34. package/dist/eval/experiment.d.ts +23 -0
  35. package/dist/eval/experiment.d.ts.map +1 -1
  36. package/dist/eval/experiment.js +87 -15
  37. package/dist/eval/experiment.js.map +1 -1
  38. package/dist/eval/from-turn.d.ts.map +1 -1
  39. package/dist/eval/from-turn.js +5 -0
  40. package/dist/eval/from-turn.js.map +1 -1
  41. package/dist/eval/index.d.ts +13 -2
  42. package/dist/eval/index.d.ts.map +1 -1
  43. package/dist/eval/index.js +6 -1
  44. package/dist/eval/index.js.map +1 -1
  45. package/dist/eval/report-stats.d.ts +77 -0
  46. package/dist/eval/report-stats.d.ts.map +1 -0
  47. package/dist/eval/report-stats.js +184 -0
  48. package/dist/eval/report-stats.js.map +1 -0
  49. package/dist/eval/run-options.d.ts +30 -0
  50. package/dist/eval/run-options.d.ts.map +1 -0
  51. package/dist/eval/run-options.js +20 -0
  52. package/dist/eval/run-options.js.map +1 -0
  53. package/dist/eval/run-summary.d.ts +36 -0
  54. package/dist/eval/run-summary.d.ts.map +1 -0
  55. package/dist/eval/run-summary.js +57 -0
  56. package/dist/eval/run-summary.js.map +1 -0
  57. package/dist/eval/scorers.d.ts +32 -0
  58. package/dist/eval/scorers.d.ts.map +1 -1
  59. package/dist/eval/scorers.js +77 -0
  60. package/dist/eval/scorers.js.map +1 -1
  61. package/dist/eval/types.d.ts +74 -0
  62. package/dist/eval/types.d.ts.map +1 -1
  63. package/dist/manager/session/turn-recorder.d.ts +3 -0
  64. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  65. package/dist/manager/session/turn-recorder.js +20 -5
  66. package/dist/manager/session/turn-recorder.js.map +1 -1
  67. package/dist/public-runtime.d.ts +5 -2
  68. package/dist/public-runtime.d.ts.map +1 -1
  69. package/dist/public-runtime.js +7 -2
  70. package/dist/public-runtime.js.map +1 -1
  71. package/dist/public-types.d.ts +2 -0
  72. package/dist/public-types.d.ts.map +1 -1
  73. package/dist/registry/command/kernel-commands.d.ts +3 -0
  74. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  75. package/dist/registry/command/kernel-commands.js +76 -5
  76. package/dist/registry/command/kernel-commands.js.map +1 -1
  77. package/dist/runtime/query/abandon-turn.d.ts.map +1 -1
  78. package/dist/runtime/query/abandon-turn.js +2 -2
  79. package/dist/runtime/query/abandon-turn.js.map +1 -1
  80. package/dist/runtime/query/index.d.ts.map +1 -1
  81. package/dist/runtime/query/index.js +15 -0
  82. package/dist/runtime/query/index.js.map +1 -1
  83. package/dist/runtime/query/iteration/index.d.ts +18 -1
  84. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  85. package/dist/runtime/query/iteration/index.js +80 -1
  86. package/dist/runtime/query/iteration/index.js.map +1 -1
  87. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  88. package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
  89. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  90. package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
  91. package/dist/runtime/query/prelude-lease.js +2 -2
  92. package/dist/runtime/query/prelude-lease.js.map +1 -1
  93. package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
  94. package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
  95. package/dist/runtime/query/turn/answer-claims.js +348 -0
  96. package/dist/runtime/query/turn/answer-claims.js.map +1 -0
  97. package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
  98. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
  99. package/dist/runtime/query/turn/outcome-ledger.js +13 -0
  100. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
  101. package/dist/store/goal/index.d.ts +3 -1
  102. package/dist/store/goal/index.d.ts.map +1 -1
  103. package/dist/store/goal/index.js +6 -0
  104. package/dist/store/goal/index.js.map +1 -1
  105. package/dist/store/session-log/index.d.ts +1 -0
  106. package/dist/store/session-log/index.d.ts.map +1 -1
  107. package/dist/store/session-log/index.js +1 -0
  108. package/dist/store/session-log/index.js.map +1 -1
  109. package/dist/store/session-log/turn-holder.d.ts +26 -0
  110. package/dist/store/session-log/turn-holder.d.ts.map +1 -0
  111. package/dist/store/session-log/turn-holder.js +79 -0
  112. package/dist/store/session-log/turn-holder.js.map +1 -0
  113. package/dist/telemetry/runtime-accessors.d.ts +14 -0
  114. package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
  115. package/dist/telemetry/runtime-accessors.js +32 -0
  116. package/dist/telemetry/runtime-accessors.js.map +1 -1
  117. package/dist/tools/goal/confirmation.d.ts +154 -0
  118. package/dist/tools/goal/confirmation.d.ts.map +1 -0
  119. package/dist/tools/goal/confirmation.js +290 -0
  120. package/dist/tools/goal/confirmation.js.map +1 -0
  121. package/dist/tools/goal/index.d.ts +4 -1
  122. package/dist/tools/goal/index.d.ts.map +1 -1
  123. package/dist/tools/goal/index.js +27 -2
  124. package/dist/tools/goal/index.js.map +1 -1
  125. package/dist/tools/hand-back/index.d.ts +48 -0
  126. package/dist/tools/hand-back/index.d.ts.map +1 -0
  127. package/dist/tools/hand-back/index.js +129 -0
  128. package/dist/tools/hand-back/index.js.map +1 -0
  129. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
  130. package/dist/tools/schedules/schedule-tool.js +15 -1
  131. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  132. package/dist/tools/schedules/types.d.ts +10 -0
  133. package/dist/tools/schedules/types.d.ts.map +1 -1
  134. package/dist/types/connector/mcp.d.ts +18 -0
  135. package/dist/types/connector/mcp.d.ts.map +1 -1
  136. package/dist/types/goal/index.d.ts +12 -0
  137. package/dist/types/goal/index.d.ts.map +1 -1
  138. package/dist/types/session/config.d.ts +8 -0
  139. package/dist/types/session/config.d.ts.map +1 -1
  140. package/dist/types/session/hand-back.d.ts +29 -0
  141. package/dist/types/session/hand-back.d.ts.map +1 -0
  142. package/dist/types/session/hand-back.js +11 -0
  143. package/dist/types/session/hand-back.js.map +1 -0
  144. package/dist/types/session/outcome.d.ts +37 -0
  145. package/dist/types/session/outcome.d.ts.map +1 -1
  146. package/dist/types/session/outcome.js.map +1 -1
  147. package/dist/types/session/turn.d.ts +19 -1
  148. package/dist/types/session/turn.d.ts.map +1 -1
  149. package/dist/types/session/turn.js.map +1 -1
  150. package/package.json +1 -1
  151. package/src/connector/index.ts +1 -0
  152. package/src/connector/mcp/client.ts +531 -14
  153. package/src/connector/mcp/index.ts +1 -0
  154. package/src/connector/mcp/tasks.ts +81 -0
  155. package/src/constants/mcp/index.ts +6 -1
  156. package/src/eval/bootstrap.ts +314 -0
  157. package/src/eval/compare.ts +473 -0
  158. package/src/eval/disclosure.ts +136 -0
  159. package/src/eval/experiment.ts +127 -15
  160. package/src/eval/from-turn.ts +5 -0
  161. package/src/eval/index.ts +51 -0
  162. package/src/eval/report-stats.ts +257 -0
  163. package/src/eval/run-options.ts +49 -0
  164. package/src/eval/run-summary.ts +104 -0
  165. package/src/eval/scorers.ts +83 -0
  166. package/src/eval/types.ts +76 -0
  167. package/src/manager/session/turn-recorder.ts +25 -4
  168. package/src/public-runtime.ts +53 -0
  169. package/src/public-types.ts +9 -0
  170. package/src/registry/command/kernel-commands.ts +94 -6
  171. package/src/runtime/query/abandon-turn.ts +2 -1
  172. package/src/runtime/query/index.ts +18 -0
  173. package/src/runtime/query/iteration/index.ts +85 -1
  174. package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
  175. package/src/runtime/query/prelude-lease.ts +7 -2
  176. package/src/runtime/query/turn/answer-claims.ts +413 -0
  177. package/src/runtime/query/turn/outcome-ledger.ts +12 -0
  178. package/src/store/goal/index.ts +11 -0
  179. package/src/store/session-log/index.ts +8 -0
  180. package/src/store/session-log/turn-holder.ts +95 -0
  181. package/src/telemetry/runtime-accessors.ts +29 -0
  182. package/src/tools/goal/confirmation.ts +375 -0
  183. package/src/tools/goal/index.ts +37 -1
  184. package/src/tools/hand-back/index.ts +150 -0
  185. package/src/tools/schedules/schedule-tool.ts +19 -1
  186. package/src/tools/schedules/types.ts +10 -0
  187. package/src/types/connector/mcp.ts +18 -0
  188. package/src/types/goal/index.ts +13 -0
  189. package/src/types/session/config.ts +8 -0
  190. package/src/types/session/hand-back.ts +31 -0
  191. package/src/types/session/outcome.ts +42 -0
  192. package/src/types/session/turn.ts +19 -1
@@ -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
- const results: CaseResult[] = new Array(config.cases.length)
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
- const evalCase = config.cases[index]
75
- if (!evalCase) return
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, config.cases.length) }, worker))
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(scored.map((r) => r.mean)),
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
- `${report.name}: ${report.passed}/${report.cases.length} passed (mean ${report.mean.toFixed(2)}) in ${report.durationMs}ms`,
356
- // On its own line and always printed, including when the interval is
357
- // undefined. A mean printed alone is the thing that has been
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(` ✗ ${failure.case} (${failure.mean.toFixed(2)})`)
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
@@ -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
+ }