@namzu/sdk 62.12.0 → 62.14.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 +31 -0
- package/dist/agents/file-definitions.d.ts +7 -0
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +20 -0
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/bridge/acp/server.d.ts +8 -1
- package/dist/bridge/acp/server.d.ts.map +1 -1
- package/dist/bridge/acp/server.js +12 -0
- package/dist/bridge/acp/server.js.map +1 -1
- 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/data-epoch.d.ts +18 -0
- package/dist/constants/data-epoch.d.ts.map +1 -0
- package/dist/constants/data-epoch.js +18 -0
- package/dist/constants/data-epoch.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- 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/constants/telemetry/index.d.ts +12 -0
- package/dist/constants/telemetry/index.d.ts.map +1 -1
- package/dist/constants/telemetry/index.js +12 -0
- package/dist/constants/telemetry/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 +18 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/loader.d.ts +11 -0
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +15 -2
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +10 -6
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +13 -5
- 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/cancelled-before-start.d.ts.map +1 -1
- package/dist/runtime/query/cancelled-before-start.js +2 -0
- package/dist/runtime/query/cancelled-before-start.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +17 -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/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +5 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +2 -0
- package/dist/runtime/query/result.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/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +15 -1
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +17 -3
- package/dist/skills/loader.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/telemetry/genai.d.ts +98 -0
- package/dist/telemetry/genai.d.ts.map +1 -0
- package/dist/telemetry/genai.js +189 -0
- package/dist/telemetry/genai.js.map +1 -0
- package/dist/telemetry/runtime-accessors.d.ts +19 -0
- package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
- package/dist/telemetry/runtime-accessors.js +43 -1
- 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/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +12 -0
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/acp/index.d.ts +18 -0
- package/dist/types/acp/index.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/agents/file-definitions.ts +21 -0
- package/src/agents/index.ts +1 -0
- package/src/bridge/acp/server.ts +20 -0
- 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/data-epoch.ts +17 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/mcp/index.ts +6 -1
- package/src/constants/telemetry/index.ts +13 -0
- 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 +23 -2
- package/src/plugin/index.ts +7 -1
- package/src/plugin/loader.ts +16 -2
- package/src/public-runtime.ts +70 -0
- package/src/public-types.ts +9 -0
- package/src/registry/command/kernel-commands.ts +94 -6
- package/src/runtime/query/cancelled-before-start.ts +2 -0
- package/src/runtime/query/index.ts +20 -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/iteration/stream-turn.ts +10 -0
- package/src/runtime/query/result.ts +2 -0
- package/src/runtime/query/turn/answer-claims.ts +413 -0
- package/src/runtime/query/turn/outcome-ledger.ts +12 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/loader.ts +21 -3
- package/src/store/goal/index.ts +11 -0
- package/src/telemetry/genai.ts +202 -0
- package/src/telemetry/runtime-accessors.ts +40 -1
- 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/toolsets/manager.ts +22 -0
- package/src/types/acp/index.ts +20 -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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CaseTally,
|
|
3
|
+
type DifferenceIntervals,
|
|
4
|
+
type Interval,
|
|
5
|
+
type PairedTally,
|
|
6
|
+
bootstrapPairedDifference,
|
|
7
|
+
exactSignTestP,
|
|
8
|
+
pooledMeanAndRate,
|
|
9
|
+
validateSeed,
|
|
10
|
+
} from './bootstrap.js'
|
|
11
|
+
import { UNKNOWN } from './disclosure.js'
|
|
12
|
+
import { type CaseRuns, duplicateRun, groupRuns, passedEveryRun, tallyOf } from './report-stats.js'
|
|
13
|
+
import type { CaseResult } from './types.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Two eval reports, side by side, paired by suite and case id.
|
|
17
|
+
*
|
|
18
|
+
* Two pass rates a few points apart read as a difference, and at the size of
|
|
19
|
+
* a hand-built suite they are usually not one. The comparison pairs the cases
|
|
20
|
+
* both sides ran, takes the difference over those, and puts an interval on it
|
|
21
|
+
* by resampling the pairs. It says "no difference can be claimed at this
|
|
22
|
+
* size" whenever that interval includes zero, and it says what else differs
|
|
23
|
+
* between the two runs, because a difference that comes with a new model and
|
|
24
|
+
* a new harness version and a new effort cannot be credited to any of them.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** One suite of a loaded report file. */
|
|
28
|
+
export interface LoadedSuite {
|
|
29
|
+
readonly id: string
|
|
30
|
+
readonly cases: readonly CaseRuns[]
|
|
31
|
+
readonly repeats: number
|
|
32
|
+
/** Whatever the file said about how the suite ran; absent in an older file. */
|
|
33
|
+
readonly facts: Readonly<Record<string, unknown>> | undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadedReportFile {
|
|
37
|
+
readonly label: string
|
|
38
|
+
readonly suites: readonly LoadedSuite[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the JSON `namzu eval --out` writes. Tolerant of a file from an older
|
|
47
|
+
* version: only `suites[].suite` and each case's name, mean and status are
|
|
48
|
+
* required; repeats, splits and disclosure are read when they are there.
|
|
49
|
+
* Throws an Error whose message names what is wrong, for a person to read.
|
|
50
|
+
*/
|
|
51
|
+
export function loadEvalReportFile(json: unknown, label: string): LoadedReportFile {
|
|
52
|
+
if (!isRecord(json) || !Array.isArray(json.suites)) {
|
|
53
|
+
throw new Error(`${label} is not an eval report: it has no "suites" list.`)
|
|
54
|
+
}
|
|
55
|
+
const suites: LoadedSuite[] = []
|
|
56
|
+
for (const [index, entry] of json.suites.entries()) {
|
|
57
|
+
if (!isRecord(entry) || typeof entry.suite !== 'string' || !isRecord(entry.report)) {
|
|
58
|
+
throw new Error(`${label}: suites[${index}] needs a "suite" id and a "report".`)
|
|
59
|
+
}
|
|
60
|
+
const report = entry.report
|
|
61
|
+
if (!Array.isArray(report.cases)) {
|
|
62
|
+
throw new Error(`${label}: suite "${entry.suite}" has no "cases" list.`)
|
|
63
|
+
}
|
|
64
|
+
const runs: CaseResult[] = []
|
|
65
|
+
for (const [caseIndex, run] of report.cases.entries()) {
|
|
66
|
+
if (!isRecord(run) || typeof run.case !== 'string' || !Number.isFinite(run.mean)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${label}: suite "${entry.suite}", case ${caseIndex} needs a name in "case" and a finite number in "mean".`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
runs.push(run as unknown as CaseResult)
|
|
72
|
+
}
|
|
73
|
+
const twice = duplicateRun(runs)
|
|
74
|
+
if (twice !== undefined) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`${label}: suite "${entry.suite}" has the case "${twice}" more than once, so its runs cannot be told from one case's repeats. A case is named by its id; the report needs each case once per repeat.`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
suites.push({
|
|
80
|
+
id: entry.suite,
|
|
81
|
+
cases: groupRuns(runs),
|
|
82
|
+
repeats: typeof report.repeats === 'number' ? report.repeats : 1,
|
|
83
|
+
facts: isRecord(report.disclosure) ? report.disclosure : undefined,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
return { label, suites }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── what differs in how two reports were produced ───────────────────────
|
|
90
|
+
|
|
91
|
+
export type DisclosureDimension = 'harness' | 'suites' | 'model' | 'route' | 'effort' | 'repeats'
|
|
92
|
+
|
|
93
|
+
const DIMENSION_ORDER: readonly DisclosureDimension[] = [
|
|
94
|
+
'harness',
|
|
95
|
+
'suites',
|
|
96
|
+
'model',
|
|
97
|
+
'route',
|
|
98
|
+
'effort',
|
|
99
|
+
'repeats',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
interface Fact {
|
|
103
|
+
readonly dimension: DisclosureDimension
|
|
104
|
+
readonly key: string
|
|
105
|
+
/** Null is unknown. */
|
|
106
|
+
readonly value: string | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function known(value: unknown): string | null {
|
|
110
|
+
return typeof value === 'string' && value !== UNKNOWN && value.length > 0 ? value : null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function canonical(value: unknown): string | null {
|
|
114
|
+
if (value === undefined || value === null || value === UNKNOWN) return null
|
|
115
|
+
if (typeof value === 'string') return value
|
|
116
|
+
return JSON.stringify(value, (_key, v) =>
|
|
117
|
+
isRecord(v)
|
|
118
|
+
? Object.fromEntries(Object.entries(v).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)))
|
|
119
|
+
: v,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rolesFact(value: unknown): string | null {
|
|
124
|
+
if (!isRecord(value)) return null
|
|
125
|
+
const parts: string[] = []
|
|
126
|
+
for (const role of Object.keys(value).sort()) {
|
|
127
|
+
const who = value[role]
|
|
128
|
+
const provider = isRecord(who) ? known(who.provider) : null
|
|
129
|
+
const model = isRecord(who) ? known(who.model) : null
|
|
130
|
+
if (provider === null || model === null) return null
|
|
131
|
+
parts.push(`${role}=${provider}/${model}`)
|
|
132
|
+
}
|
|
133
|
+
return parts.length === 0 ? 'no model' : parts.join('; ')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function firstKnown(suites: readonly LoadedSuite[], field: string): string | null {
|
|
137
|
+
for (const suite of suites) {
|
|
138
|
+
const value = known(suite.facts?.[field])
|
|
139
|
+
if (value !== null) return value
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function factsOf(file: LoadedReportFile, suiteIds: readonly string[]): Fact[] {
|
|
145
|
+
const facts: Fact[] = [
|
|
146
|
+
{
|
|
147
|
+
dimension: 'harness',
|
|
148
|
+
key: 'harness version',
|
|
149
|
+
value: firstKnown(file.suites, 'harnessVersion'),
|
|
150
|
+
},
|
|
151
|
+
{ dimension: 'harness', key: 'sdk version', value: firstKnown(file.suites, 'sdkVersion') },
|
|
152
|
+
{ dimension: 'harness', key: 'commit', value: firstKnown(file.suites, 'commit') },
|
|
153
|
+
]
|
|
154
|
+
for (const id of suiteIds) {
|
|
155
|
+
const suite = file.suites.find((s) => s.id === id)
|
|
156
|
+
if (!suite) continue
|
|
157
|
+
const f = suite.facts
|
|
158
|
+
facts.push(
|
|
159
|
+
{ dimension: 'suites', key: `${id} version`, value: known(f?.suiteVersion) },
|
|
160
|
+
{ dimension: 'model', key: `${id} mode`, value: known(f?.mode) },
|
|
161
|
+
{ dimension: 'model', key: `${id} models`, value: rolesFact(f?.roles) },
|
|
162
|
+
{ dimension: 'route', key: `${id} routing`, value: canonical(f?.routing) },
|
|
163
|
+
{ dimension: 'effort', key: `${id} reasoning effort`, value: known(f?.reasoningEffort) },
|
|
164
|
+
// A file from before repeats existed ran every case once.
|
|
165
|
+
{ dimension: 'repeats', key: `${id} repeats`, value: String(suite.repeats) },
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return facts
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface DisclosureDifference {
|
|
172
|
+
readonly dimension: DisclosureDimension
|
|
173
|
+
readonly key: string
|
|
174
|
+
readonly a: string
|
|
175
|
+
readonly b: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface DisclosureUnknown {
|
|
179
|
+
readonly dimension: DisclosureDimension
|
|
180
|
+
readonly key: string
|
|
181
|
+
/** Which side does not say; the other side's value is given. */
|
|
182
|
+
readonly missingIn: 'a' | 'b'
|
|
183
|
+
readonly value: string
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface DisclosureComparison {
|
|
187
|
+
readonly differences: readonly DisclosureDifference[]
|
|
188
|
+
/** Facts one side states and the other does not. */
|
|
189
|
+
readonly unknown: readonly DisclosureUnknown[]
|
|
190
|
+
/** Dimensions in which at least one stated fact differs, in a fixed order. */
|
|
191
|
+
readonly differingDimensions: readonly DisclosureDimension[]
|
|
192
|
+
/** Set when more than one dimension differs. */
|
|
193
|
+
readonly warning?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function compareDisclosure(
|
|
197
|
+
a: LoadedReportFile,
|
|
198
|
+
b: LoadedReportFile,
|
|
199
|
+
sharedSuites: readonly string[],
|
|
200
|
+
): DisclosureComparison {
|
|
201
|
+
const left = new Map(factsOf(a, sharedSuites).map((f) => [f.key, f]))
|
|
202
|
+
const right = new Map(factsOf(b, sharedSuites).map((f) => [f.key, f]))
|
|
203
|
+
const differences: DisclosureDifference[] = []
|
|
204
|
+
const unknown: DisclosureUnknown[] = []
|
|
205
|
+
for (const [key, fa] of left) {
|
|
206
|
+
const fb = right.get(key) as Fact
|
|
207
|
+
if (fa.value !== null && fb.value !== null) {
|
|
208
|
+
if (fa.value !== fb.value) {
|
|
209
|
+
differences.push({ dimension: fa.dimension, key, a: fa.value, b: fb.value })
|
|
210
|
+
}
|
|
211
|
+
} else if (fa.value !== null || fb.value !== null) {
|
|
212
|
+
unknown.push({
|
|
213
|
+
dimension: fa.dimension,
|
|
214
|
+
key,
|
|
215
|
+
missingIn: fa.value === null ? 'a' : 'b',
|
|
216
|
+
value: (fa.value ?? fb.value) as string,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const differingDimensions = DIMENSION_ORDER.filter((d) =>
|
|
221
|
+
differences.some((x) => x.dimension === d),
|
|
222
|
+
)
|
|
223
|
+
return {
|
|
224
|
+
differences,
|
|
225
|
+
unknown,
|
|
226
|
+
differingDimensions,
|
|
227
|
+
...(differingDimensions.length > 1
|
|
228
|
+
? {
|
|
229
|
+
warning: `More than one thing differs between these two runs (${differingDimensions.join(', ')}), so a difference in the scores cannot be attributed to any one of them.`,
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── the comparison ──────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
export interface CaseRef {
|
|
238
|
+
readonly suite: string
|
|
239
|
+
readonly case: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface SideNumbers {
|
|
243
|
+
/** Over the paired cases only. */
|
|
244
|
+
readonly mean: number | null
|
|
245
|
+
readonly passRate: number | null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface Flips {
|
|
249
|
+
/** Cases that passed in every run on side B and not on side A. */
|
|
250
|
+
readonly onlyBPasses: number
|
|
251
|
+
/** The reverse: passed in every run on side A and not on side B. */
|
|
252
|
+
readonly onlyAPasses: number
|
|
253
|
+
readonly bothPass: number
|
|
254
|
+
readonly bothFail: number
|
|
255
|
+
/** Exact two-sided sign test on the two flip counts; 1 with no flips. */
|
|
256
|
+
readonly pValue: number
|
|
257
|
+
readonly test: 'exact two-sided sign test on the cases that flipped'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface EvalComparison {
|
|
261
|
+
readonly seed: number
|
|
262
|
+
readonly a: { readonly label: string; readonly cases: number }
|
|
263
|
+
readonly b: { readonly label: string; readonly cases: number }
|
|
264
|
+
readonly onlyInA: readonly CaseRef[]
|
|
265
|
+
readonly onlyInB: readonly CaseRef[]
|
|
266
|
+
/** Present on both sides but with no judged run on one of them. */
|
|
267
|
+
readonly notJudged: readonly CaseRef[]
|
|
268
|
+
readonly paired: number
|
|
269
|
+
readonly sideA: SideNumbers
|
|
270
|
+
readonly sideB: SideNumbers
|
|
271
|
+
/** B minus A. */
|
|
272
|
+
readonly difference: DifferenceIntervals
|
|
273
|
+
readonly flips: Flips
|
|
274
|
+
readonly verdict: {
|
|
275
|
+
readonly claim: 'better' | 'worse' | 'none'
|
|
276
|
+
/** The one sentence. */
|
|
277
|
+
readonly sentence: string
|
|
278
|
+
/** Why, in plain words. */
|
|
279
|
+
readonly detail: string
|
|
280
|
+
}
|
|
281
|
+
readonly disclosure: DisclosureComparison
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sorted(refs: CaseRef[]): CaseRef[] {
|
|
285
|
+
return refs.sort((x, y) =>
|
|
286
|
+
x.suite === y.suite ? (x.case < y.case ? -1 : 1) : x.suite < y.suite ? -1 : 1,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function signed(value: number, digits = 3): string {
|
|
291
|
+
const text = value.toFixed(digits)
|
|
292
|
+
return value > 0 ? `+${text}` : text
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function compareEvalReports(
|
|
296
|
+
a: LoadedReportFile,
|
|
297
|
+
b: LoadedReportFile,
|
|
298
|
+
options: { seed?: number } = {},
|
|
299
|
+
): EvalComparison {
|
|
300
|
+
const seed = validateSeed(options.seed)
|
|
301
|
+
const index = (file: LoadedReportFile) => {
|
|
302
|
+
const map = new Map<string, { ref: CaseRef; runs: CaseRuns }>()
|
|
303
|
+
for (const suite of file.suites) {
|
|
304
|
+
for (const c of suite.cases) {
|
|
305
|
+
map.set(`${suite.id}\u0000${c.name}`, { ref: { suite: suite.id, case: c.name }, runs: c })
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return map
|
|
309
|
+
}
|
|
310
|
+
const left = index(a)
|
|
311
|
+
const right = index(b)
|
|
312
|
+
|
|
313
|
+
const onlyInA: CaseRef[] = []
|
|
314
|
+
const onlyInB: CaseRef[] = []
|
|
315
|
+
const notJudged: CaseRef[] = []
|
|
316
|
+
const pairs: PairedTally[] = []
|
|
317
|
+
let onlyBPasses = 0
|
|
318
|
+
let onlyAPasses = 0
|
|
319
|
+
let bothPass = 0
|
|
320
|
+
let bothFail = 0
|
|
321
|
+
|
|
322
|
+
for (const [key, l] of left) {
|
|
323
|
+
const r = right.get(key)
|
|
324
|
+
if (!r) {
|
|
325
|
+
onlyInA.push(l.ref)
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
const ta = tallyOf(l.runs.runs)
|
|
329
|
+
const tb = tallyOf(r.runs.runs)
|
|
330
|
+
if (!ta || !tb) {
|
|
331
|
+
notJudged.push(l.ref)
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
pairs.push({ a: ta, b: tb })
|
|
335
|
+
const passA = passedEveryRun(l.runs.runs)
|
|
336
|
+
const passB = passedEveryRun(r.runs.runs)
|
|
337
|
+
if (passA && passB) bothPass++
|
|
338
|
+
else if (!passA && !passB) bothFail++
|
|
339
|
+
else if (passB) onlyBPasses++
|
|
340
|
+
else onlyAPasses++
|
|
341
|
+
}
|
|
342
|
+
for (const [key, r] of right) if (!left.has(key)) onlyInB.push(r.ref)
|
|
343
|
+
|
|
344
|
+
const aSide: readonly CaseTally[] = pairs.map((p) => p.a)
|
|
345
|
+
const bSide: readonly CaseTally[] = pairs.map((p) => p.b)
|
|
346
|
+
const numbers = (tallies: readonly CaseTally[]): SideNumbers => {
|
|
347
|
+
if (tallies.length === 0) return { mean: null, passRate: null }
|
|
348
|
+
const [mean, passRate] = pooledMeanAndRate(tallies)
|
|
349
|
+
return { mean, passRate }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const difference = bootstrapPairedDifference(pairs, seed)
|
|
353
|
+
const flips: Flips = {
|
|
354
|
+
onlyBPasses,
|
|
355
|
+
onlyAPasses,
|
|
356
|
+
bothPass,
|
|
357
|
+
bothFail,
|
|
358
|
+
pValue: exactSignTestP(onlyBPasses, onlyAPasses),
|
|
359
|
+
test: 'exact two-sided sign test on the cases that flipped',
|
|
360
|
+
}
|
|
361
|
+
const verdict = decide(difference.mean, pairs.length, flips)
|
|
362
|
+
const sharedSuites = a.suites.map((s) => s.id).filter((id) => b.suites.some((s) => s.id === id))
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
seed,
|
|
366
|
+
a: { label: a.label, cases: left.size },
|
|
367
|
+
b: { label: b.label, cases: right.size },
|
|
368
|
+
onlyInA: sorted(onlyInA),
|
|
369
|
+
onlyInB: sorted(onlyInB),
|
|
370
|
+
notJudged: sorted(notJudged),
|
|
371
|
+
paired: pairs.length,
|
|
372
|
+
sideA: numbers(aSide),
|
|
373
|
+
sideB: numbers(bSide),
|
|
374
|
+
difference,
|
|
375
|
+
flips,
|
|
376
|
+
verdict,
|
|
377
|
+
disclosure: compareDisclosure(a, b, sharedSuites),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The sign test must be at least this convincing before the sentence names a winner. */
|
|
382
|
+
const CLAIM_P = 0.05
|
|
383
|
+
|
|
384
|
+
/** Bounds are judged at the precision they are printed at. */
|
|
385
|
+
function atPrintedPrecision(value: number): number {
|
|
386
|
+
return Math.round(value * 1000) / 1000 || 0
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function decide(mean: Interval, paired: number, flips: Flips): EvalComparison['verdict'] {
|
|
390
|
+
const none = 'no difference can be claimed at this size'
|
|
391
|
+
if (!mean.computed) {
|
|
392
|
+
return { claim: 'none', sentence: none, detail: `no interval: ${mean.reason}.` }
|
|
393
|
+
}
|
|
394
|
+
const range = `[${mean.low.toFixed(3)}, ${mean.high.toFixed(3)}]`
|
|
395
|
+
// A bound is zero when it prints as zero: sums that differ by rounding only
|
|
396
|
+
// (the same scores added in another order) must not read as a difference.
|
|
397
|
+
const low = atPrintedPrecision(mean.low)
|
|
398
|
+
const high = atPrintedPrecision(mean.high)
|
|
399
|
+
const excludesZero = (low > 0 || high < 0) && atPrintedPrecision(mean.estimate) !== 0
|
|
400
|
+
if (!excludesZero) {
|
|
401
|
+
return {
|
|
402
|
+
claim: 'none',
|
|
403
|
+
sentence: none,
|
|
404
|
+
detail: `the 95% interval ${range} for the difference in mean score over ${paired} paired cases includes zero.`,
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const flipped = flips.onlyAPasses + flips.onlyBPasses
|
|
408
|
+
if (flipped > 0 && flips.pValue >= CLAIM_P) {
|
|
409
|
+
return {
|
|
410
|
+
claim: 'none',
|
|
411
|
+
sentence: none,
|
|
412
|
+
detail: `the 95% interval ${range} over ${paired} paired cases excludes zero, but the exact test on the ${flipped} case${flipped === 1 ? '' : 's'} that flipped gives p = ${flips.pValue.toFixed(4)}, not below ${CLAIM_P}; two checks that disagree are not a finding.`,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const better = low > 0
|
|
416
|
+
const size = Math.abs(mean.estimate).toFixed(3)
|
|
417
|
+
return {
|
|
418
|
+
claim: better ? 'better' : 'worse',
|
|
419
|
+
sentence: `B is ${better ? 'better' : 'worse'} than A by ${size} in mean score, 95% interval ${range}`,
|
|
420
|
+
detail: `the interval over ${paired} paired cases excludes zero${flipped > 0 ? ` and the exact test on the flips agrees (p = ${flips.pValue.toFixed(4)})` : ''}.`,
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function listCases(title: string, refs: readonly CaseRef[]): string[] {
|
|
425
|
+
if (refs.length === 0) return [`${title}: none`]
|
|
426
|
+
const shown = refs.slice(0, 20).map((r) => ` ${r.suite} / ${r.case}`)
|
|
427
|
+
if (refs.length > shown.length) shown.push(` and ${refs.length - shown.length} more`)
|
|
428
|
+
return [`${title} (${refs.length}):`, ...shown]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function intervalText(interval: Interval, show: (n: number) => string): string {
|
|
432
|
+
if (!interval.computed) return `no interval: ${interval.reason}`
|
|
433
|
+
return `95% paired interval [${show(interval.low)}, ${show(interval.high)}]`
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function formatEvalComparison(c: EvalComparison): string {
|
|
437
|
+
const score = (n: number | null) => (n === null ? 'none' : n.toFixed(3))
|
|
438
|
+
const rate = (n: number | null) => (n === null ? 'none' : `${(n * 100).toFixed(1)}%`)
|
|
439
|
+
const points = (n: number) => `${signed(n * 100, 1)} points`
|
|
440
|
+
const lines = [
|
|
441
|
+
`A: ${c.a.label} (${c.a.cases} cases)`,
|
|
442
|
+
`B: ${c.b.label} (${c.b.cases} cases)`,
|
|
443
|
+
`Cases are paired by suite and id; ${c.paired} paired${c.notJudged.length > 0 ? `, ${c.notJudged.length} more present on both but not judged on one side` : ''}. Seed ${c.seed}.`,
|
|
444
|
+
'',
|
|
445
|
+
...listCases('Only in A', c.onlyInA),
|
|
446
|
+
...listCases('Only in B', c.onlyInB),
|
|
447
|
+
'',
|
|
448
|
+
`Mean score: A ${score(c.sideA.mean)}, B ${score(c.sideB.mean)}${c.difference.mean.estimate !== null ? `, difference ${signed(c.difference.mean.estimate)}` : ''}; ${intervalText(c.difference.mean, (n) => signed(n))}`,
|
|
449
|
+
`Pass rate: A ${rate(c.sideA.passRate)}, B ${rate(c.sideB.passRate)}${c.difference.passRate.estimate !== null ? `, difference ${points(c.difference.passRate.estimate)}` : ''}; ${intervalText(c.difference.passRate, (n) => points(n))}`,
|
|
450
|
+
`Flips: ${c.flips.onlyBPasses} case${c.flips.onlyBPasses === 1 ? '' : 's'} passed on B and not on A, ${c.flips.onlyAPasses} passed on A and not on B (${c.flips.bothPass} passed on both, ${c.flips.bothFail} failed on both). A case passes when every one of its runs passed. ${c.flips.test}: p = ${c.flips.pValue.toFixed(4)}`,
|
|
451
|
+
'',
|
|
452
|
+
`${c.verdict.sentence}.`,
|
|
453
|
+
` (${c.verdict.detail})`,
|
|
454
|
+
'',
|
|
455
|
+
]
|
|
456
|
+
const d = c.disclosure
|
|
457
|
+
if (d.differences.length === 0) {
|
|
458
|
+
lines.push('What the two reports say about how they were produced: no stated difference.')
|
|
459
|
+
} else {
|
|
460
|
+
lines.push('What differs in how the two reports were produced:')
|
|
461
|
+
for (const x of d.differences) lines.push(` ${x.key}: A ${x.a}, B ${x.b}`)
|
|
462
|
+
}
|
|
463
|
+
if (d.unknown.length > 0) {
|
|
464
|
+
lines.push('Stated on one side only, so it cannot be compared:')
|
|
465
|
+
for (const x of d.unknown) {
|
|
466
|
+
lines.push(
|
|
467
|
+
` ${x.key}: ${x.missingIn === 'a' ? 'A does not say' : 'B does not say'}, the other says ${x.value}`,
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (d.warning) lines.push('', `Warning: ${d.warning}`)
|
|
472
|
+
return lines.join('\n')
|
|
473
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import type { EvalRunOptions } from './run-options.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a report says about how it was produced.
|
|
7
|
+
*
|
|
8
|
+
* A score without its provenance cannot be compared with another: two runs
|
|
9
|
+
* three points apart may differ by a model, a route, a reasoning effort or a
|
|
10
|
+
* harness version, and the number alone does not say which. Every fact here
|
|
11
|
+
* is either known or the literal string `unknown`. Nothing is guessed, and a
|
|
12
|
+
* report written before this existed has none of it and is read as unknown.
|
|
13
|
+
*/
|
|
14
|
+
export type RunMode =
|
|
15
|
+
/** The model is a script; nothing measures a vendor. */
|
|
16
|
+
| 'scripted'
|
|
17
|
+
/** A real provider answered. */
|
|
18
|
+
| 'real-model'
|
|
19
|
+
/** No model was involved at all. */
|
|
20
|
+
| 'no-model'
|
|
21
|
+
| 'unknown'
|
|
22
|
+
|
|
23
|
+
export interface RoleDisclosure {
|
|
24
|
+
readonly provider: string
|
|
25
|
+
readonly model: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The routing configuration, `off` when routing was not on. */
|
|
29
|
+
export type RoutingDisclosure = 'off' | 'unknown' | Readonly<Record<string, unknown>>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a suite declares about itself in its `runExperiment` config. Every
|
|
33
|
+
* field is optional; the ones left out are written `unknown`.
|
|
34
|
+
*/
|
|
35
|
+
export interface SuiteFacts {
|
|
36
|
+
readonly mode?: RunMode
|
|
37
|
+
/** One entry per role that ran, such as `agent`, `judge`, `child`. */
|
|
38
|
+
readonly roles?: Readonly<Record<string, { provider?: string; model?: string }>>
|
|
39
|
+
readonly routing?: RoutingDisclosure
|
|
40
|
+
/** The reasoning effort the model ran at, or `not applicable`. */
|
|
41
|
+
readonly reasoningEffort?: string
|
|
42
|
+
/** Version of the suite. The runner fills it from the suite's package when it can. */
|
|
43
|
+
readonly suiteVersion?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ReportDisclosure {
|
|
47
|
+
/** The program that ran the suites, for example the CLI's version. */
|
|
48
|
+
readonly harnessVersion: string
|
|
49
|
+
readonly sdkVersion: string
|
|
50
|
+
/** ISO time the experiment finished. */
|
|
51
|
+
readonly generatedAt: string
|
|
52
|
+
/** Cases in the suite. */
|
|
53
|
+
readonly cases: number
|
|
54
|
+
readonly repeats: number
|
|
55
|
+
readonly seed: number
|
|
56
|
+
readonly mode: RunMode
|
|
57
|
+
/** `unknown` when the suite declared no roles; an empty object when no model ran. */
|
|
58
|
+
readonly roles: Readonly<Record<string, RoleDisclosure>> | 'unknown'
|
|
59
|
+
readonly routing: RoutingDisclosure
|
|
60
|
+
readonly reasoningEffort: string
|
|
61
|
+
readonly suiteVersion: string
|
|
62
|
+
/** Source revision, when the runner could read one. */
|
|
63
|
+
readonly commit: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const UNKNOWN = 'unknown'
|
|
67
|
+
|
|
68
|
+
let cachedSdkVersion: string | undefined
|
|
69
|
+
|
|
70
|
+
/** The version of this SDK, read from its manifest; `unknown` when it cannot be read. */
|
|
71
|
+
export function sdkVersion(): string {
|
|
72
|
+
if (cachedSdkVersion !== undefined) return cachedSdkVersion
|
|
73
|
+
try {
|
|
74
|
+
// `src/eval/` and `dist/eval/` are both two levels below the package.
|
|
75
|
+
const path = fileURLToPath(new URL('../../package.json', import.meta.url))
|
|
76
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
|
|
77
|
+
cachedSdkVersion =
|
|
78
|
+
typeof manifest.version === 'string' && manifest.version.length > 0
|
|
79
|
+
? manifest.version
|
|
80
|
+
: UNKNOWN
|
|
81
|
+
} catch {
|
|
82
|
+
cachedSdkVersion = UNKNOWN
|
|
83
|
+
}
|
|
84
|
+
return cachedSdkVersion
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function text(value: string | undefined): string {
|
|
88
|
+
return value !== undefined && value.length > 0 ? value : UNKNOWN
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildDisclosure(
|
|
92
|
+
facts: SuiteFacts | undefined,
|
|
93
|
+
counts: { cases: number; repeats: number; seed: number },
|
|
94
|
+
options: EvalRunOptions,
|
|
95
|
+
generatedAt: string,
|
|
96
|
+
): ReportDisclosure {
|
|
97
|
+
let roles: ReportDisclosure['roles'] = UNKNOWN
|
|
98
|
+
if (facts?.roles !== undefined) {
|
|
99
|
+
const declared: Record<string, RoleDisclosure> = {}
|
|
100
|
+
for (const [role, who] of Object.entries(facts.roles)) {
|
|
101
|
+
declared[role] = { provider: text(who.provider), model: text(who.model) }
|
|
102
|
+
}
|
|
103
|
+
roles = declared
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
harnessVersion: text(options.harnessVersion),
|
|
107
|
+
sdkVersion: sdkVersion(),
|
|
108
|
+
generatedAt,
|
|
109
|
+
cases: counts.cases,
|
|
110
|
+
repeats: counts.repeats,
|
|
111
|
+
seed: counts.seed,
|
|
112
|
+
mode: facts?.mode ?? UNKNOWN,
|
|
113
|
+
roles,
|
|
114
|
+
routing: facts?.routing ?? UNKNOWN,
|
|
115
|
+
reasoningEffort: text(facts?.reasoningEffort),
|
|
116
|
+
suiteVersion: text(facts?.suiteVersion ?? options.suiteVersion),
|
|
117
|
+
commit: text(options.commit),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One plain line for a terminal. */
|
|
122
|
+
export function describeDisclosure(d: ReportDisclosure): string {
|
|
123
|
+
const roles =
|
|
124
|
+
d.roles === UNKNOWN
|
|
125
|
+
? 'models unknown'
|
|
126
|
+
: Object.keys(d.roles).length === 0
|
|
127
|
+
? 'no model'
|
|
128
|
+
: Object.entries(d.roles)
|
|
129
|
+
.map(([role, who]) => `${role} ${who.provider}/${who.model}`)
|
|
130
|
+
.join(', ')
|
|
131
|
+
const routing =
|
|
132
|
+
d.routing === 'off' || d.routing === UNKNOWN
|
|
133
|
+
? `routing ${d.routing}`
|
|
134
|
+
: `routing ${JSON.stringify(d.routing)}`
|
|
135
|
+
return `ran as ${d.mode}: ${roles}; ${routing}; effort ${d.reasoningEffort}; suite ${d.suiteVersion}`
|
|
136
|
+
}
|