@hawkeyexl/manni 4.2.0 → 4.3.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/README.md +2 -2
- package/dist/batch-7JJWXWSF.js +18 -0
- package/dist/batch-7JJWXWSF.js.map +1 -0
- package/dist/calibrate-IA2ARFMH.js +13 -0
- package/dist/calibrate-IA2ARFMH.js.map +1 -0
- package/dist/capture-C32JXIFN.js +15 -0
- package/dist/capture-C32JXIFN.js.map +1 -0
- package/dist/chunk-3R23C5DT.js +120 -0
- package/dist/chunk-3R23C5DT.js.map +1 -0
- package/dist/chunk-CI2GOU3A.js +4447 -0
- package/dist/chunk-CI2GOU3A.js.map +1 -0
- package/dist/{chunk-TH565O2N.js → chunk-DP2ZA3WH.js} +898 -278
- package/dist/chunk-DP2ZA3WH.js.map +1 -0
- package/dist/chunk-ESJ5W7EV.js +297 -0
- package/dist/chunk-ESJ5W7EV.js.map +1 -0
- package/dist/chunk-IOYXPBCL.js +37 -0
- package/dist/chunk-IOYXPBCL.js.map +1 -0
- package/dist/chunk-KTSJ2ZHA.js +40 -0
- package/dist/chunk-KTSJ2ZHA.js.map +1 -0
- package/dist/{chunk-BNHQWIAE.js → chunk-MFDD3YGU.js} +2 -2
- package/dist/chunk-MOQXWXSG.js +1941 -0
- package/dist/chunk-MOQXWXSG.js.map +1 -0
- package/dist/{chunk-UYDRDFYF.js → chunk-O6RVQKK6.js} +1943 -909
- package/dist/chunk-O6RVQKK6.js.map +1 -0
- package/dist/chunk-QROCFW5S.js +174 -0
- package/dist/chunk-QROCFW5S.js.map +1 -0
- package/dist/chunk-RVVCA4WL.js +379 -0
- package/dist/chunk-RVVCA4WL.js.map +1 -0
- package/dist/cli.js +422 -42
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +3 -2
- package/dist/docmeta.js.map +1 -1
- package/dist/index.d.ts +3873 -795
- package/dist/index.js +292 -4
- package/dist/index.js.map +1 -1
- package/dist/picker-6HRMETE7.js +12 -0
- package/dist/picker-6HRMETE7.js.map +1 -0
- package/package.json +3 -1
- package/plugin/tracevals/.claude-plugin/plugin.json +9 -0
- package/plugin/tracevals/hooks/hooks.json +17 -0
- package/dist/chunk-TH565O2N.js.map +0 -1
- package/dist/chunk-UYDRDFYF.js.map +0 -1
- /package/dist/{chunk-BNHQWIAE.js.map → chunk-MFDD3YGU.js.map} +0 -0
|
@@ -0,0 +1,4447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
NOT_GRANTED_REASON,
|
|
4
|
+
applyAvailabilityRecord,
|
|
5
|
+
declaredAt,
|
|
6
|
+
discoverConfig,
|
|
7
|
+
extractEvals,
|
|
8
|
+
findManifest,
|
|
9
|
+
foldBranchAvailability,
|
|
10
|
+
grantsFor,
|
|
11
|
+
isExecutionGrant,
|
|
12
|
+
makeRedactor,
|
|
13
|
+
newAvailability,
|
|
14
|
+
newReplay,
|
|
15
|
+
remapAvailability,
|
|
16
|
+
resolveArtifacts,
|
|
17
|
+
unknownGrantsMessage
|
|
18
|
+
} from "./chunk-MOQXWXSG.js";
|
|
19
|
+
import {
|
|
20
|
+
DocmetaError,
|
|
21
|
+
PATH_JOIN,
|
|
22
|
+
assertKnownProvider,
|
|
23
|
+
assertLocalFlag,
|
|
24
|
+
assertModelHasProvider,
|
|
25
|
+
classifyRef,
|
|
26
|
+
escapeWorkflowCommandMessage,
|
|
27
|
+
escapeWorkflowCommandProperty,
|
|
28
|
+
externalMetadataJoin,
|
|
29
|
+
extractFrontmatter,
|
|
30
|
+
familyMarks,
|
|
31
|
+
hasPagePlaceholder,
|
|
32
|
+
loadExternalMetadata,
|
|
33
|
+
localNotice,
|
|
34
|
+
memberOf,
|
|
35
|
+
mergeWithMarks,
|
|
36
|
+
noticeOnce,
|
|
37
|
+
ownsKey,
|
|
38
|
+
palette,
|
|
39
|
+
providerSpecFor,
|
|
40
|
+
resolveIdentity,
|
|
41
|
+
selectProvider,
|
|
42
|
+
warn,
|
|
43
|
+
xmlEscape
|
|
44
|
+
} from "./chunk-DP2ZA3WH.js";
|
|
45
|
+
import {
|
|
46
|
+
TracevalsError,
|
|
47
|
+
discoverTraces
|
|
48
|
+
} from "./chunk-3R23C5DT.js";
|
|
49
|
+
import {
|
|
50
|
+
errorMessage
|
|
51
|
+
} from "./chunk-IOYXPBCL.js";
|
|
52
|
+
|
|
53
|
+
// src/tracevals/commands/batch.ts
|
|
54
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
55
|
+
|
|
56
|
+
// src/docevals/judge/budget.ts
|
|
57
|
+
var TURN_BUDGET_SKIP = "turn budget";
|
|
58
|
+
function turnBudgetSkipReason(maxTurns) {
|
|
59
|
+
return `judge ${TURN_BUDGET_SKIP} exhausted (${maxTurns})`;
|
|
60
|
+
}
|
|
61
|
+
function isTurnBudgetSkip(reason) {
|
|
62
|
+
return reason !== void 0 && reason.includes(TURN_BUDGET_SKIP);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/tracevals/aggregate.ts
|
|
66
|
+
var zero = () => ({
|
|
67
|
+
pass: 0,
|
|
68
|
+
fail: 0,
|
|
69
|
+
error: 0,
|
|
70
|
+
needsReview: 0,
|
|
71
|
+
skipped: 0
|
|
72
|
+
});
|
|
73
|
+
function tally(counts2, outcome) {
|
|
74
|
+
if (outcome === "needs-review") counts2.needsReview += 1;
|
|
75
|
+
else counts2[outcome] += 1;
|
|
76
|
+
}
|
|
77
|
+
var byKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
78
|
+
function accumulate(into, key, result, file, withEvalName) {
|
|
79
|
+
let row = into.get(key);
|
|
80
|
+
if (row === void 0) {
|
|
81
|
+
row = {
|
|
82
|
+
key,
|
|
83
|
+
artifactName: result.artifactName,
|
|
84
|
+
artifactType: result.artifactType,
|
|
85
|
+
...withEvalName ? { evalName: result.evalName } : {},
|
|
86
|
+
artifacts: /* @__PURE__ */ new Set(),
|
|
87
|
+
graders: /* @__PURE__ */ new Set(),
|
|
88
|
+
traces: /* @__PURE__ */ new Set(),
|
|
89
|
+
counts: zero(),
|
|
90
|
+
failingTraces: [],
|
|
91
|
+
reviewTraces: [],
|
|
92
|
+
skipReasons: /* @__PURE__ */ new Set()
|
|
93
|
+
};
|
|
94
|
+
into.set(key, row);
|
|
95
|
+
}
|
|
96
|
+
row.artifacts.add(result.artifact);
|
|
97
|
+
row.graders.add(result.grader);
|
|
98
|
+
row.traces.add(file);
|
|
99
|
+
tally(row.counts, result.outcome);
|
|
100
|
+
if (result.outcome === "fail" || result.outcome === "error") {
|
|
101
|
+
if (!row.failingTraces.includes(file)) row.failingTraces.push(file);
|
|
102
|
+
}
|
|
103
|
+
if (result.outcome === "needs-review" && !row.reviewTraces.includes(file)) {
|
|
104
|
+
row.reviewTraces.push(file);
|
|
105
|
+
}
|
|
106
|
+
if (result.outcome === "skipped" && result.skipReason !== void 0) {
|
|
107
|
+
row.skipReasons.add(result.skipReason);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function finish(row, withGraders) {
|
|
111
|
+
const c = row.counts;
|
|
112
|
+
const total = c.pass + c.fail + c.error + c.needsReview + c.skipped;
|
|
113
|
+
const graded = total - c.skipped;
|
|
114
|
+
return {
|
|
115
|
+
key: row.key,
|
|
116
|
+
artifactName: row.artifactName,
|
|
117
|
+
artifactType: row.artifactType,
|
|
118
|
+
artifacts: [...row.artifacts].sort(),
|
|
119
|
+
...row.evalName !== void 0 ? { evalName: row.evalName } : {},
|
|
120
|
+
...withGraders ? { graders: [...row.graders].sort() } : {},
|
|
121
|
+
traces: row.traces.size,
|
|
122
|
+
total,
|
|
123
|
+
counts: { ...c },
|
|
124
|
+
graded,
|
|
125
|
+
passRate: graded > 0 ? c.pass / graded : null,
|
|
126
|
+
failingTraces: [...row.failingTraces],
|
|
127
|
+
reviewTraces: [...row.reviewTraces],
|
|
128
|
+
skipReasons: [...row.skipReasons].sort()
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
var artifactKey = (r) => `${r.artifactType}:${r.artifactName}`;
|
|
132
|
+
function aggregate(outcomes, options) {
|
|
133
|
+
const artifactRows = /* @__PURE__ */ new Map();
|
|
134
|
+
const evalRows = /* @__PURE__ */ new Map();
|
|
135
|
+
const traces = [];
|
|
136
|
+
let budgetSkips = 0;
|
|
137
|
+
const budgetTraces = /* @__PURE__ */ new Set();
|
|
138
|
+
let budgetReason = "";
|
|
139
|
+
const summary = {
|
|
140
|
+
total: 0,
|
|
141
|
+
pass: 0,
|
|
142
|
+
fail: 0,
|
|
143
|
+
error: 0,
|
|
144
|
+
needsReview: 0,
|
|
145
|
+
skipped: 0,
|
|
146
|
+
passRate: 1,
|
|
147
|
+
traces: outcomes.length,
|
|
148
|
+
tracesPassed: 0,
|
|
149
|
+
tracesFailed: 0,
|
|
150
|
+
tracesErrored: 0
|
|
151
|
+
};
|
|
152
|
+
let turns = 0;
|
|
153
|
+
let gradedWeight = 0;
|
|
154
|
+
let passedWeight = 0;
|
|
155
|
+
for (const outcome of outcomes) {
|
|
156
|
+
if ("error" in outcome) {
|
|
157
|
+
traces.push({
|
|
158
|
+
file: outcome.file,
|
|
159
|
+
error: outcome.error,
|
|
160
|
+
warnings: [],
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
turns: 0,
|
|
163
|
+
durationMs: outcome.durationMs
|
|
164
|
+
});
|
|
165
|
+
summary.tracesErrored += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const { report } = outcome;
|
|
169
|
+
traces.push({
|
|
170
|
+
file: outcome.file,
|
|
171
|
+
...report.trace.sessionId !== void 0 ? { sessionId: report.trace.sessionId } : {},
|
|
172
|
+
summary: report.summary,
|
|
173
|
+
warnings: report.warnings,
|
|
174
|
+
exitCode: report.exitCode,
|
|
175
|
+
turns: report.turns,
|
|
176
|
+
durationMs: report.durationMs
|
|
177
|
+
});
|
|
178
|
+
if (report.exitCode === 1) summary.tracesFailed += 1;
|
|
179
|
+
else summary.tracesPassed += 1;
|
|
180
|
+
turns += report.turns;
|
|
181
|
+
summary.total += report.summary.total;
|
|
182
|
+
summary.pass += report.summary.pass;
|
|
183
|
+
summary.fail += report.summary.fail;
|
|
184
|
+
summary.error += report.summary.error;
|
|
185
|
+
summary.needsReview += report.summary.needsReview;
|
|
186
|
+
summary.skipped += report.summary.skipped;
|
|
187
|
+
for (const result of report.evalResults) {
|
|
188
|
+
if (result.outcome === "skipped" && isTurnBudgetSkip(result.skipReason)) {
|
|
189
|
+
budgetSkips += 1;
|
|
190
|
+
budgetTraces.add(outcome.file);
|
|
191
|
+
budgetReason = result.skipReason;
|
|
192
|
+
}
|
|
193
|
+
if (result.outcome === "pass" || result.outcome === "fail" || result.outcome === "error") {
|
|
194
|
+
const weight = result.weight ?? 1;
|
|
195
|
+
gradedWeight += weight;
|
|
196
|
+
if (result.outcome === "pass") passedWeight += weight;
|
|
197
|
+
}
|
|
198
|
+
const base = artifactKey(result);
|
|
199
|
+
accumulate(artifactRows, base, result, outcome.file, false);
|
|
200
|
+
accumulate(
|
|
201
|
+
evalRows,
|
|
202
|
+
`${base}::${result.evalName}`,
|
|
203
|
+
result,
|
|
204
|
+
outcome.file,
|
|
205
|
+
true
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
summary.passRate = gradedWeight === 0 ? 1 : passedWeight / gradedWeight;
|
|
210
|
+
const budget = budgetSkips > 0 ? {
|
|
211
|
+
skippedEvals: budgetSkips,
|
|
212
|
+
traces: budgetTraces.size,
|
|
213
|
+
reason: budgetReason
|
|
214
|
+
} : void 0;
|
|
215
|
+
const warnings = [...options.warnings ?? []];
|
|
216
|
+
if (budget !== void 0) {
|
|
217
|
+
warnings.push(
|
|
218
|
+
`${budget.reason}: ${budget.skippedEvals} eval(s) across ${budget.traces} of ${outcomes.length} trace(s) were never judged \u2014 raise judge.maxTurns or narrow the corpus`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
traces,
|
|
223
|
+
artifacts: [...artifactRows.values()].map((r) => finish(r, false)).sort(byKey),
|
|
224
|
+
evals: [...evalRows.values()].map((r) => finish(r, true)).sort(byKey),
|
|
225
|
+
summary,
|
|
226
|
+
warnings,
|
|
227
|
+
// A batch that ran out of money mid-corpus did not measure the corpus, and
|
|
228
|
+
// "did not measure" must never render as "measured clean". Exit 1 rather
|
|
229
|
+
// than 2: unlike an empty selector, the report is real and worth keeping —
|
|
230
|
+
// the traces that were judged carry genuine verdicts, and throwing them
|
|
231
|
+
// away to raise an operational error would cost more than it says.
|
|
232
|
+
exitCode: summary.tracesFailed > 0 || summary.tracesErrored > 0 || budget !== void 0 ? 1 : 0,
|
|
233
|
+
...budget !== void 0 ? { budget } : {},
|
|
234
|
+
turns,
|
|
235
|
+
durationMs: options.durationMs
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/shared/duration.ts
|
|
240
|
+
var UNITS = {
|
|
241
|
+
m: 6e4,
|
|
242
|
+
h: 60 * 6e4,
|
|
243
|
+
d: 24 * 60 * 6e4,
|
|
244
|
+
w: 7 * 24 * 60 * 6e4
|
|
245
|
+
};
|
|
246
|
+
function durationMessage(value) {
|
|
247
|
+
return `--newer-than must be a duration such as 30m, 24h, 7d or 2w, got "${value}"`;
|
|
248
|
+
}
|
|
249
|
+
function parseDuration(text, makeError) {
|
|
250
|
+
const match = /^(\d+(?:\.\d+)?)([mhdw])$/.exec(text.trim());
|
|
251
|
+
const count = match?.[1];
|
|
252
|
+
const unit = match?.[2];
|
|
253
|
+
const ms = unit === void 0 ? void 0 : UNITS[unit];
|
|
254
|
+
if (count === void 0 || ms === void 0) {
|
|
255
|
+
throw makeError(durationMessage(text));
|
|
256
|
+
}
|
|
257
|
+
return Number(count) * ms;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/tracevals/reporters/batch.ts
|
|
261
|
+
function budgetLine(report) {
|
|
262
|
+
const b = report.budget;
|
|
263
|
+
if (b === void 0) return void 0;
|
|
264
|
+
return `${b.skippedEvals} eval(s) across ${b.traces} trace(s) were never judged \u2014 ${b.reason}`;
|
|
265
|
+
}
|
|
266
|
+
var cell = (value) => value.replace(/\|/g, "\\|");
|
|
267
|
+
function rate(row) {
|
|
268
|
+
return row.passRate === null ? "\u2014" : `${Math.round(row.passRate * 1e3) / 10}%`;
|
|
269
|
+
}
|
|
270
|
+
function detail(row) {
|
|
271
|
+
const parts = [];
|
|
272
|
+
const c = row.counts;
|
|
273
|
+
parts.push(
|
|
274
|
+
`${c.pass} pass, ${c.fail} fail, ${c.error} error, ${c.needsReview} review, ${c.skipped} skip`
|
|
275
|
+
);
|
|
276
|
+
return parts.join(" \xB7 ");
|
|
277
|
+
}
|
|
278
|
+
function names(files, limit = 3) {
|
|
279
|
+
const shown = files.slice(0, limit).map((f) => basename(f));
|
|
280
|
+
return files.length > limit ? `${shown.join(", ")} +${files.length - limit} more` : shown.join(", ");
|
|
281
|
+
}
|
|
282
|
+
function basename(file) {
|
|
283
|
+
const parts = file.split(/[\\/]/);
|
|
284
|
+
return parts[parts.length - 1] ?? file;
|
|
285
|
+
}
|
|
286
|
+
function outliers(row) {
|
|
287
|
+
const parts = [];
|
|
288
|
+
if (row.failingTraces.length > 0) {
|
|
289
|
+
parts.push(`fail: ${names(row.failingTraces)}`);
|
|
290
|
+
}
|
|
291
|
+
if (row.reviewTraces.length > 0) {
|
|
292
|
+
parts.push(`review: ${names(row.reviewTraces)}`);
|
|
293
|
+
}
|
|
294
|
+
return parts.join("; ");
|
|
295
|
+
}
|
|
296
|
+
function renderBatchPretty(report, opts = {}) {
|
|
297
|
+
const pc = palette(opts.color === true);
|
|
298
|
+
const lines = [];
|
|
299
|
+
const s = report.summary;
|
|
300
|
+
lines.push(pc.bold(`manni tracevals \u2014 ${s.traces} trace(s)`));
|
|
301
|
+
lines.push(
|
|
302
|
+
pc.dim(
|
|
303
|
+
`${s.tracesPassed} passed \xB7 ${s.tracesFailed} failed \xB7 ${s.tracesErrored} unreadable`
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
const cutShort = budgetLine(report);
|
|
307
|
+
if (cutShort !== void 0) {
|
|
308
|
+
lines.push(pc.red(`${pc.bold("BUDGET EXHAUSTED")} ${cutShort}`));
|
|
309
|
+
}
|
|
310
|
+
lines.push("");
|
|
311
|
+
lines.push(pc.bold("Eval pass rates"));
|
|
312
|
+
if (report.evals.length === 0) {
|
|
313
|
+
lines.push(pc.dim(" no evals were planned for any trace"));
|
|
314
|
+
}
|
|
315
|
+
for (const row of report.evals) {
|
|
316
|
+
const label = row.passRate === null ? pc.dim(rate(row).padStart(6)) : row.passRate === 1 ? pc.green(rate(row).padStart(6)) : pc.red(rate(row).padStart(6));
|
|
317
|
+
lines.push(
|
|
318
|
+
` ${label} ${row.artifactName} \u203A ${row.evalName ?? ""} ${pc.dim(`(${row.traces} trace(s))`)}`
|
|
319
|
+
);
|
|
320
|
+
lines.push(` ${pc.dim(detail(row))}`);
|
|
321
|
+
if (row.failingTraces.length > 0) {
|
|
322
|
+
lines.push(` ${pc.red(`failing: ${names(row.failingTraces)}`)}`);
|
|
323
|
+
}
|
|
324
|
+
if (row.reviewTraces.length > 0) {
|
|
325
|
+
lines.push(
|
|
326
|
+
` ${pc.yellow(`review: ${names(row.reviewTraces)}`)}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
for (const reason of row.skipReasons) {
|
|
330
|
+
lines.push(` ${pc.dim(`skipped: ${reason}`)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push(pc.bold("Artifact pass rates"));
|
|
335
|
+
for (const row of report.artifacts) {
|
|
336
|
+
lines.push(
|
|
337
|
+
` ${rate(row).padStart(6)} ${row.artifactType}: ${row.artifactName} ${pc.dim(detail(row))}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
lines.push("");
|
|
341
|
+
lines.push(pc.bold("Traces"));
|
|
342
|
+
for (const t of report.traces) {
|
|
343
|
+
if (t.error !== void 0) {
|
|
344
|
+
lines.push(` ${pc.red("ERROR ")} ${basename(t.file)}`);
|
|
345
|
+
lines.push(` ${pc.red(t.error)}`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const mark = t.exitCode === 0 ? pc.green("PASS ") : pc.red("FAIL ");
|
|
349
|
+
const ts = t.summary;
|
|
350
|
+
lines.push(
|
|
351
|
+
` ${mark} ${basename(t.file)} ${pc.dim(
|
|
352
|
+
`${ts?.pass ?? 0} pass, ${ts?.fail ?? 0} fail, ${ts?.error ?? 0} error, ${ts?.needsReview ?? 0} review, ${ts?.skipped ?? 0} skip`
|
|
353
|
+
)}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (report.warnings.length > 0) {
|
|
357
|
+
lines.push("");
|
|
358
|
+
lines.push(pc.bold("Warnings"));
|
|
359
|
+
for (const warning of report.warnings) {
|
|
360
|
+
lines.push(` ${pc.yellow("!")} ${warning}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
lines.push("");
|
|
364
|
+
lines.push(
|
|
365
|
+
pc.bold(
|
|
366
|
+
`${s.total} eval(s) across ${s.traces} trace(s): ${pc.green(`${s.pass} pass`)}, ${s.fail > 0 ? pc.red(`${s.fail} fail`) : `${s.fail} fail`}, ${s.error} error, ${s.needsReview} needs-review, ${s.skipped} skipped`
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
if (report.turns > 0) {
|
|
370
|
+
lines.push(pc.dim(`judge turns ${report.turns}`));
|
|
371
|
+
}
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
function renderBatchMarkdown(report) {
|
|
375
|
+
const lines = [];
|
|
376
|
+
const s = report.summary;
|
|
377
|
+
lines.push(`# manni tracevals batch report`);
|
|
378
|
+
lines.push("");
|
|
379
|
+
lines.push(`- **Traces**: ${s.traces}`);
|
|
380
|
+
lines.push(
|
|
381
|
+
`- **Outcome**: ${s.tracesPassed} passed, ${s.tracesFailed} failed, ${s.tracesErrored} unreadable`
|
|
382
|
+
);
|
|
383
|
+
const cutShort = budgetLine(report);
|
|
384
|
+
if (cutShort !== void 0) lines.push(`- **Budget**: ${cutShort}`);
|
|
385
|
+
lines.push("");
|
|
386
|
+
lines.push(`## Eval pass rates`);
|
|
387
|
+
lines.push("");
|
|
388
|
+
lines.push(`| Rate | Artifact | Eval | Grader | Traces | Outcomes | Outliers |`);
|
|
389
|
+
lines.push(`|---|---|---|---|---|---|---|`);
|
|
390
|
+
for (const row of report.evals) {
|
|
391
|
+
lines.push(
|
|
392
|
+
`| ${rate(row)} | ${cell(row.artifactName)} | ${cell(row.evalName ?? "")} | ${cell(
|
|
393
|
+
(row.graders ?? []).join(", ")
|
|
394
|
+
)} | ${row.traces} | ${cell(detail(row))} | ${cell(outliers(row))} |`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
lines.push("");
|
|
398
|
+
lines.push(`## Artifact pass rates`);
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push(`| Rate | Kind | Artifact | Traces | Outcomes |`);
|
|
401
|
+
lines.push(`|---|---|---|---|---|`);
|
|
402
|
+
for (const row of report.artifacts) {
|
|
403
|
+
lines.push(
|
|
404
|
+
`| ${rate(row)} | ${cell(row.artifactType)} | ${cell(row.artifactName)} | ${row.traces} | ${cell(detail(row))} |`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
lines.push("");
|
|
408
|
+
lines.push(`## Traces`);
|
|
409
|
+
lines.push("");
|
|
410
|
+
lines.push(`| Result | Trace | Evals | Detail |`);
|
|
411
|
+
lines.push(`|---|---|---|---|`);
|
|
412
|
+
for (const t of report.traces) {
|
|
413
|
+
const result = t.error !== void 0 ? "error" : t.exitCode === 0 ? "pass" : "fail";
|
|
414
|
+
const evals = t.summary === void 0 ? "" : `${t.summary.pass}/${t.summary.total} pass`;
|
|
415
|
+
lines.push(
|
|
416
|
+
`| ${result} | \`${cell(t.file)}\` | ${evals} | ${cell(t.error ?? "")} |`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
lines.push("");
|
|
420
|
+
if (report.warnings.length > 0) {
|
|
421
|
+
lines.push(`## Warnings`);
|
|
422
|
+
lines.push("");
|
|
423
|
+
for (const warning of report.warnings) lines.push(`- ${warning}`);
|
|
424
|
+
lines.push("");
|
|
425
|
+
}
|
|
426
|
+
lines.push(
|
|
427
|
+
`**${s.total} eval(s)** across ${s.traces} trace(s) \u2014 ${s.pass} pass, ${s.fail} fail, ${s.error} error, ${s.needsReview} needs-review, ${s.skipped} skipped.`
|
|
428
|
+
);
|
|
429
|
+
return lines.join("\n");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/tracevals/reporters/calibration.ts
|
|
433
|
+
import { isAbsolute, relative } from "node:path";
|
|
434
|
+
var cell2 = (value) => value.replace(/\|/g, "\\|");
|
|
435
|
+
function basename2(file) {
|
|
436
|
+
const parts = file.split(/[\\/]/);
|
|
437
|
+
return parts[parts.length - 1] ?? file;
|
|
438
|
+
}
|
|
439
|
+
function display(file) {
|
|
440
|
+
const rel = relative(process.cwd(), file);
|
|
441
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? file : rel;
|
|
442
|
+
}
|
|
443
|
+
var pct = (value) => value === null ? "\u2014" : `${Math.round(value * 1e3) / 10}%`;
|
|
444
|
+
var settingOf = (c) => `runs=${c.setting.ensembleRuns} autoPass=${c.setting.zones.autoPass} autoFail=${c.setting.zones.autoFail}`;
|
|
445
|
+
var SWEEP_COLUMNS = [
|
|
446
|
+
{ label: "scored", width: 6, of: (c) => c.scored },
|
|
447
|
+
{ label: "agree", width: 5, of: (c) => c.agree },
|
|
448
|
+
{ label: "false-pass", width: 10, of: (c) => c.falsePass },
|
|
449
|
+
{ label: "false-fail", width: 10, of: (c) => c.falseFail },
|
|
450
|
+
{ label: "review", width: 6, of: (c) => c.review },
|
|
451
|
+
{ label: "reviewVol", width: 9, of: (c) => c.reviewVolume },
|
|
452
|
+
{ label: "insuff", width: 6, of: (c) => c.insufficient }
|
|
453
|
+
];
|
|
454
|
+
var sweepRow = (axis, setting, cells) => ` ${axis.padEnd(20)} ${setting.padEnd(38)} ${cells.join(" ")}`;
|
|
455
|
+
function evidence(d) {
|
|
456
|
+
if (d.consensus === void 0) return "";
|
|
457
|
+
const v = d.consensus.votes;
|
|
458
|
+
return `${v.pass}p/${v.fail}f/${v.partial}?/${v.error}e over ${d.consensus.runs} run(s), confidence ${d.consensus.meanConfidence.toFixed(2)}`;
|
|
459
|
+
}
|
|
460
|
+
var KIND_LABEL = {
|
|
461
|
+
agree: "AGREE",
|
|
462
|
+
"false-pass": "FALSE-PASS",
|
|
463
|
+
"false-fail": "FALSE-FAIL",
|
|
464
|
+
review: "REVIEW",
|
|
465
|
+
"missed-review": "MISSED-REVIEW",
|
|
466
|
+
error: "ERROR",
|
|
467
|
+
skipped: "SKIPPED"
|
|
468
|
+
};
|
|
469
|
+
function renderCalibrationPretty(report, opts = {}) {
|
|
470
|
+
const pc = palette(opts.color === true);
|
|
471
|
+
const lines = [];
|
|
472
|
+
const c = report.counts;
|
|
473
|
+
lines.push(
|
|
474
|
+
pc.bold(
|
|
475
|
+
`manni tracevals calibrate \u2014 ${report.corpus.length} trace(s), ${c.labels} label(s)`
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
lines.push(
|
|
479
|
+
pc.dim(
|
|
480
|
+
`runs ${report.setting.ensembleRuns} \xB7 autoPass ${report.setting.zones.autoPass} \xB7 autoFail ${report.setting.zones.autoFail} \xB7 labels ${display(report.labelsFile)}`
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
lines.push("");
|
|
484
|
+
lines.push(
|
|
485
|
+
pc.bold(
|
|
486
|
+
`Agreement ${c.agree}/${c.scored} (${pct(c.agreement)})`
|
|
487
|
+
)
|
|
488
|
+
);
|
|
489
|
+
const headline = (count, label, gloss, colour) => {
|
|
490
|
+
const text = ` ${String(count).padStart(4)} ${label.padEnd(14)} ${pc.dim(gloss)}`;
|
|
491
|
+
lines.push(count > 0 ? colour(text) : pc.dim(text));
|
|
492
|
+
};
|
|
493
|
+
headline(c.falsePass, "false passes", "labelled fail, judged pass \u2014 the expensive error", pc.red);
|
|
494
|
+
headline(c.falseFail, "false fails", "labelled pass, judged fail \u2014 erodes trust fastest", pc.red);
|
|
495
|
+
headline(c.reviewVolume, "needs-review", "across the whole corpus \u2014 the review volume", pc.yellow);
|
|
496
|
+
headline(c.review, "deferred", "labelled pass or fail, routed to a human instead", pc.yellow);
|
|
497
|
+
headline(c.missedReview, "missed reviews", "labelled ambiguous, decided anyway", pc.yellow);
|
|
498
|
+
headline(c.errored, "errors", "the eval could not be graded at all", pc.red);
|
|
499
|
+
headline(c.skipped, "unscored", "labelled, but the eval never armed", (s) => s);
|
|
500
|
+
if (report.disagreements.length > 0) {
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push(pc.bold("Disagreements"));
|
|
503
|
+
for (const d of report.disagreements) {
|
|
504
|
+
const tag = KIND_LABEL[d.kind];
|
|
505
|
+
const colour = d.kind === "false-pass" || d.kind === "false-fail" || d.kind === "error" ? pc.red : pc.yellow;
|
|
506
|
+
lines.push(
|
|
507
|
+
` ${colour(tag.padEnd(14))} ${basename2(d.trace)} ${d.artifactName} \u203A ${d.evalName} ${pc.dim(`(${d.grader})`)}`
|
|
508
|
+
);
|
|
509
|
+
lines.push(
|
|
510
|
+
` ${pc.dim(`expected ${d.expected}, got ${d.actual}`)}${evidence(d) === "" ? "" : pc.dim(` \xB7 ${evidence(d)}`)}`
|
|
511
|
+
);
|
|
512
|
+
if (d.note !== void 0) {
|
|
513
|
+
lines.push(` ${pc.dim(`note: ${d.note}`)}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (report.unscored.length > 0) {
|
|
518
|
+
lines.push("");
|
|
519
|
+
lines.push(pc.bold("Unscored (no evidence either way)"));
|
|
520
|
+
for (const d of report.unscored) {
|
|
521
|
+
lines.push(
|
|
522
|
+
` ${pc.dim("SKIPPED".padEnd(14))} ${basename2(d.trace)} ${d.artifactName} \u203A ${d.evalName}`
|
|
523
|
+
);
|
|
524
|
+
if (d.skipReason !== void 0) {
|
|
525
|
+
lines.push(` ${pc.dim(d.skipReason)}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (report.sweep !== void 0) {
|
|
530
|
+
lines.push("");
|
|
531
|
+
lines.push(pc.bold("Sweep"));
|
|
532
|
+
lines.push(
|
|
533
|
+
pc.dim(
|
|
534
|
+
" re-scored from the verdicts this run already produced \u2014 no further model calls"
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
lines.push(
|
|
538
|
+
pc.dim(
|
|
539
|
+
sweepRow(
|
|
540
|
+
"axis",
|
|
541
|
+
"setting",
|
|
542
|
+
SWEEP_COLUMNS.map((c2) => c2.label.padStart(c2.width))
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
for (const s of report.sweep) {
|
|
547
|
+
lines.push(
|
|
548
|
+
sweepRow(
|
|
549
|
+
s.axis,
|
|
550
|
+
settingOf(s),
|
|
551
|
+
SWEEP_COLUMNS.map((c2) => String(c2.of(s.counts)).padStart(c2.width))
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (report.gates.length > 0) {
|
|
557
|
+
lines.push("");
|
|
558
|
+
lines.push(pc.bold("Thresholds"));
|
|
559
|
+
for (const g of report.gates) {
|
|
560
|
+
const text = ` ${g.name}: ${g.actual} of at most ${g.limit}`;
|
|
561
|
+
lines.push(g.exceeded ? pc.red(`${text} \u2014 exceeded`) : pc.green(text));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (report.warnings.length > 0) {
|
|
565
|
+
lines.push("");
|
|
566
|
+
lines.push(pc.bold("Warnings"));
|
|
567
|
+
for (const w of report.warnings) lines.push(` ${pc.yellow("!")} ${w}`);
|
|
568
|
+
}
|
|
569
|
+
lines.push("");
|
|
570
|
+
lines.push(pc.dim(verdictLine(report)));
|
|
571
|
+
if (report.turns > 0) {
|
|
572
|
+
lines.push(pc.dim(`judge turns ${report.turns}`));
|
|
573
|
+
}
|
|
574
|
+
return lines.join("\n");
|
|
575
|
+
}
|
|
576
|
+
function countsRow(counts2) {
|
|
577
|
+
return `${counts2.scored} | ${counts2.agree} | ${counts2.falsePass} | ${counts2.falseFail} | ${counts2.review} | ${counts2.missedReview} | ${counts2.reviewVolume} | ${counts2.insufficient}`;
|
|
578
|
+
}
|
|
579
|
+
function verdictLine(report) {
|
|
580
|
+
if (report.counts.scored === 0) {
|
|
581
|
+
return "Exit 1: nothing was scored \u2014 every label joined to a skipped result, so agreement and every threshold above rest on an empty denominator.";
|
|
582
|
+
}
|
|
583
|
+
return report.exitCode === 0 ? "Measured cleanly. Disagreement is the finding, not a failure \u2014 exit 0." : "Exit 1: a threshold was exceeded, or the measurement is incomplete \u2014 a trace could not be evaluated, or the judge budget cut the corpus short.";
|
|
584
|
+
}
|
|
585
|
+
function renderCalibrationMarkdown(report) {
|
|
586
|
+
const lines = [];
|
|
587
|
+
const c = report.counts;
|
|
588
|
+
lines.push("# manni tracevals calibration report");
|
|
589
|
+
lines.push("");
|
|
590
|
+
lines.push(`- **Corpus**: ${report.corpus.length} trace(s), ${c.labels} label(s)`);
|
|
591
|
+
lines.push(
|
|
592
|
+
`- **Setting**: \`ensembleRuns\` ${report.setting.ensembleRuns}, \`zones.autoPass\` ${report.setting.zones.autoPass}, \`zones.autoFail\` ${report.setting.zones.autoFail}`
|
|
593
|
+
);
|
|
594
|
+
lines.push(`- **Agreement**: ${c.agree}/${c.scored} (${pct(c.agreement)})`);
|
|
595
|
+
lines.push(`- **Labels**: \`${cell2(display(report.labelsFile))}\``);
|
|
596
|
+
lines.push("");
|
|
597
|
+
lines.push("| Measure | Count | What it means |");
|
|
598
|
+
lines.push("|---|---|---|");
|
|
599
|
+
lines.push(`| False passes | ${c.falsePass} | Labelled fail, judged pass \u2014 the expensive error. |`);
|
|
600
|
+
lines.push(`| False fails | ${c.falseFail} | Labelled pass, judged fail \u2014 erodes trust fastest. |`);
|
|
601
|
+
lines.push(`| Review volume | ${c.reviewVolume} | \`needs-review\` across the whole corpus. |`);
|
|
602
|
+
lines.push(`| Deferred labels | ${c.review} | Labelled pass or fail, routed to a human instead. |`);
|
|
603
|
+
lines.push(`| Missed reviews | ${c.missedReview} | Labelled ambiguous, decided anyway. |`);
|
|
604
|
+
lines.push(`| Errors | ${c.errored} | The eval could not be graded at all. |`);
|
|
605
|
+
lines.push(`| Unscored | ${c.skipped} | Labelled, but the eval never armed. |`);
|
|
606
|
+
lines.push("");
|
|
607
|
+
lines.push("## Disagreements");
|
|
608
|
+
lines.push("");
|
|
609
|
+
if (report.disagreements.length === 0) {
|
|
610
|
+
lines.push("None \u2014 every scored label agreed.");
|
|
611
|
+
} else {
|
|
612
|
+
lines.push("| Kind | Trace | Artifact | Eval | Grader | Expected | Got | Evidence | Note |");
|
|
613
|
+
lines.push("|---|---|---|---|---|---|---|---|---|");
|
|
614
|
+
for (const d of report.disagreements) {
|
|
615
|
+
lines.push(
|
|
616
|
+
`| ${KIND_LABEL[d.kind]} | \`${cell2(basename2(d.trace))}\` | ${cell2(d.artifactName)} | ${cell2(
|
|
617
|
+
d.evalName
|
|
618
|
+
)} | ${cell2(d.grader)} | ${d.expected} | ${d.actual} | ${cell2(evidence(d))} | ${cell2(d.note ?? "")} |`
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
lines.push("");
|
|
623
|
+
if (report.unscored.length > 0) {
|
|
624
|
+
lines.push("## Unscored");
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push("| Trace | Artifact | Eval | Reason |");
|
|
627
|
+
lines.push("|---|---|---|---|");
|
|
628
|
+
for (const d of report.unscored) {
|
|
629
|
+
lines.push(
|
|
630
|
+
`| \`${cell2(basename2(d.trace))}\` | ${cell2(d.artifactName)} | ${cell2(d.evalName)} | ${cell2(
|
|
631
|
+
d.skipReason ?? ""
|
|
632
|
+
)} |`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
lines.push("");
|
|
636
|
+
}
|
|
637
|
+
if (report.sweep !== void 0) {
|
|
638
|
+
lines.push("## Sweep");
|
|
639
|
+
lines.push("");
|
|
640
|
+
lines.push(
|
|
641
|
+
"Re-scored from the verdicts this run already produced \u2014 no further model calls."
|
|
642
|
+
);
|
|
643
|
+
lines.push("");
|
|
644
|
+
lines.push(
|
|
645
|
+
"| Axis | Setting | Scored | Agree | False passes | False fails | Deferred | Missed reviews | Review volume | Insufficient |"
|
|
646
|
+
);
|
|
647
|
+
lines.push("|---|---|---|---|---|---|---|---|---|---|");
|
|
648
|
+
for (const s of report.sweep) {
|
|
649
|
+
lines.push(`| ${s.axis} | ${cell2(settingOf(s))} | ${countsRow(s.counts)} |`);
|
|
650
|
+
}
|
|
651
|
+
lines.push("");
|
|
652
|
+
}
|
|
653
|
+
if (report.gates.length > 0) {
|
|
654
|
+
lines.push("## Thresholds");
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("| Threshold | Limit | Actual | Result |");
|
|
657
|
+
lines.push("|---|---|---|---|");
|
|
658
|
+
for (const g of report.gates) {
|
|
659
|
+
lines.push(
|
|
660
|
+
`| ${g.name} | ${g.limit} | ${g.actual} | ${g.exceeded ? "exceeded" : "ok"} |`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
lines.push("");
|
|
664
|
+
}
|
|
665
|
+
if (report.warnings.length > 0) {
|
|
666
|
+
lines.push("## Warnings");
|
|
667
|
+
lines.push("");
|
|
668
|
+
for (const w of report.warnings) lines.push(`- ${w}`);
|
|
669
|
+
lines.push("");
|
|
670
|
+
}
|
|
671
|
+
lines.push(
|
|
672
|
+
report.exitCode === 0 ? verdictLine(report) : `**${verdictLine(report)}**`
|
|
673
|
+
);
|
|
674
|
+
return lines.join("\n");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/shared/sarif-location.ts
|
|
678
|
+
var URI_BASE_ID = "SRCROOT";
|
|
679
|
+
var DRIVE = /^[A-Za-z]:\//;
|
|
680
|
+
function toPosix(p) {
|
|
681
|
+
return p.replace(/\\/g, "/");
|
|
682
|
+
}
|
|
683
|
+
function isAbsolutePath(posix) {
|
|
684
|
+
return posix.startsWith("/") || DRIVE.test(posix);
|
|
685
|
+
}
|
|
686
|
+
function isWindowsPath(posix) {
|
|
687
|
+
return DRIVE.test(posix) || posix.startsWith("//");
|
|
688
|
+
}
|
|
689
|
+
function underRoot(posix, root) {
|
|
690
|
+
if (posix.startsWith(root)) return posix.slice(root.length);
|
|
691
|
+
if (isWindowsPath(posix) && isWindowsPath(root)) {
|
|
692
|
+
if (posix.toLowerCase().startsWith(root.toLowerCase())) {
|
|
693
|
+
return posix.slice(root.length);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
function encodePath(p) {
|
|
699
|
+
return encodeURI(p).replace(/#/g, "%23").replace(/\?/g, "%3F");
|
|
700
|
+
}
|
|
701
|
+
function fileUri(posix) {
|
|
702
|
+
const encoded = encodePath(posix);
|
|
703
|
+
if (posix.startsWith("//")) return `file:${encoded}`;
|
|
704
|
+
if (posix.startsWith("/")) return `file://${encoded}`;
|
|
705
|
+
return `file:///${encoded}`;
|
|
706
|
+
}
|
|
707
|
+
function escapesRoot(posix) {
|
|
708
|
+
return posix === ".." || posix.startsWith("../");
|
|
709
|
+
}
|
|
710
|
+
function resolveFromRoot(root, rel) {
|
|
711
|
+
const parts = root.replace(/\/+$/, "").split("/");
|
|
712
|
+
for (const segment of rel.split("/")) {
|
|
713
|
+
if (segment === "" || segment === ".") continue;
|
|
714
|
+
if (segment === "..") {
|
|
715
|
+
if (parts.length > 1) parts.pop();
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
parts.push(segment);
|
|
719
|
+
}
|
|
720
|
+
const joined = parts.join("/");
|
|
721
|
+
return joined === "" ? "/" : joined;
|
|
722
|
+
}
|
|
723
|
+
function normalizeRoot(root) {
|
|
724
|
+
const posix = toPosix(root);
|
|
725
|
+
return posix.endsWith("/") ? posix : `${posix}/`;
|
|
726
|
+
}
|
|
727
|
+
function artifactLocation(file, root) {
|
|
728
|
+
const posix = toPosix(file);
|
|
729
|
+
if (!isAbsolutePath(posix)) {
|
|
730
|
+
if (escapesRoot(posix)) return { uri: fileUri(resolveFromRoot(root, posix)) };
|
|
731
|
+
return { uri: encodePath(posix), uriBaseId: URI_BASE_ID };
|
|
732
|
+
}
|
|
733
|
+
const rel = underRoot(posix, root);
|
|
734
|
+
if (rel !== null) return { uri: encodePath(rel), uriBaseId: URI_BASE_ID };
|
|
735
|
+
return { uri: fileUri(posix) };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/tracevals/reporters/ci.ts
|
|
739
|
+
function ciInputFromRun(report, opts = {}) {
|
|
740
|
+
return {
|
|
741
|
+
runs: [report],
|
|
742
|
+
traceErrors: [],
|
|
743
|
+
warnings: [],
|
|
744
|
+
root: normalizeRoot(opts.root ?? process.cwd())
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function ciInputFromBatch(batch, runs, opts = {}) {
|
|
748
|
+
const traceErrors = [];
|
|
749
|
+
for (const t of batch.traces) {
|
|
750
|
+
if (t.error !== void 0) {
|
|
751
|
+
traceErrors.push({ file: t.file, error: t.error, durationMs: t.durationMs });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
runs,
|
|
756
|
+
traceErrors,
|
|
757
|
+
warnings: batch.warnings,
|
|
758
|
+
root: normalizeRoot(opts.root ?? process.cwd())
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function displayPath(file, root) {
|
|
762
|
+
const posix = toPosix(file);
|
|
763
|
+
return underRoot(posix, root) ?? posix;
|
|
764
|
+
}
|
|
765
|
+
var ABSOLUTE = /^(?:[A-Za-z]:)?\//;
|
|
766
|
+
function locationPath(location, root) {
|
|
767
|
+
if (classifyRef(location.file).kind === "url") return location.file;
|
|
768
|
+
const posix = toPosix(location.file);
|
|
769
|
+
const abs = ABSOLUTE.test(posix) ? posix : `${normalizeRoot(process.cwd())}${posix}`;
|
|
770
|
+
return underRoot(abs, root) ?? abs;
|
|
771
|
+
}
|
|
772
|
+
function locationLabel(location) {
|
|
773
|
+
return location.line === void 0 ? location.file : `${location.file}:${String(location.line)}`;
|
|
774
|
+
}
|
|
775
|
+
function ruleIdFor(grader) {
|
|
776
|
+
return `tracevals/${grader}`;
|
|
777
|
+
}
|
|
778
|
+
var TRACE_RULE_ID = "tracevals/trace";
|
|
779
|
+
function traceErrorText(error) {
|
|
780
|
+
return `trace could not be evaluated: ${error}`;
|
|
781
|
+
}
|
|
782
|
+
function judgeFailText(result) {
|
|
783
|
+
if (result.outcome !== "fail" || !result.consensus) return void 0;
|
|
784
|
+
const reasoning = result.consensus.runs.find((run) => run.verdict)?.verdict?.reasoning ?? "";
|
|
785
|
+
return `AI judge: fail (confidence ${result.consensus.meanConfidence.toFixed(2)}). ${reasoning}`.trim();
|
|
786
|
+
}
|
|
787
|
+
function errorText(result) {
|
|
788
|
+
return result.error ?? result.skipReason ?? "eval errored";
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/tracevals/reporters/github.ts
|
|
792
|
+
var TITLE = "manni tracevals";
|
|
793
|
+
function annotation(level, message, props) {
|
|
794
|
+
const parts = [
|
|
795
|
+
props.file !== void 0 ? `file=${escapeWorkflowCommandProperty(props.file)}` : void 0,
|
|
796
|
+
props.line !== void 0 ? `line=${String(props.line)}` : void 0,
|
|
797
|
+
`title=${escapeWorkflowCommandProperty(props.title)}`
|
|
798
|
+
].filter((p) => p !== void 0);
|
|
799
|
+
return `::${level} ${parts.join(",")}::${escapeWorkflowCommandMessage(message)}`;
|
|
800
|
+
}
|
|
801
|
+
function renderGithub(input, summary) {
|
|
802
|
+
const lines = [];
|
|
803
|
+
for (const run of input.runs) {
|
|
804
|
+
const trace = ` (trace: ${displayPath(run.trace.file, input.root)})`;
|
|
805
|
+
for (const r of run.evalResults) {
|
|
806
|
+
const at = {
|
|
807
|
+
file: locationPath(r.location, input.root),
|
|
808
|
+
...r.location.line === void 0 ? {} : { line: r.location.line },
|
|
809
|
+
title: `${TITLE}: ${r.evalName}`
|
|
810
|
+
};
|
|
811
|
+
for (const f of r.findings ?? []) {
|
|
812
|
+
lines.push(annotation(f.severity, f.message + trace, at));
|
|
813
|
+
}
|
|
814
|
+
const judged = judgeFailText(r);
|
|
815
|
+
if (judged !== void 0) {
|
|
816
|
+
lines.push(annotation("error", judged + trace, at));
|
|
817
|
+
}
|
|
818
|
+
if (r.outcome === "error") {
|
|
819
|
+
lines.push(annotation("error", errorText(r) + trace, at));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
for (const w of run.warnings) {
|
|
823
|
+
if (input.warnings.includes(w)) continue;
|
|
824
|
+
lines.push(annotation("warning", w + trace, { title: TITLE }));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const t of input.traceErrors) {
|
|
828
|
+
lines.push(
|
|
829
|
+
annotation("error", traceErrorText(t.error), {
|
|
830
|
+
file: displayPath(t.file, input.root),
|
|
831
|
+
title: TITLE
|
|
832
|
+
})
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
for (const w of input.warnings) {
|
|
836
|
+
lines.push(annotation("warning", w, { title: TITLE }));
|
|
837
|
+
}
|
|
838
|
+
lines.push("", summary);
|
|
839
|
+
return lines.join("\n");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/tracevals/reporters/junit.ts
|
|
843
|
+
function attr(name, value) {
|
|
844
|
+
return `${name}="${xmlEscape(String(value))}"`;
|
|
845
|
+
}
|
|
846
|
+
var seconds = (ms) => (ms / 1e3).toFixed(3);
|
|
847
|
+
function failureText(result) {
|
|
848
|
+
const findings = (result.findings ?? []).map((f) => f.message);
|
|
849
|
+
if (findings.length > 0) return findings.join("\n");
|
|
850
|
+
return judgeFailText(result) ?? `${result.evalName} failed`;
|
|
851
|
+
}
|
|
852
|
+
function testcase(result, root) {
|
|
853
|
+
const line = result.location.line;
|
|
854
|
+
const open = ` <testcase ${attr("classname", displayPath(result.artifact, root))} ${attr(
|
|
855
|
+
"name",
|
|
856
|
+
result.evalName
|
|
857
|
+
)} ${attr("file", locationPath(result.location, root))}${line === void 0 ? "" : ` ${attr("line", line)}`} ${attr("time", seconds(result.durationMs))}`;
|
|
858
|
+
const type = attr("type", ruleIdFor(result.grader));
|
|
859
|
+
switch (result.outcome) {
|
|
860
|
+
case "pass":
|
|
861
|
+
return `${open} />`;
|
|
862
|
+
case "skipped":
|
|
863
|
+
return `${open}>
|
|
864
|
+
<skipped ${attr(
|
|
865
|
+
"message",
|
|
866
|
+
result.skipReason ?? "skipped"
|
|
867
|
+
)} />
|
|
868
|
+
</testcase>`;
|
|
869
|
+
case "error":
|
|
870
|
+
return `${open}>
|
|
871
|
+
<error ${attr(
|
|
872
|
+
"message",
|
|
873
|
+
`${result.evalName} errored`
|
|
874
|
+
)} ${type}>${xmlEscape(errorText(result))}</error>
|
|
875
|
+
</testcase>`;
|
|
876
|
+
case "needs-review":
|
|
877
|
+
return `${open}>
|
|
878
|
+
<skipped ${attr(
|
|
879
|
+
"message",
|
|
880
|
+
"needs human review"
|
|
881
|
+
)} />
|
|
882
|
+
</testcase>`;
|
|
883
|
+
case "fail":
|
|
884
|
+
return `${open}>
|
|
885
|
+
<failure ${attr(
|
|
886
|
+
"message",
|
|
887
|
+
`${result.evalName} failed`
|
|
888
|
+
)} ${type}>${xmlEscape(failureText(result))}</failure>
|
|
889
|
+
</testcase>`;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function counts(run) {
|
|
893
|
+
const r = run.evalResults;
|
|
894
|
+
return {
|
|
895
|
+
tests: r.length,
|
|
896
|
+
failures: r.filter((x) => x.outcome === "fail").length,
|
|
897
|
+
errors: r.filter((x) => x.outcome === "error").length,
|
|
898
|
+
skipped: r.filter((x) => x.outcome === "skipped" || x.outcome === "needs-review").length
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function suiteOpen(name, c, ms) {
|
|
902
|
+
return ` <testsuite ${attr("name", name)} ${attr("tests", c.tests)} ${attr(
|
|
903
|
+
"failures",
|
|
904
|
+
c.failures
|
|
905
|
+
)} ${attr("errors", c.errors)} ${attr("skipped", c.skipped)} ${attr(
|
|
906
|
+
"time",
|
|
907
|
+
seconds(ms)
|
|
908
|
+
)}>`;
|
|
909
|
+
}
|
|
910
|
+
function renderJunit(input) {
|
|
911
|
+
const total = { tests: 0, failures: 0, errors: 0, skipped: 0 };
|
|
912
|
+
const suites = [];
|
|
913
|
+
for (const run of input.runs) {
|
|
914
|
+
const c = counts(run);
|
|
915
|
+
total.tests += c.tests;
|
|
916
|
+
total.failures += c.failures;
|
|
917
|
+
total.errors += c.errors;
|
|
918
|
+
suites.push(
|
|
919
|
+
[
|
|
920
|
+
suiteOpen(displayPath(run.trace.file, input.root), c, run.durationMs),
|
|
921
|
+
...run.evalResults.map((r) => testcase(r, input.root)),
|
|
922
|
+
" </testsuite>"
|
|
923
|
+
].join("\n")
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
for (const t of input.traceErrors) {
|
|
927
|
+
total.tests += 1;
|
|
928
|
+
total.errors += 1;
|
|
929
|
+
const name = displayPath(t.file, input.root);
|
|
930
|
+
suites.push(
|
|
931
|
+
[
|
|
932
|
+
suiteOpen(name, { tests: 1, failures: 0, errors: 1, skipped: 0 }, t.durationMs),
|
|
933
|
+
` <testcase ${attr("classname", name)} ${attr("name", "trace")} ${attr(
|
|
934
|
+
"time",
|
|
935
|
+
seconds(t.durationMs)
|
|
936
|
+
)}>`,
|
|
937
|
+
` <error ${attr("message", "trace could not be evaluated")} ${attr(
|
|
938
|
+
"type",
|
|
939
|
+
TRACE_RULE_ID
|
|
940
|
+
)}>${xmlEscape(t.error)}</error>`,
|
|
941
|
+
" </testcase>",
|
|
942
|
+
" </testsuite>"
|
|
943
|
+
].join("\n")
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return [
|
|
947
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
948
|
+
`<testsuites ${attr("name", "manni-tracevals")} ${attr("tests", total.tests)} ${attr(
|
|
949
|
+
"failures",
|
|
950
|
+
total.failures
|
|
951
|
+
)} ${attr("errors", total.errors)}>`,
|
|
952
|
+
...suites,
|
|
953
|
+
"</testsuites>",
|
|
954
|
+
""
|
|
955
|
+
].join("\n");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/tracevals/reporters/coverage.ts
|
|
959
|
+
function coverageLocation(entry) {
|
|
960
|
+
if (entry.resolved) return entry.path ?? entry.note ?? "";
|
|
961
|
+
if (entry.availability === "offered-not-used") {
|
|
962
|
+
return entry.note ?? "offered, never used";
|
|
963
|
+
}
|
|
964
|
+
return entry.note ?? `not found (${entry.tried.length} location(s) tried)`;
|
|
965
|
+
}
|
|
966
|
+
function availabilityTag(entry) {
|
|
967
|
+
if (entry.availability === "not-offered") return "not offered";
|
|
968
|
+
if (entry.availability === "offered-not-used") return "offered, unused";
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
971
|
+
function availabilityLines(report) {
|
|
972
|
+
if (!report.recorded) {
|
|
973
|
+
return [
|
|
974
|
+
"no availability roster in this trace, so what the session was offered is unknown"
|
|
975
|
+
];
|
|
976
|
+
}
|
|
977
|
+
const lines = [
|
|
978
|
+
`${report.skills.offered} skill(s) offered, ${report.skills.used} used, ${report.skills.unused} never used`,
|
|
979
|
+
`${report.agents.offered} agent(s) offered, ${report.agents.used} used, ${report.agents.unused} never used`
|
|
980
|
+
];
|
|
981
|
+
if (!report.listed && report.skills.unused + report.agents.unused > 0) {
|
|
982
|
+
lines.push("pass --report-unused-artifacts to list them");
|
|
983
|
+
}
|
|
984
|
+
return lines;
|
|
985
|
+
}
|
|
986
|
+
function coverageStaleness(entry) {
|
|
987
|
+
if (entry.stale !== true) return "";
|
|
988
|
+
if (entry.contentCheck?.status === "mismatch") {
|
|
989
|
+
return "changed since the session started (session manifest sha256)";
|
|
990
|
+
}
|
|
991
|
+
return entry.modifiedAt !== void 0 ? `modified after the session ended (${entry.modifiedAt})` : "modified after the session ended";
|
|
992
|
+
}
|
|
993
|
+
function manifestLines(report) {
|
|
994
|
+
const lines = [
|
|
995
|
+
`${report.path} captured ${report.capturedAt}${report.gitSha !== void 0 ? ` at ${report.gitSha.slice(0, 12)}` : ""}`,
|
|
996
|
+
`${report.matched} artifact(s) unchanged, ${report.changed} changed, ${report.unrecorded} not recorded`
|
|
997
|
+
];
|
|
998
|
+
if (report.unrecorded > 0) {
|
|
999
|
+
lines.push(
|
|
1000
|
+
"artifacts the manifest did not record keep the mtime heuristic"
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
return lines;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// src/tracevals/reporters/pretty.ts
|
|
1007
|
+
var OUTCOME_LABEL = {
|
|
1008
|
+
pass: "PASS",
|
|
1009
|
+
fail: "FAIL",
|
|
1010
|
+
error: "ERROR",
|
|
1011
|
+
"needs-review": "REVIEW",
|
|
1012
|
+
skipped: "SKIP"
|
|
1013
|
+
};
|
|
1014
|
+
function paintFor(pc, outcome) {
|
|
1015
|
+
switch (outcome) {
|
|
1016
|
+
case "pass":
|
|
1017
|
+
return pc.green;
|
|
1018
|
+
case "fail":
|
|
1019
|
+
case "error":
|
|
1020
|
+
return pc.red;
|
|
1021
|
+
case "needs-review":
|
|
1022
|
+
return pc.yellow;
|
|
1023
|
+
default:
|
|
1024
|
+
return pc.dim;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function coverageMark(pc, entry) {
|
|
1028
|
+
if (entry.resolved) return pc.green("\u2713");
|
|
1029
|
+
if (entry.availability === "offered-not-used") return pc.dim("\xB7");
|
|
1030
|
+
return pc.yellow("\u25CB");
|
|
1031
|
+
}
|
|
1032
|
+
function renderPretty(report, opts = {}) {
|
|
1033
|
+
const pc = palette(opts.color === true);
|
|
1034
|
+
const lines = [];
|
|
1035
|
+
const t = report.trace;
|
|
1036
|
+
lines.push(pc.bold(`manni tracevals \u2014 ${t.file}`));
|
|
1037
|
+
lines.push(
|
|
1038
|
+
pc.dim(
|
|
1039
|
+
`${t.source} \xB7 ${t.model ?? "unknown model"} \xB7 ${t.turnCount} turn(s) \xB7 ${t.cwd}`
|
|
1040
|
+
)
|
|
1041
|
+
);
|
|
1042
|
+
lines.push("");
|
|
1043
|
+
for (const result of report.evalResults) {
|
|
1044
|
+
const paint = paintFor(pc, result.outcome);
|
|
1045
|
+
const label = paint(OUTCOME_LABEL[result.outcome].padEnd(6));
|
|
1046
|
+
const name = `${result.artifactName} \u203A ${result.evalName}`;
|
|
1047
|
+
lines.push(
|
|
1048
|
+
` ${label} ${name}${result.implicit ? pc.dim(" (implicit)") : ""} ${pc.dim(locationLabel(result.location))}`
|
|
1049
|
+
);
|
|
1050
|
+
for (const finding2 of result.findings ?? []) {
|
|
1051
|
+
if (result.outcome !== "pass" || finding2.severity !== "error") {
|
|
1052
|
+
lines.push(` ${pc.dim(`[${finding2.severity}]`)} ${finding2.message}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (result.error) lines.push(` ${pc.red(result.error)}`);
|
|
1056
|
+
if (result.skipReason) lines.push(` ${pc.dim(result.skipReason)}`);
|
|
1057
|
+
if (result.consensus && result.outcome !== "pass") {
|
|
1058
|
+
const v = result.consensus.votes;
|
|
1059
|
+
lines.push(
|
|
1060
|
+
` ${pc.dim(`votes pass:${v.pass} fail:${v.fail} partial:${v.partial} error:${v.error} \xB7 confidence ${result.consensus.meanConfidence.toFixed(2)}`)}`
|
|
1061
|
+
);
|
|
1062
|
+
const observed = result.consensus.runs.find((r) => r.verdict)?.verdict?.observed;
|
|
1063
|
+
if (observed) lines.push(` ${pc.dim(observed)}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
lines.push("");
|
|
1067
|
+
lines.push(pc.bold("Artifact coverage"));
|
|
1068
|
+
for (const entry of report.coverage) {
|
|
1069
|
+
const tag = availabilityTag(entry);
|
|
1070
|
+
const location = coverageLocation(entry);
|
|
1071
|
+
const stale = coverageStaleness(entry);
|
|
1072
|
+
lines.push(
|
|
1073
|
+
[
|
|
1074
|
+
` ${coverageMark(pc, entry)} ${entry.kind}: ${entry.ref}`,
|
|
1075
|
+
tag === void 0 ? "" : pc.yellow(`[${tag}]`),
|
|
1076
|
+
location ? pc.dim(location) : "",
|
|
1077
|
+
stale ? pc.yellow(`\u26A0 ${stale}`) : ""
|
|
1078
|
+
].filter((part) => part !== "").join(" ")
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (report.manifest !== void 0) {
|
|
1082
|
+
lines.push("");
|
|
1083
|
+
lines.push(pc.bold("Session manifest"));
|
|
1084
|
+
for (const line of manifestLines(report.manifest)) {
|
|
1085
|
+
lines.push(` ${pc.dim(line)}`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
lines.push("");
|
|
1089
|
+
lines.push(pc.bold("Availability"));
|
|
1090
|
+
for (const line of availabilityLines(report.availability)) {
|
|
1091
|
+
lines.push(` ${pc.dim(line)}`);
|
|
1092
|
+
}
|
|
1093
|
+
if (report.warnings.length > 0) {
|
|
1094
|
+
lines.push("");
|
|
1095
|
+
lines.push(pc.bold("Warnings"));
|
|
1096
|
+
for (const warning of report.warnings) {
|
|
1097
|
+
lines.push(` ${pc.yellow("!")} ${warning}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const s = report.summary;
|
|
1101
|
+
lines.push("");
|
|
1102
|
+
lines.push(
|
|
1103
|
+
pc.bold(
|
|
1104
|
+
`${s.total} eval(s): ${pc.green(`${s.pass} pass`)}, ${s.fail > 0 ? pc.red(`${s.fail} fail`) : `${s.fail} fail`}, ${s.error} error, ${s.needsReview} needs-review, ${s.skipped} skipped`
|
|
1105
|
+
)
|
|
1106
|
+
);
|
|
1107
|
+
const weighted = report.evalResults.some(
|
|
1108
|
+
(r) => r.weight !== void 0 && r.weight !== 1
|
|
1109
|
+
);
|
|
1110
|
+
if (weighted) {
|
|
1111
|
+
lines.push(
|
|
1112
|
+
pc.dim(`weighted pass rate ${(s.passRate * 100).toFixed(0)}%`)
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (report.turns > 0) {
|
|
1116
|
+
lines.push(pc.dim(`judge turns ${report.turns}`));
|
|
1117
|
+
}
|
|
1118
|
+
return lines.join("\n");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/tracevals/reporters/markdown.ts
|
|
1122
|
+
var cell3 = (value) => value.replace(/\|/g, "\\|");
|
|
1123
|
+
function renderMarkdown(report) {
|
|
1124
|
+
const lines = [];
|
|
1125
|
+
const t = report.trace;
|
|
1126
|
+
lines.push(`# manni tracevals report`);
|
|
1127
|
+
lines.push("");
|
|
1128
|
+
lines.push(`- **Trace**: \`${t.file}\``);
|
|
1129
|
+
lines.push(`- **Source**: ${t.source} (${t.model ?? "unknown model"})`);
|
|
1130
|
+
lines.push(`- **Project**: \`${t.cwd}\``);
|
|
1131
|
+
lines.push(`- **Turns**: ${t.turnCount}`);
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
lines.push(`## Evals`);
|
|
1134
|
+
lines.push("");
|
|
1135
|
+
lines.push(`| Outcome | Artifact | Eval | Grader | Detail |`);
|
|
1136
|
+
lines.push(`|---|---|---|---|---|`);
|
|
1137
|
+
for (const r of report.evalResults) {
|
|
1138
|
+
const detail2 = r.error ?? r.skipReason ?? r.findings?.map((f) => f.message).join("; ") ?? (r.consensus ? `votes p:${r.consensus.votes.pass} f:${r.consensus.votes.fail} e:${r.consensus.votes.error}` : "");
|
|
1139
|
+
lines.push(
|
|
1140
|
+
`| ${r.outcome} | ${cell3(r.artifactName)} | ${cell3(r.evalName)}${r.implicit ? " (implicit)" : ""} \`${cell3(locationLabel(r.location))}\` | ${cell3(r.grader)} | ${cell3(detail2)} |`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
lines.push("");
|
|
1144
|
+
lines.push(`## Artifact coverage`);
|
|
1145
|
+
lines.push("");
|
|
1146
|
+
lines.push(`| Resolved | Kind | Ref | Availability | Where |`);
|
|
1147
|
+
lines.push(`|---|---|---|---|---|`);
|
|
1148
|
+
for (const entry of report.coverage) {
|
|
1149
|
+
const location = cell3(coverageLocation(entry));
|
|
1150
|
+
const where = entry.resolved && entry.path !== void 0 ? `\`${location}\`` : location;
|
|
1151
|
+
const resolved = entry.availability === "offered-not-used" ? "n/a" : entry.resolved ? "yes" : "no";
|
|
1152
|
+
const stale = coverageStaleness(entry);
|
|
1153
|
+
const cellText = stale ? `${where}${where ? " \u2014 " : ""}\u26A0 ${cell3(stale)}` : where;
|
|
1154
|
+
lines.push(
|
|
1155
|
+
`| ${resolved} | ${cell3(entry.kind)} | ${cell3(entry.ref)} | ${cell3(entry.availability ?? "")} | ${cellText} |`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
lines.push("");
|
|
1159
|
+
if (report.manifest !== void 0) {
|
|
1160
|
+
lines.push(`## Session manifest`);
|
|
1161
|
+
lines.push("");
|
|
1162
|
+
for (const line of manifestLines(report.manifest)) {
|
|
1163
|
+
lines.push(`- ${cell3(line)}`);
|
|
1164
|
+
}
|
|
1165
|
+
lines.push("");
|
|
1166
|
+
}
|
|
1167
|
+
lines.push(`## Availability`);
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
for (const line of availabilityLines(report.availability)) {
|
|
1170
|
+
lines.push(`- ${line}`);
|
|
1171
|
+
}
|
|
1172
|
+
lines.push("");
|
|
1173
|
+
if (report.warnings.length > 0) {
|
|
1174
|
+
lines.push(`## Warnings`);
|
|
1175
|
+
lines.push("");
|
|
1176
|
+
for (const warning of report.warnings) lines.push(`- ${warning}`);
|
|
1177
|
+
lines.push("");
|
|
1178
|
+
}
|
|
1179
|
+
const s = report.summary;
|
|
1180
|
+
lines.push(
|
|
1181
|
+
`**${s.total} eval(s)** \u2014 ${s.pass} pass, ${s.fail} fail, ${s.error} error, ${s.needsReview} needs-review, ${s.skipped} skipped.`
|
|
1182
|
+
);
|
|
1183
|
+
return lines.join("\n");
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/tracevals/reporters/sarif.ts
|
|
1187
|
+
var SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json";
|
|
1188
|
+
var LEVELS = {
|
|
1189
|
+
error: "error",
|
|
1190
|
+
warning: "warning",
|
|
1191
|
+
notice: "note"
|
|
1192
|
+
};
|
|
1193
|
+
function declaringLocation(r, root) {
|
|
1194
|
+
if (classifyRef(r.location.file).kind === "url") return { uri: r.location.file };
|
|
1195
|
+
return artifactLocation(locationPath(r.location, root), root);
|
|
1196
|
+
}
|
|
1197
|
+
function renderSarif(input) {
|
|
1198
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1199
|
+
const results = [];
|
|
1200
|
+
const declare = (id, name, text, level) => {
|
|
1201
|
+
if (!rules.has(id)) {
|
|
1202
|
+
rules.set(id, {
|
|
1203
|
+
id,
|
|
1204
|
+
name,
|
|
1205
|
+
shortDescription: { text },
|
|
1206
|
+
defaultConfiguration: { level }
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
const push = (run, r, level, text) => {
|
|
1211
|
+
const id = ruleIdFor(r.grader);
|
|
1212
|
+
declare(
|
|
1213
|
+
id,
|
|
1214
|
+
r.grader,
|
|
1215
|
+
r.grader === "ai" ? "manni tracevals AI judge" : `manni tracevals ${r.grader} grader`,
|
|
1216
|
+
level
|
|
1217
|
+
);
|
|
1218
|
+
results.push({
|
|
1219
|
+
ruleId: id,
|
|
1220
|
+
level,
|
|
1221
|
+
message: { text },
|
|
1222
|
+
locations: [
|
|
1223
|
+
{
|
|
1224
|
+
physicalLocation: {
|
|
1225
|
+
artifactLocation: declaringLocation(r, input.root),
|
|
1226
|
+
// SARIF regions are 1-based and need `startLine`, so a location
|
|
1227
|
+
// with no line has no region rather than an invented line 1.
|
|
1228
|
+
...r.location.line === void 0 ? {} : { region: { startLine: r.location.line } }
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
],
|
|
1232
|
+
properties: {
|
|
1233
|
+
eval: r.evalName,
|
|
1234
|
+
artifactName: r.artifactName,
|
|
1235
|
+
artifactType: r.artifactType,
|
|
1236
|
+
trace: displayPath(run.trace.file, input.root),
|
|
1237
|
+
...run.trace.sessionId !== void 0 ? { sessionId: run.trace.sessionId } : {}
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
};
|
|
1241
|
+
for (const run of input.runs) {
|
|
1242
|
+
for (const r of run.evalResults) {
|
|
1243
|
+
for (const f of r.findings ?? []) {
|
|
1244
|
+
push(run, r, LEVELS[f.severity], f.message);
|
|
1245
|
+
}
|
|
1246
|
+
const judged = judgeFailText(r);
|
|
1247
|
+
if (judged !== void 0) push(run, r, "error", judged);
|
|
1248
|
+
if (r.outcome === "error") push(run, r, "error", errorText(r));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
for (const t of input.traceErrors) {
|
|
1252
|
+
declare(TRACE_RULE_ID, "trace", "manni tracevals could not read the trace", "error");
|
|
1253
|
+
results.push({
|
|
1254
|
+
ruleId: TRACE_RULE_ID,
|
|
1255
|
+
level: "error",
|
|
1256
|
+
message: { text: traceErrorText(t.error) },
|
|
1257
|
+
locations: [
|
|
1258
|
+
{
|
|
1259
|
+
physicalLocation: {
|
|
1260
|
+
artifactLocation: artifactLocation(t.file, input.root)
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
]
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
const log = {
|
|
1267
|
+
$schema: SARIF_SCHEMA,
|
|
1268
|
+
version: "2.1.0",
|
|
1269
|
+
runs: [
|
|
1270
|
+
{
|
|
1271
|
+
tool: {
|
|
1272
|
+
driver: {
|
|
1273
|
+
name: "manni-tracevals",
|
|
1274
|
+
informationUri: "https://hawkeyexl.github.io/manni/tracevals/",
|
|
1275
|
+
rules: [...rules.values()]
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
originalUriBaseIds: { [URI_BASE_ID]: { uri: fileUri(input.root) } },
|
|
1279
|
+
results
|
|
1280
|
+
}
|
|
1281
|
+
]
|
|
1282
|
+
};
|
|
1283
|
+
return `${JSON.stringify(log, null, 2)}
|
|
1284
|
+
`;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/tracevals/reporters/format.ts
|
|
1288
|
+
var REPORT_FORMATS = [
|
|
1289
|
+
"pretty",
|
|
1290
|
+
"json",
|
|
1291
|
+
"markdown",
|
|
1292
|
+
"github",
|
|
1293
|
+
"sarif",
|
|
1294
|
+
"junit"
|
|
1295
|
+
];
|
|
1296
|
+
var CALIBRATE_FORMATS = ["pretty", "json", "markdown"];
|
|
1297
|
+
var SUMMARY_FORMATS = ["pretty", "json"];
|
|
1298
|
+
function parseFormat(value, allowed, flag) {
|
|
1299
|
+
if (!allowed.includes(value)) {
|
|
1300
|
+
throw new TracevalsError(
|
|
1301
|
+
`${flag} must be one of ${allowed.join(" | ")}, got "${value}"`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/tracevals/reporters/index.ts
|
|
1308
|
+
function render(report, format, opts = {}) {
|
|
1309
|
+
parseFormat(format, REPORT_FORMATS, "format");
|
|
1310
|
+
switch (format) {
|
|
1311
|
+
case "json":
|
|
1312
|
+
return JSON.stringify(report, null, 2);
|
|
1313
|
+
case "markdown":
|
|
1314
|
+
return renderMarkdown(report);
|
|
1315
|
+
case "pretty":
|
|
1316
|
+
return renderPretty(report, opts);
|
|
1317
|
+
case "github":
|
|
1318
|
+
return renderGithub(ciInputFromRun(report), renderMarkdown(report));
|
|
1319
|
+
case "sarif":
|
|
1320
|
+
return renderSarif(ciInputFromRun(report));
|
|
1321
|
+
case "junit":
|
|
1322
|
+
return renderJunit(ciInputFromRun(report));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function renderBatch(report, format, runs, opts = {}) {
|
|
1326
|
+
parseFormat(format, REPORT_FORMATS, "format");
|
|
1327
|
+
const ci = () => {
|
|
1328
|
+
if (runs === void 0) {
|
|
1329
|
+
throw new TracevalsError(
|
|
1330
|
+
`format "${format}" annotates findings, which live in the per-trace reports; pass them as renderBatch's third argument`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return ciInputFromBatch(report, runs);
|
|
1334
|
+
};
|
|
1335
|
+
switch (format) {
|
|
1336
|
+
case "json":
|
|
1337
|
+
return JSON.stringify(report, null, 2);
|
|
1338
|
+
case "markdown":
|
|
1339
|
+
return renderBatchMarkdown(report);
|
|
1340
|
+
case "pretty":
|
|
1341
|
+
return renderBatchPretty(report, opts);
|
|
1342
|
+
case "github":
|
|
1343
|
+
return renderGithub(ci(), renderBatchMarkdown(report));
|
|
1344
|
+
case "sarif":
|
|
1345
|
+
return renderSarif(ci());
|
|
1346
|
+
case "junit":
|
|
1347
|
+
return renderJunit(ci());
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function renderCalibration(report, format, opts = {}) {
|
|
1351
|
+
const narrowed = parseFormat(format, CALIBRATE_FORMATS, "format");
|
|
1352
|
+
switch (narrowed) {
|
|
1353
|
+
case "json":
|
|
1354
|
+
return JSON.stringify(report, null, 2);
|
|
1355
|
+
case "markdown":
|
|
1356
|
+
return renderCalibrationMarkdown(report);
|
|
1357
|
+
case "pretty":
|
|
1358
|
+
return renderCalibrationPretty(report, opts);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/tracevals/commands/run.ts
|
|
1363
|
+
import { writeFile } from "node:fs/promises";
|
|
1364
|
+
import { resolve as resolve5 } from "node:path";
|
|
1365
|
+
|
|
1366
|
+
// src/tracevals/trace/claude.ts
|
|
1367
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1368
|
+
import { basename as basename3, dirname, join, resolve } from "node:path";
|
|
1369
|
+
|
|
1370
|
+
// src/tracevals/trace/detect.ts
|
|
1371
|
+
var SUPPORTED = "supported formats: Claude Code session files (~/.claude/projects/**.jsonl) and claude -p stream-json";
|
|
1372
|
+
function detectFormat(line) {
|
|
1373
|
+
let rec;
|
|
1374
|
+
try {
|
|
1375
|
+
rec = JSON.parse(line);
|
|
1376
|
+
} catch {
|
|
1377
|
+
throw new TracevalsError(`trace is not JSONL (${SUPPORTED})`);
|
|
1378
|
+
}
|
|
1379
|
+
if (typeof rec !== "object" || rec === null) {
|
|
1380
|
+
throw new TracevalsError(`unrecognized trace format (${SUPPORTED})`);
|
|
1381
|
+
}
|
|
1382
|
+
if ("parentUuid" in rec || "sessionId" in rec) return "claude-session";
|
|
1383
|
+
if ("session_id" in rec) return "claude-stream";
|
|
1384
|
+
throw new TracevalsError(`unrecognized trace format (${SUPPORTED})`);
|
|
1385
|
+
}
|
|
1386
|
+
function detectContentFormat(content) {
|
|
1387
|
+
let sawParseable = false;
|
|
1388
|
+
for (const line of content.split("\n")) {
|
|
1389
|
+
const trimmed = line.trim();
|
|
1390
|
+
if (!trimmed) continue;
|
|
1391
|
+
let rec;
|
|
1392
|
+
try {
|
|
1393
|
+
rec = JSON.parse(trimmed);
|
|
1394
|
+
} catch {
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (typeof rec !== "object" || rec === null) continue;
|
|
1398
|
+
sawParseable = true;
|
|
1399
|
+
const r = rec;
|
|
1400
|
+
if ("parentUuid" in r || "sessionId" in r) return "claude-session";
|
|
1401
|
+
if ("session_id" in r) return "claude-stream";
|
|
1402
|
+
}
|
|
1403
|
+
throw new TracevalsError(
|
|
1404
|
+
sawParseable ? `unrecognized trace format (${SUPPORTED})` : `empty or unparseable trace file (${SUPPORTED})`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/tracevals/trace/claude.ts
|
|
1409
|
+
async function parseTraceFile(filePath) {
|
|
1410
|
+
const absolute = resolve(filePath);
|
|
1411
|
+
const content = await readFile(absolute, "utf-8");
|
|
1412
|
+
const trace = parseTraceContent(content, absolute);
|
|
1413
|
+
await mergeSidecarBranches(trace, absolute);
|
|
1414
|
+
return trace;
|
|
1415
|
+
}
|
|
1416
|
+
function parseTraceContent(content, filePath) {
|
|
1417
|
+
const format = detectContentFormat(content);
|
|
1418
|
+
const { records, unparseable } = parseLines(content);
|
|
1419
|
+
const trace = format === "claude-session" ? parseSession(records, filePath) : parseStream(records, filePath);
|
|
1420
|
+
if (unparseable > 0) {
|
|
1421
|
+
trace.warnings.push(
|
|
1422
|
+
`${unparseable} unparseable JSONL line(s) were skipped`
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
return trace;
|
|
1426
|
+
}
|
|
1427
|
+
function parseLines(content) {
|
|
1428
|
+
const records = [];
|
|
1429
|
+
let unparseable = 0;
|
|
1430
|
+
for (const line of content.split("\n")) {
|
|
1431
|
+
const trimmed = line.trim();
|
|
1432
|
+
if (!trimmed) continue;
|
|
1433
|
+
try {
|
|
1434
|
+
const rec = JSON.parse(trimmed);
|
|
1435
|
+
if (typeof rec === "object" && rec !== null) records.push(rec);
|
|
1436
|
+
else unparseable += 1;
|
|
1437
|
+
} catch {
|
|
1438
|
+
unparseable += 1;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return { records, unparseable };
|
|
1442
|
+
}
|
|
1443
|
+
var SESSION_META_TYPES = /* @__PURE__ */ new Set([
|
|
1444
|
+
"queue-operation",
|
|
1445
|
+
"attachment",
|
|
1446
|
+
"last-prompt",
|
|
1447
|
+
"custom-title",
|
|
1448
|
+
"summary",
|
|
1449
|
+
"mode",
|
|
1450
|
+
"pr-link"
|
|
1451
|
+
]);
|
|
1452
|
+
var COMMAND_NAME_RE = /<command-name>([^<]+)<\/command-name>/;
|
|
1453
|
+
var COMMAND_ARGS_RE = /<command-args>([^<]*)<\/command-args>/;
|
|
1454
|
+
function newTrace(filePath) {
|
|
1455
|
+
return {
|
|
1456
|
+
source: "claude-code",
|
|
1457
|
+
file: filePath,
|
|
1458
|
+
cwd: "",
|
|
1459
|
+
events: [],
|
|
1460
|
+
toolCalls: [],
|
|
1461
|
+
skillInvocations: [],
|
|
1462
|
+
agentSpawns: [],
|
|
1463
|
+
subagentBranches: [],
|
|
1464
|
+
availability: newAvailability(),
|
|
1465
|
+
fileAccesses: [],
|
|
1466
|
+
userMessages: [],
|
|
1467
|
+
assistantTexts: [],
|
|
1468
|
+
turnCount: 0,
|
|
1469
|
+
warnings: []
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function pushEvent(trace, event) {
|
|
1473
|
+
const index = trace.events.length;
|
|
1474
|
+
trace.events.push({ ...event, index });
|
|
1475
|
+
return index;
|
|
1476
|
+
}
|
|
1477
|
+
function plainMessageText(rec) {
|
|
1478
|
+
const content = rec.message?.content;
|
|
1479
|
+
return typeof content === "string" ? content : void 0;
|
|
1480
|
+
}
|
|
1481
|
+
function resolveBranch(branches, parentUuid, rootText) {
|
|
1482
|
+
if (typeof parentUuid !== "string") return void 0;
|
|
1483
|
+
const inherited = branches.resolved.get(parentUuid);
|
|
1484
|
+
if (inherited !== void 0) return inherited;
|
|
1485
|
+
const pending = branches.spawns.get(parentUuid);
|
|
1486
|
+
if (pending === void 0 || pending.length === 0) return void 0;
|
|
1487
|
+
const byPrompt = rootText !== void 0 ? pending.find((b) => !b.claimed && b.prompt === rootText) : void 0;
|
|
1488
|
+
const branch = byPrompt ?? pending.find((b) => !b.claimed) ?? pending[0];
|
|
1489
|
+
if (branch === void 0) return void 0;
|
|
1490
|
+
branch.claimed = true;
|
|
1491
|
+
return branch.toolUseId;
|
|
1492
|
+
}
|
|
1493
|
+
function parseSession(records, filePath) {
|
|
1494
|
+
const trace = newTrace(filePath);
|
|
1495
|
+
const branches = { spawns: /* @__PURE__ */ new Map(), resolved: /* @__PURE__ */ new Map() };
|
|
1496
|
+
const availability = newReplay();
|
|
1497
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
1498
|
+
for (const rec of records) {
|
|
1499
|
+
if (typeof rec.uuid === "string") uuids.add(rec.uuid);
|
|
1500
|
+
}
|
|
1501
|
+
const continuations = [];
|
|
1502
|
+
let sawMessage = false;
|
|
1503
|
+
let compactions = 0;
|
|
1504
|
+
let inputTokens = 0;
|
|
1505
|
+
let outputTokens = 0;
|
|
1506
|
+
let sawUsage = false;
|
|
1507
|
+
for (const rec of records) {
|
|
1508
|
+
const type = rec.type;
|
|
1509
|
+
const timestamp = rec.timestamp;
|
|
1510
|
+
if (timestamp) {
|
|
1511
|
+
trace.startedAt ??= timestamp;
|
|
1512
|
+
trace.endedAt = timestamp;
|
|
1513
|
+
}
|
|
1514
|
+
if (rec.sessionId) trace.sessionId ??= rec.sessionId;
|
|
1515
|
+
if (rec.subtype === "compact_boundary") compactions += 1;
|
|
1516
|
+
if (type !== void 0 && SESSION_META_TYPES.has(type)) {
|
|
1517
|
+
if (type === "summary" && !sawMessage && typeof rec.leafUuid === "string" && !uuids.has(rec.leafUuid)) {
|
|
1518
|
+
continuations.push(rec.leafUuid);
|
|
1519
|
+
}
|
|
1520
|
+
const index = pushEvent(trace, { kind: "meta", timestamp, raw: rec });
|
|
1521
|
+
if (type === "attachment" && applyAvailabilityRecord(availability, rec.attachment, index)) {
|
|
1522
|
+
availability.roster.recorded = true;
|
|
1523
|
+
}
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
const sidechain = rec.isSidechain === true;
|
|
1527
|
+
if (!sidechain && typeof rec.cwd === "string") trace.cwd = rec.cwd;
|
|
1528
|
+
if (!sidechain && typeof rec.gitBranch === "string") {
|
|
1529
|
+
trace.gitBranch = rec.gitBranch;
|
|
1530
|
+
}
|
|
1531
|
+
const context = { sidechain, timestamp, branches };
|
|
1532
|
+
if (typeof rec.uuid === "string") context.uuid = rec.uuid;
|
|
1533
|
+
if (sidechain) {
|
|
1534
|
+
const branchId = resolveBranch(
|
|
1535
|
+
branches,
|
|
1536
|
+
rec.parentUuid,
|
|
1537
|
+
plainMessageText(rec)
|
|
1538
|
+
);
|
|
1539
|
+
if (branchId !== void 0) {
|
|
1540
|
+
context.branchId = branchId;
|
|
1541
|
+
if (context.uuid) branches.resolved.set(context.uuid, branchId);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const message = rec.message;
|
|
1545
|
+
if (type === "user" && message) {
|
|
1546
|
+
sawMessage = true;
|
|
1547
|
+
handleUserMessage(trace, message, context, rec);
|
|
1548
|
+
} else if (type === "assistant" && message) {
|
|
1549
|
+
sawMessage = true;
|
|
1550
|
+
const usage = handleAssistantMessage(trace, message, context, rec);
|
|
1551
|
+
if (usage && !sidechain) {
|
|
1552
|
+
inputTokens += usage.input;
|
|
1553
|
+
outputTokens += usage.output;
|
|
1554
|
+
sawUsage = true;
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
pushEvent(trace, { kind: "system", timestamp, raw: rec });
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (sawUsage) trace.usage = { inputTokens, outputTokens };
|
|
1561
|
+
for (const leaf of continuations) {
|
|
1562
|
+
trace.warnings.push(
|
|
1563
|
+
`this trace is a fragment of a longer session: it resumes an earlier one recorded elsewhere (summary leafUuid ${leaf} is not in this file), so evals see only the turns recorded here`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (compactions > 0) {
|
|
1567
|
+
trace.warnings.push(
|
|
1568
|
+
`the conversation was compacted ${compactions} time(s): the agent stopped seeing the turns before each boundary, so early transcript content is not what it was working from`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
trace.availability = availability.roster;
|
|
1572
|
+
trace.subagentBranches = buildBranches(trace);
|
|
1573
|
+
return trace;
|
|
1574
|
+
}
|
|
1575
|
+
function buildBranches(trace, sidecars = /* @__PURE__ */ new Map()) {
|
|
1576
|
+
const ranges = /* @__PURE__ */ new Map();
|
|
1577
|
+
for (const event of trace.events) {
|
|
1578
|
+
const id = event.branchId;
|
|
1579
|
+
if (id === void 0) continue;
|
|
1580
|
+
const range = ranges.get(id);
|
|
1581
|
+
if (range === void 0) ranges.set(id, { min: event.index, max: event.index });
|
|
1582
|
+
else range.max = Math.max(range.max, event.index);
|
|
1583
|
+
}
|
|
1584
|
+
if (ranges.size === 0) return [];
|
|
1585
|
+
const spawnFor = /* @__PURE__ */ new Map();
|
|
1586
|
+
for (const spawn of trace.agentSpawns) {
|
|
1587
|
+
if (spawn.toolUseId) spawnFor.set(spawn.toolUseId, spawn);
|
|
1588
|
+
}
|
|
1589
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
1590
|
+
for (const id of ranges.keys()) {
|
|
1591
|
+
const spawn = spawnFor.get(id);
|
|
1592
|
+
parentOf.set(
|
|
1593
|
+
id,
|
|
1594
|
+
spawn ? trace.events[spawn.index]?.branchId : void 0
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
const depthOf = (id) => {
|
|
1598
|
+
let depth = 1;
|
|
1599
|
+
let cursor = parentOf.get(id);
|
|
1600
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
1601
|
+
while (cursor !== void 0 && !seen.has(cursor)) {
|
|
1602
|
+
seen.add(cursor);
|
|
1603
|
+
depth += 1;
|
|
1604
|
+
cursor = parentOf.get(cursor);
|
|
1605
|
+
}
|
|
1606
|
+
return depth;
|
|
1607
|
+
};
|
|
1608
|
+
const branches = [];
|
|
1609
|
+
for (const [id, range] of ranges) {
|
|
1610
|
+
const spawn = spawnFor.get(id);
|
|
1611
|
+
const sidecar = sidecars.get(id);
|
|
1612
|
+
const branch = {
|
|
1613
|
+
branchId: id,
|
|
1614
|
+
agentType: sidecar?.agentType ?? spawn?.subagentType ?? id,
|
|
1615
|
+
origin: sidecar ? "sidecar" : "inline",
|
|
1616
|
+
spawnDepth: sidecar?.spawnDepth ?? depthOf(id),
|
|
1617
|
+
spawnIndex: spawn?.index ?? range.min,
|
|
1618
|
+
startIndex: range.min,
|
|
1619
|
+
endIndex: range.max + 1
|
|
1620
|
+
};
|
|
1621
|
+
const description = sidecar?.description ?? spawn?.description;
|
|
1622
|
+
if (description !== void 0) branch.description = description;
|
|
1623
|
+
if (sidecar?.agentId) branch.agentId = sidecar.agentId;
|
|
1624
|
+
if (sidecar?.parentAgentId) branch.parentAgentId = sidecar.parentAgentId;
|
|
1625
|
+
if (sidecar?.file) branch.file = sidecar.file;
|
|
1626
|
+
branches.push(branch);
|
|
1627
|
+
}
|
|
1628
|
+
branches.sort((a, b) => b.spawnDepth - a.spawnDepth);
|
|
1629
|
+
const byId = new Map(branches.map((b) => [b.branchId, b]));
|
|
1630
|
+
for (const branch of branches) {
|
|
1631
|
+
const parentId = parentOf.get(branch.branchId);
|
|
1632
|
+
const parent = parentId === void 0 ? void 0 : byId.get(parentId);
|
|
1633
|
+
if (parent === void 0) continue;
|
|
1634
|
+
parent.startIndex = Math.min(parent.startIndex, branch.startIndex);
|
|
1635
|
+
parent.endIndex = Math.max(parent.endIndex, branch.endIndex);
|
|
1636
|
+
}
|
|
1637
|
+
branches.sort((a, b) => a.startIndex - b.startIndex);
|
|
1638
|
+
return branches;
|
|
1639
|
+
}
|
|
1640
|
+
function handleUserMessage(trace, message, context, raw) {
|
|
1641
|
+
const { sidechain, timestamp, branchId } = context;
|
|
1642
|
+
const content = message.content;
|
|
1643
|
+
if (typeof content === "string") {
|
|
1644
|
+
const index = pushEvent(trace, {
|
|
1645
|
+
kind: "user",
|
|
1646
|
+
timestamp,
|
|
1647
|
+
text: content,
|
|
1648
|
+
sidechain,
|
|
1649
|
+
branchId,
|
|
1650
|
+
raw
|
|
1651
|
+
});
|
|
1652
|
+
if (sidechain) return;
|
|
1653
|
+
const command = COMMAND_NAME_RE.exec(content);
|
|
1654
|
+
if (command?.[1]) {
|
|
1655
|
+
const invocation = {
|
|
1656
|
+
name: command[1].replace(/^\//, ""),
|
|
1657
|
+
via: "command-injection",
|
|
1658
|
+
index
|
|
1659
|
+
};
|
|
1660
|
+
const args = COMMAND_ARGS_RE.exec(content)?.[1]?.trim();
|
|
1661
|
+
if (args) invocation.args = args;
|
|
1662
|
+
trace.skillInvocations.push(invocation);
|
|
1663
|
+
}
|
|
1664
|
+
trace.userMessages.push(content);
|
|
1665
|
+
trace.turnCount += 1;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (Array.isArray(content)) {
|
|
1669
|
+
let hasToolResult = false;
|
|
1670
|
+
const texts = [];
|
|
1671
|
+
for (const block of content) {
|
|
1672
|
+
if (block?.type === "tool_result") hasToolResult = true;
|
|
1673
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
1674
|
+
texts.push(block.text);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
if (hasToolResult) {
|
|
1678
|
+
pushEvent(trace, {
|
|
1679
|
+
kind: "tool_result",
|
|
1680
|
+
timestamp,
|
|
1681
|
+
sidechain,
|
|
1682
|
+
branchId,
|
|
1683
|
+
raw
|
|
1684
|
+
});
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const text = texts.join("\n");
|
|
1688
|
+
pushEvent(trace, {
|
|
1689
|
+
kind: "user",
|
|
1690
|
+
timestamp,
|
|
1691
|
+
text,
|
|
1692
|
+
sidechain,
|
|
1693
|
+
branchId,
|
|
1694
|
+
raw
|
|
1695
|
+
});
|
|
1696
|
+
if (!sidechain && text) {
|
|
1697
|
+
trace.userMessages.push(text);
|
|
1698
|
+
trace.turnCount += 1;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function handleAssistantMessage(trace, message, context, raw) {
|
|
1703
|
+
const { sidechain, timestamp, branchId } = context;
|
|
1704
|
+
if (!sidechain && typeof message.model === "string") {
|
|
1705
|
+
trace.model ??= message.model;
|
|
1706
|
+
}
|
|
1707
|
+
const content = message.content;
|
|
1708
|
+
if (Array.isArray(content)) {
|
|
1709
|
+
for (const block of content) {
|
|
1710
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
1711
|
+
pushEvent(trace, {
|
|
1712
|
+
kind: "assistant",
|
|
1713
|
+
timestamp,
|
|
1714
|
+
text: block.text,
|
|
1715
|
+
sidechain,
|
|
1716
|
+
branchId,
|
|
1717
|
+
raw
|
|
1718
|
+
});
|
|
1719
|
+
if (!sidechain) trace.assistantTexts.push(block.text);
|
|
1720
|
+
} else if (block?.type === "tool_use") {
|
|
1721
|
+
extractToolUse(trace, block, context);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const usage = message.usage;
|
|
1726
|
+
if (usage) {
|
|
1727
|
+
return {
|
|
1728
|
+
input: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
|
|
1729
|
+
output: typeof usage.output_tokens === "number" ? usage.output_tokens : 0
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
return void 0;
|
|
1733
|
+
}
|
|
1734
|
+
function extractToolUse(trace, block, context) {
|
|
1735
|
+
const { sidechain, timestamp, branchId } = context;
|
|
1736
|
+
const name = block.name;
|
|
1737
|
+
const input = block.input ?? {};
|
|
1738
|
+
const toolUseId = typeof block.id === "string" ? block.id : void 0;
|
|
1739
|
+
const index = pushEvent(trace, {
|
|
1740
|
+
kind: "tool_call",
|
|
1741
|
+
timestamp,
|
|
1742
|
+
toolName: name,
|
|
1743
|
+
sidechain,
|
|
1744
|
+
branchId,
|
|
1745
|
+
raw: block
|
|
1746
|
+
});
|
|
1747
|
+
const call = { name, input, index, sidechain };
|
|
1748
|
+
if (timestamp) call.timestamp = timestamp;
|
|
1749
|
+
if (branchId) call.branchId = branchId;
|
|
1750
|
+
trace.toolCalls.push(call);
|
|
1751
|
+
switch (name) {
|
|
1752
|
+
case "Skill": {
|
|
1753
|
+
if (typeof input.skill === "string") {
|
|
1754
|
+
const invocation = {
|
|
1755
|
+
name: input.skill,
|
|
1756
|
+
via: "skill-tool",
|
|
1757
|
+
index
|
|
1758
|
+
};
|
|
1759
|
+
if (toolUseId) invocation.toolUseId = toolUseId;
|
|
1760
|
+
if (typeof input.args === "string" && input.args) {
|
|
1761
|
+
invocation.args = input.args;
|
|
1762
|
+
}
|
|
1763
|
+
trace.skillInvocations.push(invocation);
|
|
1764
|
+
}
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
case "Agent": {
|
|
1768
|
+
if (typeof input.subagent_type === "string") {
|
|
1769
|
+
const spawn = {
|
|
1770
|
+
subagentType: input.subagent_type,
|
|
1771
|
+
index
|
|
1772
|
+
};
|
|
1773
|
+
if (typeof input.description === "string") {
|
|
1774
|
+
spawn.description = input.description;
|
|
1775
|
+
}
|
|
1776
|
+
if (toolUseId) spawn.toolUseId = toolUseId;
|
|
1777
|
+
trace.agentSpawns.push(spawn);
|
|
1778
|
+
}
|
|
1779
|
+
if (context.branches && context.uuid && toolUseId) {
|
|
1780
|
+
const pending = context.branches.spawns.get(context.uuid) ?? [];
|
|
1781
|
+
pending.push({
|
|
1782
|
+
toolUseId,
|
|
1783
|
+
prompt: typeof input.prompt === "string" ? input.prompt : void 0,
|
|
1784
|
+
claimed: false
|
|
1785
|
+
});
|
|
1786
|
+
context.branches.spawns.set(context.uuid, pending);
|
|
1787
|
+
}
|
|
1788
|
+
break;
|
|
1789
|
+
}
|
|
1790
|
+
case "Read":
|
|
1791
|
+
case "Write":
|
|
1792
|
+
case "Edit": {
|
|
1793
|
+
if (typeof input.file_path === "string") {
|
|
1794
|
+
const op = name === "Read" ? "read" : name === "Write" ? "write" : "edit";
|
|
1795
|
+
trace.fileAccesses.push({ path: input.file_path, op, index });
|
|
1796
|
+
}
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function parseStream(records, filePath) {
|
|
1802
|
+
const trace = newTrace(filePath);
|
|
1803
|
+
const context = { sidechain: false };
|
|
1804
|
+
let inputTokens = 0;
|
|
1805
|
+
let outputTokens = 0;
|
|
1806
|
+
let sawMessageUsage = false;
|
|
1807
|
+
for (const rec of records) {
|
|
1808
|
+
const type = rec.type;
|
|
1809
|
+
if (type === "system" && (rec.subtype === "init" || !rec.subtype)) {
|
|
1810
|
+
if (typeof rec.cwd === "string") trace.cwd = rec.cwd;
|
|
1811
|
+
if (typeof rec.model === "string") trace.model = rec.model;
|
|
1812
|
+
if (typeof rec.session_id === "string") trace.sessionId = rec.session_id;
|
|
1813
|
+
pushEvent(trace, { kind: "system", raw: rec });
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
if (type === "assistant" || type === "user") {
|
|
1817
|
+
const message = rec.message ?? rec;
|
|
1818
|
+
if (type === "assistant") {
|
|
1819
|
+
const usage = handleAssistantMessage(trace, message, context, rec);
|
|
1820
|
+
if (usage) {
|
|
1821
|
+
inputTokens += usage.input;
|
|
1822
|
+
outputTokens += usage.output;
|
|
1823
|
+
sawMessageUsage = true;
|
|
1824
|
+
}
|
|
1825
|
+
} else {
|
|
1826
|
+
handleUserMessage(trace, message, context, rec);
|
|
1827
|
+
}
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (type === "result") {
|
|
1831
|
+
trace.isError = rec.is_error === true;
|
|
1832
|
+
if (typeof rec.num_turns === "number") trace.turnCount = rec.num_turns;
|
|
1833
|
+
const usage = rec.usage;
|
|
1834
|
+
const totalCost = typeof rec.total_cost_usd === "number" ? rec.total_cost_usd : void 0;
|
|
1835
|
+
if (usage) {
|
|
1836
|
+
trace.usage = {
|
|
1837
|
+
inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
|
|
1838
|
+
outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0
|
|
1839
|
+
};
|
|
1840
|
+
if (totalCost !== void 0) trace.usage.totalCostUsd = totalCost;
|
|
1841
|
+
} else if (sawMessageUsage) {
|
|
1842
|
+
trace.usage = { inputTokens, outputTokens };
|
|
1843
|
+
if (totalCost !== void 0) trace.usage.totalCostUsd = totalCost;
|
|
1844
|
+
}
|
|
1845
|
+
pushEvent(trace, { kind: "meta", raw: rec });
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
pushEvent(trace, { kind: "meta", raw: rec });
|
|
1849
|
+
}
|
|
1850
|
+
if (!trace.usage && sawMessageUsage) {
|
|
1851
|
+
trace.usage = { inputTokens, outputTokens };
|
|
1852
|
+
}
|
|
1853
|
+
return trace;
|
|
1854
|
+
}
|
|
1855
|
+
var AGENT_META_RE = /^agent-(.+)\.meta\.json$/;
|
|
1856
|
+
var AGENT_JSONL_RE = /^agent-(.+)\.jsonl$/;
|
|
1857
|
+
function sidecarDirFor(traceFile) {
|
|
1858
|
+
const stem = basename3(traceFile).replace(/\.jsonl$/i, "");
|
|
1859
|
+
return join(dirname(traceFile), stem, "subagents");
|
|
1860
|
+
}
|
|
1861
|
+
function asSource(id, trace) {
|
|
1862
|
+
return {
|
|
1863
|
+
id,
|
|
1864
|
+
events: trace.events,
|
|
1865
|
+
toolCalls: trace.toolCalls,
|
|
1866
|
+
fileAccesses: trace.fileAccesses,
|
|
1867
|
+
skillInvocations: trace.skillInvocations,
|
|
1868
|
+
agentSpawns: trace.agentSpawns
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function stampBranch(sub, branchId) {
|
|
1872
|
+
for (const event of sub.events) {
|
|
1873
|
+
event.sidechain = true;
|
|
1874
|
+
event.branchId = branchId;
|
|
1875
|
+
}
|
|
1876
|
+
for (const call of sub.toolCalls) {
|
|
1877
|
+
call.sidechain = true;
|
|
1878
|
+
call.branchId = branchId;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async function loadSidecars(dir, trace) {
|
|
1882
|
+
let names2;
|
|
1883
|
+
try {
|
|
1884
|
+
names2 = await readdir(dir);
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
const code = error.code;
|
|
1887
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
1888
|
+
trace.warnings.push(
|
|
1889
|
+
`could not read the subagent transcript directory ${dir} (${code ?? "unknown error"}), so subagent turns are missing here`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
return [];
|
|
1893
|
+
}
|
|
1894
|
+
const metas = /* @__PURE__ */ new Set();
|
|
1895
|
+
const transcripts = /* @__PURE__ */ new Set();
|
|
1896
|
+
for (const name of names2) {
|
|
1897
|
+
const meta = AGENT_META_RE.exec(name);
|
|
1898
|
+
if (meta?.[1] !== void 0) {
|
|
1899
|
+
metas.add(meta[1]);
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
const jsonl = AGENT_JSONL_RE.exec(name);
|
|
1903
|
+
if (jsonl?.[1] !== void 0) transcripts.add(jsonl[1]);
|
|
1904
|
+
}
|
|
1905
|
+
for (const orphan of [...transcripts].sort()) {
|
|
1906
|
+
if (!metas.has(orphan)) {
|
|
1907
|
+
trace.warnings.push(
|
|
1908
|
+
`subagent transcript agent-${orphan}.jsonl has no agent-${orphan}.meta.json, so it names no Agent spawn and was not merged`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
const loaded = [];
|
|
1913
|
+
for (const agentId of [...metas].sort()) {
|
|
1914
|
+
let meta;
|
|
1915
|
+
try {
|
|
1916
|
+
const raw = await readFile(
|
|
1917
|
+
join(dir, `agent-${agentId}.meta.json`),
|
|
1918
|
+
"utf-8"
|
|
1919
|
+
);
|
|
1920
|
+
const parsed = JSON.parse(raw);
|
|
1921
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1922
|
+
throw new Error("shape");
|
|
1923
|
+
}
|
|
1924
|
+
meta = parsed;
|
|
1925
|
+
} catch {
|
|
1926
|
+
trace.warnings.push(
|
|
1927
|
+
`subagent metadata agent-${agentId}.meta.json is unreadable or not an object, so that branch was not merged`
|
|
1928
|
+
);
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const file = join(dir, `agent-${agentId}.jsonl`);
|
|
1932
|
+
let sub;
|
|
1933
|
+
try {
|
|
1934
|
+
sub = parseTraceContent(await readFile(file, "utf-8"), file);
|
|
1935
|
+
} catch {
|
|
1936
|
+
trace.warnings.push(
|
|
1937
|
+
`subagent transcript agent-${agentId}.jsonl is missing or unreadable, so that branch was not merged`
|
|
1938
|
+
);
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
for (const warning of sub.warnings) {
|
|
1942
|
+
trace.warnings.push(`subagent ${meta.agentType ?? agentId}: ${warning}`);
|
|
1943
|
+
}
|
|
1944
|
+
loaded.push({ agentId, file, meta, sub, source: asSource(agentId, sub) });
|
|
1945
|
+
}
|
|
1946
|
+
return loaded;
|
|
1947
|
+
}
|
|
1948
|
+
async function mergeSidecarBranches(trace, traceFile) {
|
|
1949
|
+
const loaded = await loadSidecars(sidecarDirFor(traceFile), trace);
|
|
1950
|
+
if (loaded.length === 0) return;
|
|
1951
|
+
const base = asSource("", trace);
|
|
1952
|
+
const sources = /* @__PURE__ */ new Map([["", base]]);
|
|
1953
|
+
const spawnOwner = /* @__PURE__ */ new Map();
|
|
1954
|
+
const registerSpawns = (source) => {
|
|
1955
|
+
for (const spawn of source.agentSpawns) {
|
|
1956
|
+
if (spawn.toolUseId !== void 0) {
|
|
1957
|
+
spawnOwner.set(spawn.toolUseId, {
|
|
1958
|
+
sourceId: source.id,
|
|
1959
|
+
local: spawn.index
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
registerSpawns(base);
|
|
1965
|
+
const children = /* @__PURE__ */ new Map();
|
|
1966
|
+
const attached = [];
|
|
1967
|
+
const pending = [...loaded].sort(
|
|
1968
|
+
(a, b) => (a.meta.spawnDepth ?? 1) - (b.meta.spawnDepth ?? 1) || (a.agentId < b.agentId ? -1 : a.agentId > b.agentId ? 1 : 0)
|
|
1969
|
+
);
|
|
1970
|
+
let progress = true;
|
|
1971
|
+
while (progress) {
|
|
1972
|
+
progress = false;
|
|
1973
|
+
for (let i = 0; i < pending.length; i += 1) {
|
|
1974
|
+
const sidecar = pending[i];
|
|
1975
|
+
const branchId = sidecar.meta.toolUseId;
|
|
1976
|
+
const owner = branchId === void 0 ? void 0 : spawnOwner.get(branchId);
|
|
1977
|
+
if (branchId === void 0 || owner === void 0) continue;
|
|
1978
|
+
pending.splice(i, 1);
|
|
1979
|
+
i -= 1;
|
|
1980
|
+
progress = true;
|
|
1981
|
+
stampBranch(sidecar.sub, branchId);
|
|
1982
|
+
sources.set(sidecar.agentId, sidecar.source);
|
|
1983
|
+
const key = `${owner.sourceId}\0${owner.local}`;
|
|
1984
|
+
children.set(key, [...children.get(key) ?? [], sidecar]);
|
|
1985
|
+
registerSpawns(sidecar.source);
|
|
1986
|
+
attached.push({ sidecar, branchId });
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
for (const sidecar of pending) {
|
|
1990
|
+
trace.warnings.push(
|
|
1991
|
+
`subagent transcript agent-${sidecar.agentId}.jsonl names Agent call ${sidecar.meta.toolUseId ?? "(none recorded)"}, which is not in this trace, so that branch was not merged`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (attached.length === 0) return;
|
|
1995
|
+
const slots = [];
|
|
1996
|
+
const emit = (sourceId) => {
|
|
1997
|
+
const source = sources.get(sourceId);
|
|
1998
|
+
if (source === void 0) return;
|
|
1999
|
+
for (let local = 0; local < source.events.length; local += 1) {
|
|
2000
|
+
slots.push({ sourceId, local });
|
|
2001
|
+
for (const kid of children.get(`${sourceId}\0${local}`) ?? []) {
|
|
2002
|
+
emit(kid.agentId);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
emit("");
|
|
2007
|
+
const remap = /* @__PURE__ */ new Map();
|
|
2008
|
+
slots.forEach((slot, final) => {
|
|
2009
|
+
const row = remap.get(slot.sourceId) ?? [];
|
|
2010
|
+
row[slot.local] = final;
|
|
2011
|
+
remap.set(slot.sourceId, row);
|
|
2012
|
+
});
|
|
2013
|
+
const finalIndex = (sourceId, local) => remap.get(sourceId)?.[local] ?? local;
|
|
2014
|
+
trace.events = slots.map((slot, final) => {
|
|
2015
|
+
const event = sources.get(slot.sourceId)?.events[slot.local];
|
|
2016
|
+
return { ...event, index: final };
|
|
2017
|
+
});
|
|
2018
|
+
const gather = (pick) => [...sources.values()].flatMap(
|
|
2019
|
+
(source) => pick(source).map((item) => ({
|
|
2020
|
+
...item,
|
|
2021
|
+
index: finalIndex(source.id, item.index)
|
|
2022
|
+
}))
|
|
2023
|
+
).sort((a, b) => a.index - b.index);
|
|
2024
|
+
trace.toolCalls = gather((s) => s.toolCalls);
|
|
2025
|
+
trace.fileAccesses = gather((s) => s.fileAccesses);
|
|
2026
|
+
trace.skillInvocations = gather((s) => s.skillInvocations);
|
|
2027
|
+
trace.agentSpawns = gather((s) => s.agentSpawns);
|
|
2028
|
+
trace.availability = remapAvailability(
|
|
2029
|
+
trace.availability,
|
|
2030
|
+
(index) => finalIndex("", index)
|
|
2031
|
+
);
|
|
2032
|
+
for (const { sidecar, branchId } of attached) {
|
|
2033
|
+
const owner = spawnOwner.get(branchId);
|
|
2034
|
+
foldBranchAvailability(
|
|
2035
|
+
trace.availability,
|
|
2036
|
+
sidecar.sub.availability,
|
|
2037
|
+
branchId,
|
|
2038
|
+
owner === void 0 ? 0 : finalIndex(owner.sourceId, owner.local)
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
const described = /* @__PURE__ */ new Map();
|
|
2042
|
+
for (const { sidecar, branchId } of attached) {
|
|
2043
|
+
const info = {
|
|
2044
|
+
agentId: sidecar.agentId,
|
|
2045
|
+
file: sidecar.file
|
|
2046
|
+
};
|
|
2047
|
+
if (sidecar.meta.agentType) info.agentType = sidecar.meta.agentType;
|
|
2048
|
+
if (sidecar.meta.description) info.description = sidecar.meta.description;
|
|
2049
|
+
if (sidecar.meta.parentAgentId) {
|
|
2050
|
+
info.parentAgentId = sidecar.meta.parentAgentId;
|
|
2051
|
+
}
|
|
2052
|
+
if (typeof sidecar.meta.spawnDepth === "number") {
|
|
2053
|
+
info.spawnDepth = sidecar.meta.spawnDepth;
|
|
2054
|
+
}
|
|
2055
|
+
described.set(branchId, info);
|
|
2056
|
+
}
|
|
2057
|
+
trace.subagentBranches = buildBranches(trace, described);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// src/tracevals/core/plan.ts
|
|
2061
|
+
var IMPLICIT_EVAL_NAME = "adheres-to-artifact";
|
|
2062
|
+
function planEvals(artifacts, metadataFor) {
|
|
2063
|
+
const plans = [];
|
|
2064
|
+
for (const artifact of artifacts) {
|
|
2065
|
+
const supplied = metadataFor?.(artifact);
|
|
2066
|
+
const metadata = {
|
|
2067
|
+
extracted: supplied?.extracted ?? extractFrontmatter(artifact.content, "markdown"),
|
|
2068
|
+
...supplied?.locate === void 0 ? {} : { locate: supplied.locate }
|
|
2069
|
+
};
|
|
2070
|
+
const extracted = extractEvals(artifact, metadata.extracted);
|
|
2071
|
+
const at = (pointer) => declaredAt(artifact.path, pointer, metadata);
|
|
2072
|
+
const whole = { file: artifact.path };
|
|
2073
|
+
if (extracted.errors.length > 0) {
|
|
2074
|
+
const detail2 = extracted.errors.map(
|
|
2075
|
+
(e) => `${e.instancePath || "/"}${e.line !== void 0 ? ` (line ${e.line})` : ""}: ${e.message}`
|
|
2076
|
+
).join("; ");
|
|
2077
|
+
plans.push({
|
|
2078
|
+
artifact,
|
|
2079
|
+
evalName: "evals-block-valid",
|
|
2080
|
+
assertion: "The artifact's metadata.evals block matches the schema.",
|
|
2081
|
+
grader: "ai",
|
|
2082
|
+
severity: "error",
|
|
2083
|
+
implicit: false,
|
|
2084
|
+
location: at("/metadata/evals"),
|
|
2085
|
+
error: `invalid metadata.evals block: ${detail2}`
|
|
2086
|
+
});
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
if (extracted.skip) {
|
|
2090
|
+
plans.push({
|
|
2091
|
+
artifact,
|
|
2092
|
+
evalName: IMPLICIT_EVAL_NAME,
|
|
2093
|
+
assertion: "Artifact skipped via metadata.eval-skip.",
|
|
2094
|
+
grader: "ai",
|
|
2095
|
+
severity: "error",
|
|
2096
|
+
implicit: true,
|
|
2097
|
+
location: at("/metadata/eval-skip"),
|
|
2098
|
+
skipped: true,
|
|
2099
|
+
skipReason: "artifact skipped via metadata.eval-skip"
|
|
2100
|
+
});
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (extracted.evals.length === 0) {
|
|
2104
|
+
plans.push({
|
|
2105
|
+
artifact,
|
|
2106
|
+
evalName: IMPLICIT_EVAL_NAME,
|
|
2107
|
+
assertion: `The session adhered to the instructions in this ${artifact.type} ("${artifact.name}"). Cite the specific instructions followed or violated.`,
|
|
2108
|
+
grader: "ai",
|
|
2109
|
+
severity: "error",
|
|
2110
|
+
implicit: true,
|
|
2111
|
+
location: whole
|
|
2112
|
+
});
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
for (const [index, entry] of extracted.evals.entries()) {
|
|
2116
|
+
const plan = {
|
|
2117
|
+
artifact,
|
|
2118
|
+
evalName: entry.id,
|
|
2119
|
+
assertion: entry.assertion,
|
|
2120
|
+
grader: entry.grader,
|
|
2121
|
+
...entry.model !== void 0 ? { model: entry.model } : {},
|
|
2122
|
+
...entry.runs !== void 0 ? { runs: entry.runs } : {},
|
|
2123
|
+
...entry.target !== void 0 ? { target: entry.target } : {},
|
|
2124
|
+
...entry.weight !== void 0 ? { weight: entry.weight } : {},
|
|
2125
|
+
severity: entry.severity,
|
|
2126
|
+
implicit: false,
|
|
2127
|
+
// A string block is one entry at index 0, and the pointer walks up to
|
|
2128
|
+
// the block's own line.
|
|
2129
|
+
location: at(`/metadata/evals/${String(index)}`)
|
|
2130
|
+
};
|
|
2131
|
+
if (entry.options) plan.options = entry.options;
|
|
2132
|
+
if (entry.evidence) plan.evidence = entry.evidence;
|
|
2133
|
+
if (entry.examples) plan.examples = entry.examples;
|
|
2134
|
+
if (entry.provider) plan.provider = entry.provider;
|
|
2135
|
+
if (entry.command) plan.command = entry.command;
|
|
2136
|
+
if (entry.successExitCodes) plan.successExitCodes = entry.successExitCodes;
|
|
2137
|
+
if (entry.timeoutMs !== void 0) plan.timeoutMs = entry.timeoutMs;
|
|
2138
|
+
if (entry.generatedAssertionHash !== void 0) {
|
|
2139
|
+
plan.generatedAssertionHash = entry.generatedAssertionHash;
|
|
2140
|
+
}
|
|
2141
|
+
const proposedBy = extracted.proposedBy.get(entry.id);
|
|
2142
|
+
if (proposedBy !== void 0 && proposedBy.length > 0) {
|
|
2143
|
+
plan.proposedBy = proposedBy;
|
|
2144
|
+
}
|
|
2145
|
+
if (entry.skip) {
|
|
2146
|
+
plan.skipped = true;
|
|
2147
|
+
plan.skipReason = "eval skipped via its own skip: true";
|
|
2148
|
+
}
|
|
2149
|
+
plans.push(plan);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return plans;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/tracevals/judge/prompt.ts
|
|
2156
|
+
var PROMPT_VERSION = 4;
|
|
2157
|
+
var JUDGE_SYSTEM_PROMPT = `You are an adherence judge for AI agent sessions. You are given some evidence from an agent session and one assertion drawn from the instructions the session was operating under \u2014 a skill definition, an agent definition, or project rules.
|
|
2158
|
+
|
|
2159
|
+
The evidence is whatever the eval asked to be graded, and its heading says which: usually the session transcript (messages, tool calls, skills used), but it may instead be the session's final message, the list of files it touched, the instruction artifact itself, or one named file. Judge the assertion against what you were actually given.
|
|
2160
|
+
|
|
2161
|
+
Decide whether the session adhered to the assertion:
|
|
2162
|
+
- "pass" only when the session fully satisfied it.
|
|
2163
|
+
- "partial" when it partly satisfied it.
|
|
2164
|
+
- "fail" when it violated it or plainly did not do it.
|
|
2165
|
+
|
|
2166
|
+
Ground every judgment in the graded content: quote or cite the specific part of it \u2014 a message, a tool call, a line, or a telling omission \u2014 that supports your verdict in the "observed" field. If the assertion concerns something the graded content cannot show (e.g. events outside what you were given), say so and lower your confidence.
|
|
2167
|
+
|
|
2168
|
+
Respond with a single JSON object matching the provided schema. No prose outside the JSON.`;
|
|
2169
|
+
var MAX_ARTIFACT_CHARS = 8e3;
|
|
2170
|
+
function artifactWasTruncated(content) {
|
|
2171
|
+
return content.length > MAX_ARTIFACT_CHARS;
|
|
2172
|
+
}
|
|
2173
|
+
function buildUserContent(plan, graded, targetLabel = "transcript") {
|
|
2174
|
+
const parts = [];
|
|
2175
|
+
parts.push(`# Assertion
|
|
2176
|
+
${plan.assertion ?? ""}`);
|
|
2177
|
+
if (plan.evidence) {
|
|
2178
|
+
parts.push(`# Where to look
|
|
2179
|
+
${plan.evidence}`);
|
|
2180
|
+
}
|
|
2181
|
+
if (plan.examples?.pass?.length) {
|
|
2182
|
+
parts.push(`# Examples of passing behavior
|
|
2183
|
+
- ${plan.examples.pass.join("\n- ")}`);
|
|
2184
|
+
}
|
|
2185
|
+
if (plan.examples?.fail?.length) {
|
|
2186
|
+
parts.push(`# Examples of failing behavior
|
|
2187
|
+
- ${plan.examples.fail.join("\n- ")}`);
|
|
2188
|
+
}
|
|
2189
|
+
let artifactBody = plan.artifact.content;
|
|
2190
|
+
if (artifactBody.length > MAX_ARTIFACT_CHARS) {
|
|
2191
|
+
artifactBody = `${artifactBody.slice(0, MAX_ARTIFACT_CHARS)}
|
|
2192
|
+
[... artifact truncated ...]`;
|
|
2193
|
+
}
|
|
2194
|
+
const gradingTheArtifact = targetLabel === "artifact";
|
|
2195
|
+
const artifactHeading = `${plan.artifact.type} ("${plan.artifact.name}")`;
|
|
2196
|
+
if (!gradingTheArtifact) {
|
|
2197
|
+
parts.push(`# Source ${artifactHeading}
|
|
2198
|
+
${artifactBody}`);
|
|
2199
|
+
}
|
|
2200
|
+
parts.push(
|
|
2201
|
+
targetLabel === "transcript" ? `# Session transcript
|
|
2202
|
+
${graded}` : gradingTheArtifact ? `# Graded content: the ${artifactHeading} itself
|
|
2203
|
+
${artifactBody}` : `# Graded content (${targetLabel})
|
|
2204
|
+
${graded}`
|
|
2205
|
+
);
|
|
2206
|
+
return parts.join("\n\n");
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// src/tracevals/graders/glob.ts
|
|
2210
|
+
var literal = (ch) => ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2211
|
+
var compiled = /* @__PURE__ */ new Map();
|
|
2212
|
+
function globToRegExp(glob) {
|
|
2213
|
+
const cached = compiled.get(glob);
|
|
2214
|
+
if (cached !== void 0) return cached;
|
|
2215
|
+
const built = compile(glob);
|
|
2216
|
+
compiled.set(glob, built);
|
|
2217
|
+
return built;
|
|
2218
|
+
}
|
|
2219
|
+
function compile(glob) {
|
|
2220
|
+
const spec = glob.replace(/\\/g, "/").toLowerCase().replace(/(?:\*\*\/)+/g, "**/");
|
|
2221
|
+
let out = "";
|
|
2222
|
+
for (let i = 0; i < spec.length; i += 1) {
|
|
2223
|
+
const ch = spec[i];
|
|
2224
|
+
if (ch === "?") {
|
|
2225
|
+
out += "[^/]";
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
if (ch !== "*") {
|
|
2229
|
+
out += literal(ch);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (spec[i + 1] !== "*") {
|
|
2233
|
+
out += "[^/]*";
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
i += 1;
|
|
2237
|
+
if (spec[i + 1] === "/") {
|
|
2238
|
+
i += 1;
|
|
2239
|
+
if (out !== "") out += "(?:.*/)?";
|
|
2240
|
+
} else if (out.endsWith("/")) {
|
|
2241
|
+
out = `${out.slice(0, -1)}(?:/.*)?`;
|
|
2242
|
+
} else {
|
|
2243
|
+
out += ".*";
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return new RegExp(`^(?:.*/)?${out}$`);
|
|
2247
|
+
}
|
|
2248
|
+
function normalizePath(path) {
|
|
2249
|
+
return path.replace(/\\/g, "/").toLowerCase();
|
|
2250
|
+
}
|
|
2251
|
+
function matchesGlob(path, glob) {
|
|
2252
|
+
return globToRegExp(glob).test(normalizePath(path));
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/tracevals/graders/when.ts
|
|
2256
|
+
var stringValue = (kind) => (value) => typeof value === "string" && value.length > 0 ? void 0 : `options.when.${kind} must be a string`;
|
|
2257
|
+
var prompts = /* @__PURE__ */ new Map();
|
|
2258
|
+
function promptRe(pattern) {
|
|
2259
|
+
const cached = prompts.get(pattern);
|
|
2260
|
+
if (cached !== void 0) return cached;
|
|
2261
|
+
const built = new RegExp(pattern);
|
|
2262
|
+
prompts.set(pattern, built);
|
|
2263
|
+
return built;
|
|
2264
|
+
}
|
|
2265
|
+
var CONDITIONS = {
|
|
2266
|
+
"file-access": {
|
|
2267
|
+
validate: stringValue("file-access"),
|
|
2268
|
+
test: (value, window) => window.fileAccesses.some((a) => matchesGlob(a.path, value)),
|
|
2269
|
+
reason: (value) => `no file matching "${value}" was accessed`
|
|
2270
|
+
},
|
|
2271
|
+
"tool-used": {
|
|
2272
|
+
validate: stringValue("tool-used"),
|
|
2273
|
+
test: (value, window) => window.toolCalls.some((c) => c.name === value),
|
|
2274
|
+
reason: (value) => `the ${value} tool was never used`
|
|
2275
|
+
},
|
|
2276
|
+
"prompt-matches": {
|
|
2277
|
+
validate: (value) => {
|
|
2278
|
+
const shape = stringValue("prompt-matches")(value);
|
|
2279
|
+
if (shape !== void 0) return shape;
|
|
2280
|
+
try {
|
|
2281
|
+
new RegExp(value);
|
|
2282
|
+
} catch (err) {
|
|
2283
|
+
return `options.when.prompt-matches is not a valid regular expression: ${err.message}`;
|
|
2284
|
+
}
|
|
2285
|
+
return void 0;
|
|
2286
|
+
},
|
|
2287
|
+
test: (value, window) => {
|
|
2288
|
+
const re = promptRe(value);
|
|
2289
|
+
return window.userMessages.some((text) => re.test(text));
|
|
2290
|
+
},
|
|
2291
|
+
reason: (value) => `no prompt matched /${value}/`
|
|
2292
|
+
},
|
|
2293
|
+
"turn-count-above": {
|
|
2294
|
+
validate: (value) => {
|
|
2295
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
2296
|
+
return "options.when.turn-count-above must be a whole number";
|
|
2297
|
+
}
|
|
2298
|
+
if (value < 0) return "options.when.turn-count-above must be at least 0";
|
|
2299
|
+
return void 0;
|
|
2300
|
+
},
|
|
2301
|
+
test: (value, window) => window.turnCount > value,
|
|
2302
|
+
reason: (value, window) => `${window.turnCount} turn(s) is not more than ${value}`
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
var WHEN_CONDITIONS = Object.keys(
|
|
2306
|
+
CONDITIONS
|
|
2307
|
+
);
|
|
2308
|
+
function validateWhen(options) {
|
|
2309
|
+
const when = options.when;
|
|
2310
|
+
if (when === void 0) return void 0;
|
|
2311
|
+
if (typeof when !== "object" || when === null || Array.isArray(when)) {
|
|
2312
|
+
return "options.when must be an object of conditions";
|
|
2313
|
+
}
|
|
2314
|
+
const conditions = when;
|
|
2315
|
+
const keys = Object.keys(conditions);
|
|
2316
|
+
if (keys.length === 0) {
|
|
2317
|
+
return `options.when needs at least one of: ${WHEN_CONDITIONS.join(", ")}`;
|
|
2318
|
+
}
|
|
2319
|
+
for (const key of keys) {
|
|
2320
|
+
const condition = CONDITIONS[key];
|
|
2321
|
+
if (condition === void 0) {
|
|
2322
|
+
return `options.when.${key} is not a known condition; expected one of: ${WHEN_CONDITIONS.join(", ")}`;
|
|
2323
|
+
}
|
|
2324
|
+
const invalid = condition.validate(conditions[key]);
|
|
2325
|
+
if (invalid !== void 0) return invalid;
|
|
2326
|
+
}
|
|
2327
|
+
return void 0;
|
|
2328
|
+
}
|
|
2329
|
+
function evaluateWhen(options, window) {
|
|
2330
|
+
const when = options.when;
|
|
2331
|
+
if (when === void 0) return { armed: true };
|
|
2332
|
+
for (const name of WHEN_CONDITIONS) {
|
|
2333
|
+
const value = when[name];
|
|
2334
|
+
if (value === void 0) continue;
|
|
2335
|
+
const condition = CONDITIONS[name];
|
|
2336
|
+
if (condition.test(value, window)) continue;
|
|
2337
|
+
return {
|
|
2338
|
+
armed: false,
|
|
2339
|
+
reason: `trigger not met (${window.label}): ${condition.reason(value, window)}`
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
return { armed: true };
|
|
2343
|
+
}
|
|
2344
|
+
function rejectWhen(kind, options) {
|
|
2345
|
+
if (options.when === void 0) return void 0;
|
|
2346
|
+
return `options.when is not supported by the ${kind} grader, which grades the whole session rather than a window`;
|
|
2347
|
+
}
|
|
2348
|
+
function skippedTrigger(trigger) {
|
|
2349
|
+
return { findings: [], skipped: trigger.reason ?? "trigger not met" };
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/tracevals/graders/util.ts
|
|
2353
|
+
function finding(plan, message) {
|
|
2354
|
+
return {
|
|
2355
|
+
evalName: plan.evalName,
|
|
2356
|
+
artifact: plan.artifact.path,
|
|
2357
|
+
message,
|
|
2358
|
+
severity: plan.severity
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
function fail(plan, message) {
|
|
2362
|
+
return { findings: [finding(plan, message)] };
|
|
2363
|
+
}
|
|
2364
|
+
var pass = { findings: [] };
|
|
2365
|
+
function optionsError(kind, message) {
|
|
2366
|
+
return { findings: [], error: `${kind}: ${message}` };
|
|
2367
|
+
}
|
|
2368
|
+
function firstError(...checks) {
|
|
2369
|
+
return checks.find((check) => check !== void 0);
|
|
2370
|
+
}
|
|
2371
|
+
function requiredString(options, key) {
|
|
2372
|
+
const value = options[key];
|
|
2373
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2374
|
+
return `options.${key} is required`;
|
|
2375
|
+
}
|
|
2376
|
+
return void 0;
|
|
2377
|
+
}
|
|
2378
|
+
function optionalString(options, key) {
|
|
2379
|
+
const value = options[key];
|
|
2380
|
+
if (value === void 0) return void 0;
|
|
2381
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2382
|
+
return `options.${key} must be a non-empty string`;
|
|
2383
|
+
}
|
|
2384
|
+
return void 0;
|
|
2385
|
+
}
|
|
2386
|
+
function optionalEnum(options, key, allowed) {
|
|
2387
|
+
const value = options[key];
|
|
2388
|
+
if (value === void 0) return void 0;
|
|
2389
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
2390
|
+
return `options.${key} must be one of: ${allowed.join(", ")}`;
|
|
2391
|
+
}
|
|
2392
|
+
return void 0;
|
|
2393
|
+
}
|
|
2394
|
+
function optionalNumber(options, key, bounds = {}) {
|
|
2395
|
+
const value = options[key];
|
|
2396
|
+
if (value === void 0) return void 0;
|
|
2397
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2398
|
+
return `options.${key} must be a finite number`;
|
|
2399
|
+
}
|
|
2400
|
+
if (bounds.integer === true && !Number.isInteger(value)) {
|
|
2401
|
+
return `options.${key} must be a whole number`;
|
|
2402
|
+
}
|
|
2403
|
+
if (bounds.min !== void 0 && value < bounds.min) {
|
|
2404
|
+
return `options.${key} must be at least ${bounds.min}`;
|
|
2405
|
+
}
|
|
2406
|
+
return void 0;
|
|
2407
|
+
}
|
|
2408
|
+
function optionalBoolean(options, key) {
|
|
2409
|
+
if (options[key] !== void 0 && typeof options[key] !== "boolean") {
|
|
2410
|
+
return `options.${key} must be a boolean`;
|
|
2411
|
+
}
|
|
2412
|
+
return void 0;
|
|
2413
|
+
}
|
|
2414
|
+
function orderedBounds(options, minKey, maxKey) {
|
|
2415
|
+
const min = options[minKey];
|
|
2416
|
+
const max = options[maxKey];
|
|
2417
|
+
if (typeof min === "number" && typeof max === "number" && min > max) {
|
|
2418
|
+
return `options.${maxKey} must be greater than or equal to options.${minKey}`;
|
|
2419
|
+
}
|
|
2420
|
+
return void 0;
|
|
2421
|
+
}
|
|
2422
|
+
function requireOneOf(options, keys) {
|
|
2423
|
+
if (keys.some((key) => options[key] !== void 0)) return void 0;
|
|
2424
|
+
return `at least one of ${keys.map((k) => `options.${k}`).join(" or ")} is required`;
|
|
2425
|
+
}
|
|
2426
|
+
function windowFor(trace, plan) {
|
|
2427
|
+
const { name, type } = plan.artifact;
|
|
2428
|
+
if (type === "skill") return skillWindow(trace, name);
|
|
2429
|
+
if (type === "agent") return agentWindow(trace, name);
|
|
2430
|
+
if (type === "slash-command") return slashCommandWindow(trace, name);
|
|
2431
|
+
return {
|
|
2432
|
+
scope: "session",
|
|
2433
|
+
label: "whole session",
|
|
2434
|
+
empty: false,
|
|
2435
|
+
events: trace.events,
|
|
2436
|
+
toolCalls: trace.toolCalls,
|
|
2437
|
+
fileAccesses: trace.fileAccesses,
|
|
2438
|
+
skillInvocations: trace.skillInvocations,
|
|
2439
|
+
userMessages: trace.userMessages,
|
|
2440
|
+
assistantTexts: trace.assistantTexts,
|
|
2441
|
+
turnCount: trace.turnCount
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
function skippedWindow(window) {
|
|
2445
|
+
return { findings: [], skipped: window.reason ?? "the artifact governed no turns" };
|
|
2446
|
+
}
|
|
2447
|
+
function skillWindow(trace, name) {
|
|
2448
|
+
return injectionWindow(trace, "skill", `skill "${name}"`, (s) => s.name === name);
|
|
2449
|
+
}
|
|
2450
|
+
function slashCommandWindow(trace, name) {
|
|
2451
|
+
return injectionWindow(
|
|
2452
|
+
trace,
|
|
2453
|
+
"slash-command",
|
|
2454
|
+
`slash command "/${name}"`,
|
|
2455
|
+
(s) => s.name === name && s.via === "command-injection"
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
function injectionWindow(trace, scope, label, opensHere) {
|
|
2459
|
+
const opens = trace.skillInvocations.filter(opensHere).map((s) => s.index);
|
|
2460
|
+
if (opens.length === 0) {
|
|
2461
|
+
return emptyWindow(
|
|
2462
|
+
scope,
|
|
2463
|
+
label,
|
|
2464
|
+
`${label} was never invoked in this trace, so it governed no turns`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const chainOf = (index) => trace.events[index]?.branchId;
|
|
2468
|
+
const own = new Set(opens.map(chainOf));
|
|
2469
|
+
const boundaries = trace.skillInvocations.filter((s) => own.has(chainOf(s.index))).map((s) => s.index).sort((a, b) => a - b);
|
|
2470
|
+
const spans = opens.map((start) => ({
|
|
2471
|
+
start,
|
|
2472
|
+
end: boundaries.find((b) => b > start) ?? Number.POSITIVE_INFINITY
|
|
2473
|
+
}));
|
|
2474
|
+
return materialize(trace, scope, label, spans, void 0, own);
|
|
2475
|
+
}
|
|
2476
|
+
function agentWindow(trace, name) {
|
|
2477
|
+
const label = `agent "${name}"`;
|
|
2478
|
+
const mine = trace.subagentBranches.filter((b) => b.agentType === name);
|
|
2479
|
+
if (mine.length === 0) {
|
|
2480
|
+
const spawned = trace.agentSpawns.some((a) => a.subagentType === name);
|
|
2481
|
+
return emptyWindow(
|
|
2482
|
+
"agent",
|
|
2483
|
+
label,
|
|
2484
|
+
spawned ? `${label} was spawned but recorded no subagent turns (no inline sidechain records and no sidecar transcript), so it governed no turns` : `${label} was never spawned in this trace, so it governed no turns`
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
const branchIds = new Set(mine.map((b) => b.branchId));
|
|
2488
|
+
for (const branch of trace.subagentBranches) {
|
|
2489
|
+
if (branchIds.has(branch.branchId)) continue;
|
|
2490
|
+
const nested = mine.some(
|
|
2491
|
+
(m) => m.startIndex <= branch.startIndex && branch.endIndex <= m.endIndex
|
|
2492
|
+
);
|
|
2493
|
+
if (nested) branchIds.add(branch.branchId);
|
|
2494
|
+
}
|
|
2495
|
+
const spans = mine.map((b) => ({ start: b.startIndex, end: b.endIndex }));
|
|
2496
|
+
return materialize(
|
|
2497
|
+
trace,
|
|
2498
|
+
"agent",
|
|
2499
|
+
label,
|
|
2500
|
+
spans,
|
|
2501
|
+
branchIds,
|
|
2502
|
+
new Set(mine.map((b) => b.branchId))
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
function emptyWindow(scope, label, reason) {
|
|
2506
|
+
return {
|
|
2507
|
+
scope,
|
|
2508
|
+
label,
|
|
2509
|
+
empty: true,
|
|
2510
|
+
reason,
|
|
2511
|
+
events: [],
|
|
2512
|
+
toolCalls: [],
|
|
2513
|
+
fileAccesses: [],
|
|
2514
|
+
skillInvocations: [],
|
|
2515
|
+
userMessages: [],
|
|
2516
|
+
assistantTexts: [],
|
|
2517
|
+
turnCount: 0
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
function materialize(trace, scope, label, spans, branchIds, own) {
|
|
2521
|
+
const inSpan = (index) => spans.some((s) => index >= s.start && index < s.end);
|
|
2522
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
2523
|
+
if (branchIds !== void 0) {
|
|
2524
|
+
for (const event of trace.events) {
|
|
2525
|
+
if (!inSpan(event.index)) continue;
|
|
2526
|
+
if (event.branchId === void 0 || !branchIds.has(event.branchId)) {
|
|
2527
|
+
excluded.add(event.index);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const inWindow = (index) => inSpan(index) && !excluded.has(index);
|
|
2532
|
+
const events = trace.events.filter((e) => inWindow(e.index));
|
|
2533
|
+
const textOf = (kind) => events.filter((e) => e.kind === kind && own.has(e.branchId) && e.text).map((e) => e.text);
|
|
2534
|
+
const userMessages = textOf("user");
|
|
2535
|
+
return {
|
|
2536
|
+
scope,
|
|
2537
|
+
label,
|
|
2538
|
+
empty: false,
|
|
2539
|
+
events,
|
|
2540
|
+
toolCalls: trace.toolCalls.filter((c) => inWindow(c.index)),
|
|
2541
|
+
fileAccesses: trace.fileAccesses.filter((a) => inWindow(a.index)),
|
|
2542
|
+
skillInvocations: trace.skillInvocations.filter((s) => inWindow(s.index)),
|
|
2543
|
+
userMessages,
|
|
2544
|
+
assistantTexts: textOf("assistant"),
|
|
2545
|
+
turnCount: userMessages.length
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// src/tracevals/graders/tool-usage.ts
|
|
2550
|
+
function checkContradiction(options) {
|
|
2551
|
+
const expect = options.expect ?? "used";
|
|
2552
|
+
const { min, max } = options;
|
|
2553
|
+
if (expect === "not-used" && typeof min === "number" && min > 0) {
|
|
2554
|
+
return "options.min is unsatisfiable with expect: not-used";
|
|
2555
|
+
}
|
|
2556
|
+
if (expect === "used" && typeof max === "number" && max === 0) {
|
|
2557
|
+
return "options.max of 0 is unsatisfiable with expect: used";
|
|
2558
|
+
}
|
|
2559
|
+
return void 0;
|
|
2560
|
+
}
|
|
2561
|
+
function validateOptions(options) {
|
|
2562
|
+
return firstError(
|
|
2563
|
+
requiredString(options, "tool"),
|
|
2564
|
+
optionalEnum(options, "expect", ["used", "not-used"]),
|
|
2565
|
+
optionalNumber(options, "min", { min: 0, integer: true }),
|
|
2566
|
+
optionalNumber(options, "max", { min: 0, integer: true }),
|
|
2567
|
+
orderedBounds(options, "min", "max"),
|
|
2568
|
+
checkContradiction(options),
|
|
2569
|
+
optionalBoolean(options, "includeSidechains"),
|
|
2570
|
+
validateWhen(options)
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
var toolUsageGrader = {
|
|
2574
|
+
kind: "tool-usage",
|
|
2575
|
+
validateOptions,
|
|
2576
|
+
grade({ trace, plan }) {
|
|
2577
|
+
const options = plan.options ?? {};
|
|
2578
|
+
const invalid = validateOptions(options);
|
|
2579
|
+
if (invalid !== void 0) return optionsError("tool-usage", invalid);
|
|
2580
|
+
const tool = options.tool;
|
|
2581
|
+
const expect = options.expect ?? "used";
|
|
2582
|
+
const includeSidechains = options.includeSidechains === true;
|
|
2583
|
+
const window = windowFor(trace, plan);
|
|
2584
|
+
if (window.empty) return skippedWindow(window);
|
|
2585
|
+
const trigger = evaluateWhen(options, window);
|
|
2586
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2587
|
+
const branchScoped = window.scope === "agent";
|
|
2588
|
+
const count = window.toolCalls.filter(
|
|
2589
|
+
(c) => c.name === tool && (branchScoped || includeSidechains || !c.sidechain)
|
|
2590
|
+
).length;
|
|
2591
|
+
if (expect === "not-used" && count > 0) {
|
|
2592
|
+
return fail(plan, `tool ${tool} was used ${count} time(s) but must not be`);
|
|
2593
|
+
}
|
|
2594
|
+
if (expect === "used" && count === 0 && options.min === void 0) {
|
|
2595
|
+
return fail(plan, `tool ${tool} was never used`);
|
|
2596
|
+
}
|
|
2597
|
+
if (typeof options.min === "number" && count < options.min) {
|
|
2598
|
+
return fail(
|
|
2599
|
+
plan,
|
|
2600
|
+
`tool ${tool} was used ${count} time(s); at least ${options.min} required`
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
if (typeof options.max === "number" && count > options.max) {
|
|
2604
|
+
return fail(
|
|
2605
|
+
plan,
|
|
2606
|
+
`tool ${tool} was used ${count} time(s); at most ${options.max} allowed`
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
return pass;
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
|
|
2613
|
+
// src/tracevals/graders/tool-order.ts
|
|
2614
|
+
function validateOptions2(options) {
|
|
2615
|
+
return firstError(
|
|
2616
|
+
requiredString(options, "before"),
|
|
2617
|
+
requiredString(options, "after"),
|
|
2618
|
+
optionalString(options, "beforeInputMatch"),
|
|
2619
|
+
optionalString(options, "afterInputMatch"),
|
|
2620
|
+
optionalBoolean(options, "includeSidechains"),
|
|
2621
|
+
validateWhen(options),
|
|
2622
|
+
...["beforeInputMatch", "afterInputMatch"].map((key) => {
|
|
2623
|
+
const value = options[key];
|
|
2624
|
+
if (typeof value !== "string") return void 0;
|
|
2625
|
+
try {
|
|
2626
|
+
new RegExp(value);
|
|
2627
|
+
return void 0;
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
return `options.${key} is not a valid regular expression (${err instanceof Error ? err.message : String(err)})`;
|
|
2630
|
+
}
|
|
2631
|
+
})
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
function ordinalsOf(calls, tool, inputMatch) {
|
|
2635
|
+
const re = inputMatch === void 0 ? void 0 : new RegExp(inputMatch);
|
|
2636
|
+
return calls.filter(
|
|
2637
|
+
(c) => c.name === tool && (re === void 0 || re.test(JSON.stringify(c.input)))
|
|
2638
|
+
).map((c) => c.index);
|
|
2639
|
+
}
|
|
2640
|
+
function describe(tool, inputMatch) {
|
|
2641
|
+
return inputMatch === void 0 ? tool : `${tool} matching /${inputMatch}/`;
|
|
2642
|
+
}
|
|
2643
|
+
var toolOrderGrader = {
|
|
2644
|
+
kind: "tool-order",
|
|
2645
|
+
validateOptions: validateOptions2,
|
|
2646
|
+
grade({ trace, plan }) {
|
|
2647
|
+
const options = plan.options ?? {};
|
|
2648
|
+
const invalid = validateOptions2(options);
|
|
2649
|
+
if (invalid !== void 0) return optionsError("tool-order", invalid);
|
|
2650
|
+
const before = options.before;
|
|
2651
|
+
const after = options.after;
|
|
2652
|
+
const beforeMatch = options.beforeInputMatch;
|
|
2653
|
+
const afterMatch = options.afterInputMatch;
|
|
2654
|
+
const includeSidechains = options.includeSidechains === true;
|
|
2655
|
+
const window = windowFor(trace, plan);
|
|
2656
|
+
if (window.empty) return skippedWindow(window);
|
|
2657
|
+
const trigger = evaluateWhen(options, window);
|
|
2658
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2659
|
+
const branchScoped = window.scope === "agent";
|
|
2660
|
+
const calls = window.toolCalls.filter(
|
|
2661
|
+
(c) => branchScoped || includeSidechains || !c.sidechain
|
|
2662
|
+
);
|
|
2663
|
+
const beforeOrdinals = ordinalsOf(calls, before, beforeMatch);
|
|
2664
|
+
const afterOrdinals = ordinalsOf(calls, after, afterMatch);
|
|
2665
|
+
const firstBefore = beforeOrdinals.length > 0 ? Math.min(...beforeOrdinals) : void 0;
|
|
2666
|
+
const lastAfter = afterOrdinals.length > 0 ? Math.max(...afterOrdinals) : void 0;
|
|
2667
|
+
if (firstBefore === void 0 && lastAfter === void 0) return pass;
|
|
2668
|
+
if (lastAfter === void 0) {
|
|
2669
|
+
return fail(
|
|
2670
|
+
plan,
|
|
2671
|
+
`${describe(after, afterMatch)} was never used, so it could not follow ${describe(before, beforeMatch)}`
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
if (firstBefore === void 0) {
|
|
2675
|
+
return fail(
|
|
2676
|
+
plan,
|
|
2677
|
+
`${describe(after, afterMatch)} was used without ${describe(before, beforeMatch)} ever being used first`
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
if (firstBefore > lastAfter) {
|
|
2681
|
+
return fail(
|
|
2682
|
+
plan,
|
|
2683
|
+
`${describe(before, beforeMatch)} was only used after ${describe(after, afterMatch)}`
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
return pass;
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
|
|
2690
|
+
// src/tracevals/graders/skill-invoked.ts
|
|
2691
|
+
function validateOptions3(options) {
|
|
2692
|
+
return firstError(
|
|
2693
|
+
requiredString(options, "skill"),
|
|
2694
|
+
optionalEnum(options, "expect", ["used", "not-used"]),
|
|
2695
|
+
validateWhen(options)
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
var skillInvokedGrader = {
|
|
2699
|
+
kind: "skill-invoked",
|
|
2700
|
+
validateOptions: validateOptions3,
|
|
2701
|
+
grade({ trace, plan }) {
|
|
2702
|
+
const options = plan.options ?? {};
|
|
2703
|
+
const invalid = validateOptions3(options);
|
|
2704
|
+
if (invalid !== void 0) return optionsError("skill-invoked", invalid);
|
|
2705
|
+
const skill = options.skill;
|
|
2706
|
+
const expect = options.expect ?? "used";
|
|
2707
|
+
const window = windowFor(trace, plan);
|
|
2708
|
+
if (window.empty) return skippedWindow(window);
|
|
2709
|
+
const trigger = evaluateWhen(options, window);
|
|
2710
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2711
|
+
const used = window.skillInvocations.some((s) => s.name === skill);
|
|
2712
|
+
if (expect === "used" && !used) {
|
|
2713
|
+
return fail(plan, `skill ${skill} was never invoked`);
|
|
2714
|
+
}
|
|
2715
|
+
if (expect === "not-used" && used) {
|
|
2716
|
+
return fail(plan, `skill ${skill} was invoked but must not be`);
|
|
2717
|
+
}
|
|
2718
|
+
return pass;
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
// src/tracevals/graders/file-access.ts
|
|
2723
|
+
var OPS = ["read", "write", "edit"];
|
|
2724
|
+
function validateOptions4(options) {
|
|
2725
|
+
return firstError(
|
|
2726
|
+
requiredString(options, "path"),
|
|
2727
|
+
optionalEnum(options, "op", OPS),
|
|
2728
|
+
optionalEnum(options, "expect", ["accessed", "not-accessed"]),
|
|
2729
|
+
validateWhen(options)
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
function matches(accessPath, spec) {
|
|
2733
|
+
return matchesGlob(accessPath, spec);
|
|
2734
|
+
}
|
|
2735
|
+
var fileAccessGrader = {
|
|
2736
|
+
kind: "file-access",
|
|
2737
|
+
validateOptions: validateOptions4,
|
|
2738
|
+
grade({ trace, plan }) {
|
|
2739
|
+
const options = plan.options ?? {};
|
|
2740
|
+
const invalid = validateOptions4(options);
|
|
2741
|
+
if (invalid !== void 0) return optionsError("file-access", invalid);
|
|
2742
|
+
const path = options.path;
|
|
2743
|
+
const op = options.op;
|
|
2744
|
+
const expect = options.expect ?? "accessed";
|
|
2745
|
+
const window = windowFor(trace, plan);
|
|
2746
|
+
if (window.empty) return skippedWindow(window);
|
|
2747
|
+
const trigger = evaluateWhen(options, window);
|
|
2748
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2749
|
+
const accessed = window.fileAccesses.some(
|
|
2750
|
+
(a) => matches(a.path, path) && (op === void 0 || a.op === op)
|
|
2751
|
+
);
|
|
2752
|
+
const opLabel = op ?? "any";
|
|
2753
|
+
if (expect === "accessed" && !accessed) {
|
|
2754
|
+
return fail(plan, `file ${path} was never accessed (op: ${opLabel})`);
|
|
2755
|
+
}
|
|
2756
|
+
if (expect === "not-accessed" && accessed) {
|
|
2757
|
+
return fail(plan, `file ${path} was accessed (op: ${opLabel}) but must not be`);
|
|
2758
|
+
}
|
|
2759
|
+
return pass;
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
// src/tracevals/graders/turn-count.ts
|
|
2764
|
+
function validateOptions5(options) {
|
|
2765
|
+
return firstError(
|
|
2766
|
+
requireOneOf(options, ["min", "max"]),
|
|
2767
|
+
optionalNumber(options, "min", { min: 0, integer: true }),
|
|
2768
|
+
optionalNumber(options, "max", { min: 0, integer: true }),
|
|
2769
|
+
orderedBounds(options, "min", "max"),
|
|
2770
|
+
validateWhen(options)
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
var turnCountGrader = {
|
|
2774
|
+
kind: "turn-count",
|
|
2775
|
+
validateOptions: validateOptions5,
|
|
2776
|
+
grade({ trace, plan }) {
|
|
2777
|
+
const options = plan.options ?? {};
|
|
2778
|
+
const invalid = validateOptions5(options);
|
|
2779
|
+
if (invalid !== void 0) return optionsError("turn-count", invalid);
|
|
2780
|
+
const window = windowFor(trace, plan);
|
|
2781
|
+
if (window.empty) return skippedWindow(window);
|
|
2782
|
+
const trigger = evaluateWhen(options, window);
|
|
2783
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2784
|
+
const { turnCount } = window;
|
|
2785
|
+
if (typeof options.max === "number" && turnCount > options.max) {
|
|
2786
|
+
return fail(plan, `${turnCount} turn(s); at most ${options.max} allowed`);
|
|
2787
|
+
}
|
|
2788
|
+
if (typeof options.min === "number" && turnCount < options.min) {
|
|
2789
|
+
return fail(plan, `${turnCount} turn(s); at least ${options.min} required`);
|
|
2790
|
+
}
|
|
2791
|
+
return pass;
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
|
|
2795
|
+
// src/tracevals/graders/cost.ts
|
|
2796
|
+
function validateOptions6(options) {
|
|
2797
|
+
return firstError(
|
|
2798
|
+
requireOneOf(options, ["maxUsd", "maxTokens"]),
|
|
2799
|
+
optionalNumber(options, "maxUsd", { min: 0 }),
|
|
2800
|
+
optionalNumber(options, "maxTokens", { min: 0 }),
|
|
2801
|
+
rejectWhen("cost", options)
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
var costGrader = {
|
|
2805
|
+
kind: "cost",
|
|
2806
|
+
validateOptions: validateOptions6,
|
|
2807
|
+
grade({ trace, plan }) {
|
|
2808
|
+
const options = plan.options ?? {};
|
|
2809
|
+
const invalid = validateOptions6(options);
|
|
2810
|
+
if (invalid !== void 0) return optionsError("cost", invalid);
|
|
2811
|
+
const usage = trace.usage;
|
|
2812
|
+
const skips = [];
|
|
2813
|
+
let checked = false;
|
|
2814
|
+
if (typeof options.maxUsd === "number") {
|
|
2815
|
+
if (usage?.totalCostUsd === void 0) {
|
|
2816
|
+
skips.push("no cost data (maxUsd not checkable)");
|
|
2817
|
+
} else {
|
|
2818
|
+
checked = true;
|
|
2819
|
+
if (usage.totalCostUsd > options.maxUsd) {
|
|
2820
|
+
return fail(
|
|
2821
|
+
plan,
|
|
2822
|
+
`session cost $${usage.totalCostUsd.toFixed(4)}; at most $${options.maxUsd} allowed`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
if (typeof options.maxTokens === "number") {
|
|
2828
|
+
if (usage === void 0) {
|
|
2829
|
+
skips.push("no usage data (maxTokens not checkable)");
|
|
2830
|
+
} else {
|
|
2831
|
+
checked = true;
|
|
2832
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
2833
|
+
if (total > options.maxTokens) {
|
|
2834
|
+
return fail(
|
|
2835
|
+
plan,
|
|
2836
|
+
`session used ${total} tokens; at most ${options.maxTokens} allowed`
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (!checked && skips.length > 0) {
|
|
2842
|
+
return {
|
|
2843
|
+
findings: [],
|
|
2844
|
+
skipped: `trace carries insufficient cost data: ${skips.join("; ")}`
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
return pass;
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
|
|
2851
|
+
// src/tracevals/core/target.ts
|
|
2852
|
+
import { readFileSync } from "node:fs";
|
|
2853
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
2854
|
+
var DEFAULT_TARGET = "transcript";
|
|
2855
|
+
function describeTarget(target) {
|
|
2856
|
+
const t = target ?? DEFAULT_TARGET;
|
|
2857
|
+
return typeof t === "string" ? t : `file ${t.path}`;
|
|
2858
|
+
}
|
|
2859
|
+
function readTarget(target, ctx) {
|
|
2860
|
+
const t = target ?? DEFAULT_TARGET;
|
|
2861
|
+
if (t === "transcript") {
|
|
2862
|
+
return { ok: true, text: ctx.renderedTrace, label: "transcript" };
|
|
2863
|
+
}
|
|
2864
|
+
if (t === "last-message") {
|
|
2865
|
+
const last = ctx.trace.assistantTexts.at(-1);
|
|
2866
|
+
return { ok: true, text: last ?? "", label: "last-message" };
|
|
2867
|
+
}
|
|
2868
|
+
if (t === "files") {
|
|
2869
|
+
const paths = [...new Set(ctx.trace.fileAccesses.map((f) => f.path))];
|
|
2870
|
+
return { ok: true, text: paths.join("\n"), label: "files" };
|
|
2871
|
+
}
|
|
2872
|
+
if (t === "artifact") {
|
|
2873
|
+
return { ok: true, text: ctx.artifactContent, label: "artifact" };
|
|
2874
|
+
}
|
|
2875
|
+
if (isAbsolute2(t.path)) {
|
|
2876
|
+
return {
|
|
2877
|
+
ok: false,
|
|
2878
|
+
reason: `target file "${t.path}" is an absolute path; name it relative to the project root`
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
const abs = resolve2(ctx.root, t.path);
|
|
2882
|
+
const rel = relative2(ctx.root, abs);
|
|
2883
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute2(rel)) {
|
|
2884
|
+
return {
|
|
2885
|
+
ok: false,
|
|
2886
|
+
reason: `target file "${t.path}" resolves outside the project root`
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
try {
|
|
2890
|
+
return { ok: true, text: readFileSync(abs, "utf8"), label: `file ${t.path}` };
|
|
2891
|
+
} catch (err) {
|
|
2892
|
+
const code = err.code ?? "unknown error";
|
|
2893
|
+
return { ok: false, reason: `target file "${t.path}" could not be read (${code})` };
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/tracevals/graders/regex.ts
|
|
2898
|
+
function checkFlags(options) {
|
|
2899
|
+
const flags = options.flags;
|
|
2900
|
+
if (flags === void 0) return void 0;
|
|
2901
|
+
if (typeof flags !== "string") return "options.flags must be a string";
|
|
2902
|
+
try {
|
|
2903
|
+
new RegExp("", flags);
|
|
2904
|
+
} catch (err) {
|
|
2905
|
+
return `options.flags is invalid: ${err.message}`;
|
|
2906
|
+
}
|
|
2907
|
+
return void 0;
|
|
2908
|
+
}
|
|
2909
|
+
function checkPattern(options) {
|
|
2910
|
+
const required = requiredString(options, "pattern");
|
|
2911
|
+
if (required !== void 0) return required;
|
|
2912
|
+
try {
|
|
2913
|
+
new RegExp(
|
|
2914
|
+
options.pattern,
|
|
2915
|
+
options.flags ?? ""
|
|
2916
|
+
);
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
return `options.pattern is not a valid regular expression: ${err.message}`;
|
|
2919
|
+
}
|
|
2920
|
+
return void 0;
|
|
2921
|
+
}
|
|
2922
|
+
function validateOptions7(options) {
|
|
2923
|
+
return firstError(
|
|
2924
|
+
checkFlags(options),
|
|
2925
|
+
checkPattern(options),
|
|
2926
|
+
optionalEnum(options, "on", ["assistant", "user", "all"]),
|
|
2927
|
+
optionalEnum(options, "expect", ["match", "no-match"]),
|
|
2928
|
+
validateWhen(options)
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
var regexGrader = {
|
|
2932
|
+
kind: "regex",
|
|
2933
|
+
validateOptions: validateOptions7,
|
|
2934
|
+
grade({ trace, plan, projectRoot }) {
|
|
2935
|
+
const options = plan.options ?? {};
|
|
2936
|
+
const invalid = validateOptions7(options);
|
|
2937
|
+
if (invalid !== void 0) return optionsError("regex", invalid);
|
|
2938
|
+
const pattern = options.pattern;
|
|
2939
|
+
const re = new RegExp(pattern, options.flags ?? "");
|
|
2940
|
+
const on = options.on ?? "assistant";
|
|
2941
|
+
const expect = options.expect ?? "match";
|
|
2942
|
+
const window = windowFor(trace, plan);
|
|
2943
|
+
if (window.empty) return skippedWindow(window);
|
|
2944
|
+
const trigger = evaluateWhen(options, window);
|
|
2945
|
+
if (!trigger.armed) return skippedTrigger(trigger);
|
|
2946
|
+
const target = plan.target ?? "transcript";
|
|
2947
|
+
let corpus;
|
|
2948
|
+
let where;
|
|
2949
|
+
if (target === "transcript") {
|
|
2950
|
+
corpus = on === "user" ? window.userMessages : on === "all" ? [...window.userMessages, ...window.assistantTexts] : window.assistantTexts;
|
|
2951
|
+
where = `${on} text`;
|
|
2952
|
+
} else {
|
|
2953
|
+
const selected = readTarget(target, {
|
|
2954
|
+
trace,
|
|
2955
|
+
// Never consulted: `transcript` took the branch above.
|
|
2956
|
+
renderedTrace: "",
|
|
2957
|
+
artifactContent: plan.artifact.content,
|
|
2958
|
+
root: projectRoot ?? trace.cwd
|
|
2959
|
+
});
|
|
2960
|
+
if (!selected.ok) return optionsError("regex", selected.reason);
|
|
2961
|
+
corpus = [selected.text];
|
|
2962
|
+
where = selected.label;
|
|
2963
|
+
}
|
|
2964
|
+
const matched = corpus.some((text) => re.test(text));
|
|
2965
|
+
if (expect === "match" && !matched) {
|
|
2966
|
+
return fail(plan, `pattern /${pattern}/ never matched ${where}`);
|
|
2967
|
+
}
|
|
2968
|
+
if (expect === "no-match" && matched) {
|
|
2969
|
+
return fail(plan, `pattern /${pattern}/ matched ${where} but must not`);
|
|
2970
|
+
}
|
|
2971
|
+
return pass;
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
// src/tracevals/graders/json-output.ts
|
|
2976
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2977
|
+
var compiled2 = /* @__PURE__ */ new Map();
|
|
2978
|
+
function compileSchema(schema) {
|
|
2979
|
+
const key = JSON.stringify(schema);
|
|
2980
|
+
const hit = compiled2.get(key);
|
|
2981
|
+
if (hit !== void 0) return hit;
|
|
2982
|
+
const built = new Ajv2020({ allErrors: true, strict: false }).compile(schema);
|
|
2983
|
+
compiled2.set(key, built);
|
|
2984
|
+
return built;
|
|
2985
|
+
}
|
|
2986
|
+
function validateOptions8(options) {
|
|
2987
|
+
const schema = options.schema;
|
|
2988
|
+
if (schema === void 0 || schema === null || typeof schema !== "object" || Array.isArray(schema)) {
|
|
2989
|
+
return "options.schema is required and must be a JSON Schema object";
|
|
2990
|
+
}
|
|
2991
|
+
return rejectWhen("json-output", options);
|
|
2992
|
+
}
|
|
2993
|
+
var jsonOutputGrader = {
|
|
2994
|
+
kind: "json-output",
|
|
2995
|
+
validateOptions: validateOptions8,
|
|
2996
|
+
grade({ trace, plan }) {
|
|
2997
|
+
const options = plan.options ?? {};
|
|
2998
|
+
const invalid = validateOptions8(options);
|
|
2999
|
+
if (invalid !== void 0) return optionsError("json-output", invalid);
|
|
3000
|
+
const schema = options.schema;
|
|
3001
|
+
const last = trace.assistantTexts.at(-1);
|
|
3002
|
+
if (last === void 0) {
|
|
3003
|
+
return fail(plan, "session produced no assistant text to validate");
|
|
3004
|
+
}
|
|
3005
|
+
let data;
|
|
3006
|
+
try {
|
|
3007
|
+
data = JSON.parse(last);
|
|
3008
|
+
} catch {
|
|
3009
|
+
return fail(plan, "final assistant output is not valid JSON");
|
|
3010
|
+
}
|
|
3011
|
+
let validate;
|
|
3012
|
+
try {
|
|
3013
|
+
validate = compileSchema(schema);
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
return optionsError(
|
|
3016
|
+
"json-output",
|
|
3017
|
+
`schema failed to compile: ${err.message}`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
if (validate(data)) return pass;
|
|
3021
|
+
return {
|
|
3022
|
+
findings: (validate.errors ?? []).map(
|
|
3023
|
+
(e) => finding(plan, `${e.instancePath || "/"} ${e.message ?? "invalid"}`)
|
|
3024
|
+
)
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
|
|
3029
|
+
// src/tracevals/graders/command.ts
|
|
3030
|
+
import { createHash } from "node:crypto";
|
|
3031
|
+
|
|
3032
|
+
// src/shared/exec.ts
|
|
3033
|
+
import { realExec } from "@hawkeyexl/inference";
|
|
3034
|
+
function outputTail(result, maxChars = 400) {
|
|
3035
|
+
const text = (result.stderr.trim() || result.stdout.trim()).trim();
|
|
3036
|
+
if (text.length <= maxChars) return text;
|
|
3037
|
+
return `\u2026${text.slice(-maxChars)}`;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
// src/tracevals/graders/command.ts
|
|
3041
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3042
|
+
var STDERR_LIMIT = 2e3;
|
|
3043
|
+
var commandGrader = {
|
|
3044
|
+
kind: "command",
|
|
3045
|
+
async grade(ctx) {
|
|
3046
|
+
const { plan, trace } = ctx;
|
|
3047
|
+
if (!plan.command || plan.command.length === 0) {
|
|
3048
|
+
return {
|
|
3049
|
+
findings: [],
|
|
3050
|
+
error: "command-graded eval has no `command`; add one (or remove the eval) \u2014 manni tracevals does not generate check scripts"
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
if (plan.generatedAssertionHash !== void 0) {
|
|
3054
|
+
if (plan.assertion === void 0) {
|
|
3055
|
+
return {
|
|
3056
|
+
findings: [],
|
|
3057
|
+
error: "generated-assertion-hash is present but the eval has no assertion, so the hash cannot be checked against anything; remove the hash or restore the assertion"
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
const actual = createHash("sha256").update(plan.assertion).digest("hex");
|
|
3061
|
+
if (actual !== plan.generatedAssertionHash) {
|
|
3062
|
+
return {
|
|
3063
|
+
findings: [],
|
|
3064
|
+
error: `assertion has changed since the command was generated (generated-assertion-hash ${plan.generatedAssertionHash.slice(0, 12)}\u2026, assertion now hashes to ${actual.slice(0, 12)}\u2026); regenerate the command or update the hash`
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const argv = plan.command.map((part) => part.replaceAll("{trace}", trace.file));
|
|
3069
|
+
const timeoutMs = plan.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3070
|
+
const successCodes = plan.successExitCodes ?? [0];
|
|
3071
|
+
const run = await execute(argv, ctx.projectRoot ?? trace.cwd, timeoutMs);
|
|
3072
|
+
if (run.timedOut) {
|
|
3073
|
+
return {
|
|
3074
|
+
findings: [],
|
|
3075
|
+
error: `command timed out after ${timeoutMs}ms: ${argv.join(" ")}`
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
if (run.spawnError !== void 0) {
|
|
3079
|
+
return { findings: [], error: `could not run command: ${run.spawnError}` };
|
|
3080
|
+
}
|
|
3081
|
+
if (run.code !== null && successCodes.includes(run.code)) {
|
|
3082
|
+
return { findings: [] };
|
|
3083
|
+
}
|
|
3084
|
+
const detail2 = run.stderr.trim();
|
|
3085
|
+
return {
|
|
3086
|
+
findings: [
|
|
3087
|
+
{
|
|
3088
|
+
evalName: plan.evalName,
|
|
3089
|
+
artifact: plan.artifact.path,
|
|
3090
|
+
severity: plan.severity,
|
|
3091
|
+
message: `command exited ${run.code ?? "on a signal"} (expected ${successCodes.join(" or ")})` + (detail2 ? `: ${detail2.slice(0, STDERR_LIMIT)}` : "")
|
|
3092
|
+
}
|
|
3093
|
+
]
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
async function execute(argv, cwd, timeoutMs) {
|
|
3098
|
+
const result = await realExec(argv, { cwd, timeoutMs });
|
|
3099
|
+
return {
|
|
3100
|
+
code: result.code,
|
|
3101
|
+
// Capped here, not merely when printed: a check script can write far more
|
|
3102
|
+
// to stderr than a report should carry.
|
|
3103
|
+
stderr: result.stderr.slice(0, STDERR_LIMIT),
|
|
3104
|
+
timedOut: result.timedOut,
|
|
3105
|
+
...result.spawnError === void 0 ? {} : { spawnError: result.spawnError }
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// src/tracevals/graders/registry.ts
|
|
3110
|
+
var graders = new Map(
|
|
3111
|
+
[
|
|
3112
|
+
toolUsageGrader,
|
|
3113
|
+
toolOrderGrader,
|
|
3114
|
+
skillInvokedGrader,
|
|
3115
|
+
fileAccessGrader,
|
|
3116
|
+
turnCountGrader,
|
|
3117
|
+
costGrader,
|
|
3118
|
+
regexGrader,
|
|
3119
|
+
jsonOutputGrader,
|
|
3120
|
+
commandGrader
|
|
3121
|
+
].map((g) => [g.kind, g])
|
|
3122
|
+
);
|
|
3123
|
+
var BUILTIN_GRADER_KINDS = new Set(graders.keys());
|
|
3124
|
+
function registerGrader(grader) {
|
|
3125
|
+
graders.set(grader.kind, grader);
|
|
3126
|
+
}
|
|
3127
|
+
function graderFor(kind) {
|
|
3128
|
+
return graders.get(kind);
|
|
3129
|
+
}
|
|
3130
|
+
function listGraderKinds() {
|
|
3131
|
+
return [...graders.keys()];
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// src/tracevals/judge/render.ts
|
|
3135
|
+
var DEFAULTS = {
|
|
3136
|
+
maxBlockChars: 2e3,
|
|
3137
|
+
maxTotalChars: 15e4,
|
|
3138
|
+
redact: []
|
|
3139
|
+
};
|
|
3140
|
+
var MIN_TIMELINE_CHARS = 200;
|
|
3141
|
+
var HEADER_TRUNCATED = "\n[... header truncated ...]";
|
|
3142
|
+
var TIMELINE_SHELL = "\n\n## Timeline\n";
|
|
3143
|
+
function renderTrace(trace, options = {}, plan) {
|
|
3144
|
+
const { maxBlockChars, maxTotalChars, redact } = { ...DEFAULTS, ...options };
|
|
3145
|
+
const scrub = makeRedactor(redact);
|
|
3146
|
+
const clip = (text, max) => {
|
|
3147
|
+
const safe = scrub(text);
|
|
3148
|
+
if (safe.length <= max) return safe;
|
|
3149
|
+
return `${safe.slice(0, max)} [... truncated ${safe.length - max} chars ...]`;
|
|
3150
|
+
};
|
|
3151
|
+
const window = plan === void 0 ? void 0 : windowFor(trace, plan);
|
|
3152
|
+
const scoped = window !== void 0 && window.scope !== "session" ? window : void 0;
|
|
3153
|
+
const events = scoped?.events ?? trace.events;
|
|
3154
|
+
const fullHeader = scrub(
|
|
3155
|
+
[
|
|
3156
|
+
"# Session",
|
|
3157
|
+
`source: ${trace.source}`,
|
|
3158
|
+
trace.model ? `model: ${trace.model}` : void 0,
|
|
3159
|
+
`cwd: ${trace.cwd}`,
|
|
3160
|
+
trace.gitBranch ? `branch: ${trace.gitBranch}` : void 0,
|
|
3161
|
+
scoped ? `scope: ${scoped.label} \u2014 ${events.length} of ${trace.events.length} session events` : void 0,
|
|
3162
|
+
// Everything below describes what the judge can actually see. On a
|
|
3163
|
+
// scoped render the timeline is the window, so a session-wide count here
|
|
3164
|
+
// reads as evidence that the rest was truncated away — the judge would
|
|
3165
|
+
// discount a digest that is complete for what it was asked about. The
|
|
3166
|
+
// session totals stay, in parentheses, because "3 of 87" is context the
|
|
3167
|
+
// window count alone does not carry.
|
|
3168
|
+
scoped ? `turns: ${scoped.turnCount} (of ${trace.turnCount} in the session)` : `turns: ${trace.turnCount}`,
|
|
3169
|
+
(() => {
|
|
3170
|
+
const used = (scoped ?? trace).skillInvocations.map((s) => s.name);
|
|
3171
|
+
return used.length > 0 ? `skills used: ${used.join(", ")}` : "skills used: none";
|
|
3172
|
+
})(),
|
|
3173
|
+
(() => {
|
|
3174
|
+
const inWindow = new Set(events.map((e) => e.index));
|
|
3175
|
+
const spawns = trace.agentSpawns.filter(
|
|
3176
|
+
(a) => !scoped || inWindow.has(a.index)
|
|
3177
|
+
);
|
|
3178
|
+
return spawns.length > 0 ? `agents spawned: ${spawns.map((a) => a.subagentType).join(", ")}` : void 0;
|
|
3179
|
+
})(),
|
|
3180
|
+
(() => {
|
|
3181
|
+
const inWindow = new Set(events.map((e) => e.index));
|
|
3182
|
+
const branches = trace.subagentBranches.filter(
|
|
3183
|
+
(b) => !scoped || inWindow.has(b.spawnIndex)
|
|
3184
|
+
);
|
|
3185
|
+
return branches.length > 0 ? `subagent transcripts: ${branches.map((b) => `${b.agentType} (depth ${b.spawnDepth})`).join(", ")}` : void 0;
|
|
3186
|
+
})()
|
|
3187
|
+
].filter((line) => line !== void 0).join("\n")
|
|
3188
|
+
);
|
|
3189
|
+
const headerRoom = Math.max(
|
|
3190
|
+
maxTotalChars - TIMELINE_SHELL.length - MIN_TIMELINE_CHARS,
|
|
3191
|
+
0
|
|
3192
|
+
);
|
|
3193
|
+
const header = fullHeader.length <= headerRoom ? fullHeader : fullHeader.slice(
|
|
3194
|
+
0,
|
|
3195
|
+
Math.max(headerRoom - HEADER_TRUNCATED.length, 0)
|
|
3196
|
+
) + HEADER_TRUNCATED;
|
|
3197
|
+
const branchTypes = /* @__PURE__ */ new Map();
|
|
3198
|
+
for (const spawn of trace.agentSpawns) {
|
|
3199
|
+
if (spawn.toolUseId) branchTypes.set(spawn.toolUseId, spawn.subagentType);
|
|
3200
|
+
}
|
|
3201
|
+
for (const branch2 of trace.subagentBranches) {
|
|
3202
|
+
branchTypes.set(branch2.branchId, branch2.agentType);
|
|
3203
|
+
}
|
|
3204
|
+
const label = (branchId) => branchTypes.get(branchId) ?? branchId;
|
|
3205
|
+
const callAt = new Map(trace.toolCalls.map((call) => [call.index, call]));
|
|
3206
|
+
if (scoped?.empty === true) {
|
|
3207
|
+
return `${header}${TIMELINE_SHELL}[no turns: ${scrub(scoped.reason ?? "")}]`;
|
|
3208
|
+
}
|
|
3209
|
+
const lines = [];
|
|
3210
|
+
let branch;
|
|
3211
|
+
for (const event of events) {
|
|
3212
|
+
const tag = event.branchId ? `:${label(event.branchId)}` : event.sidechain ? ":sidechain" : "";
|
|
3213
|
+
let line;
|
|
3214
|
+
switch (event.kind) {
|
|
3215
|
+
case "user":
|
|
3216
|
+
if (event.text) line = `[user${tag}] ${clip(event.text, maxBlockChars)}`;
|
|
3217
|
+
break;
|
|
3218
|
+
case "assistant":
|
|
3219
|
+
if (event.text) {
|
|
3220
|
+
line = `[assistant${tag}] ${clip(event.text, maxBlockChars)}`;
|
|
3221
|
+
}
|
|
3222
|
+
break;
|
|
3223
|
+
case "tool_call": {
|
|
3224
|
+
const call = callAt.get(event.index);
|
|
3225
|
+
const input = call ? JSON.stringify(call.input) : "";
|
|
3226
|
+
line = `[tool${tag}] ${event.toolName ?? call?.name ?? "?"} ${clip(input, Math.min(maxBlockChars, 500))}`;
|
|
3227
|
+
break;
|
|
3228
|
+
}
|
|
3229
|
+
default:
|
|
3230
|
+
break;
|
|
3231
|
+
}
|
|
3232
|
+
if (line === void 0) continue;
|
|
3233
|
+
if (event.branchId !== branch) {
|
|
3234
|
+
if (branch !== void 0) lines.push(`[/subagent ${label(branch)}]`);
|
|
3235
|
+
branch = event.branchId;
|
|
3236
|
+
if (branch !== void 0) {
|
|
3237
|
+
lines.push(`[subagent ${label(branch)} (${branch})]`);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
lines.push(line);
|
|
3241
|
+
}
|
|
3242
|
+
if (branch !== void 0) lines.push(`[/subagent ${label(branch)}]`);
|
|
3243
|
+
let timeline = scrub(lines.join("\n"));
|
|
3244
|
+
const budget = Math.max(
|
|
3245
|
+
maxTotalChars - header.length - TIMELINE_SHELL.length,
|
|
3246
|
+
MIN_TIMELINE_CHARS
|
|
3247
|
+
);
|
|
3248
|
+
if (timeline.length > budget) {
|
|
3249
|
+
const marker = (omitted2) => `
|
|
3250
|
+
[... truncated ${omitted2} chars of transcript ...]
|
|
3251
|
+
`;
|
|
3252
|
+
const room = Math.max(budget - marker(timeline.length).length, 0);
|
|
3253
|
+
const headLen = Math.floor(room * 0.6);
|
|
3254
|
+
const tailLen = room - headLen;
|
|
3255
|
+
const omitted = timeline.length - headLen - tailLen;
|
|
3256
|
+
timeline = `${timeline.slice(0, headLen)}${marker(omitted)}${tailLen > 0 ? timeline.slice(-tailLen) : ""}`;
|
|
3257
|
+
}
|
|
3258
|
+
return `${header}${TIMELINE_SHELL}${timeline}`;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// src/tracevals/core/engine.ts
|
|
3262
|
+
async function runEvals(options) {
|
|
3263
|
+
const start = Date.now();
|
|
3264
|
+
const { config } = options;
|
|
3265
|
+
const trace = await parseTraceFile(options.tracePath);
|
|
3266
|
+
const refusals = [];
|
|
3267
|
+
const found = await findManifest({
|
|
3268
|
+
tracePath: options.tracePath,
|
|
3269
|
+
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
3270
|
+
projectDir: options.projectDir ?? trace.cwd,
|
|
3271
|
+
captureDir: config.capture.dir,
|
|
3272
|
+
...options.manifest !== void 0 ? { explicit: options.manifest } : {},
|
|
3273
|
+
onRefused: (path, reason) => refusals.push({ path, reason })
|
|
3274
|
+
});
|
|
3275
|
+
if (found === null && options.manifest !== void 0) {
|
|
3276
|
+
throw new TracevalsError(
|
|
3277
|
+
`no usable session manifest at ${options.manifest} \u2014 ${refusals[0]?.reason ?? "it is missing, unreadable, or was recorded for a different session"}`
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
const manifestWarnings = found === null ? refusals.map(
|
|
3281
|
+
(r) => `ignoring the session manifest at ${r.path} \u2014 ${r.reason}`
|
|
3282
|
+
) : [];
|
|
3283
|
+
const resolved = await resolveArtifacts(trace, {
|
|
3284
|
+
...options.projectDir !== void 0 ? { projectDir: options.projectDir, projectRoot: options.projectDir } : {},
|
|
3285
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
3286
|
+
reportUnusedArtifacts: config.reportUnusedArtifacts,
|
|
3287
|
+
...found !== null ? { manifest: found.manifest } : {}
|
|
3288
|
+
});
|
|
3289
|
+
const loader = options.metadataFor;
|
|
3290
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
3291
|
+
if (loader !== void 0) {
|
|
3292
|
+
for (const artifact of resolved.artifacts) {
|
|
3293
|
+
metadata.set(artifact, await loader(artifact));
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
const plans = planEvals(
|
|
3297
|
+
resolved.artifacts,
|
|
3298
|
+
loader === void 0 ? void 0 : (artifact) => metadata.get(artifact)
|
|
3299
|
+
);
|
|
3300
|
+
const results = [];
|
|
3301
|
+
const aiPlans = [];
|
|
3302
|
+
const root = normalizeRoot(process.cwd());
|
|
3303
|
+
for (const plan of plans) {
|
|
3304
|
+
const base = {
|
|
3305
|
+
evalName: plan.evalName,
|
|
3306
|
+
artifact: plan.artifact.path,
|
|
3307
|
+
artifactName: plan.artifact.name,
|
|
3308
|
+
artifactType: plan.artifact.type,
|
|
3309
|
+
grader: plan.grader,
|
|
3310
|
+
implicit: plan.implicit,
|
|
3311
|
+
location: spelled(plan.location, root)
|
|
3312
|
+
};
|
|
3313
|
+
if (plan.error !== void 0) {
|
|
3314
|
+
results.push({ ...base, outcome: "error", error: plan.error, durationMs: 0 });
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
if (plan.skipped) {
|
|
3318
|
+
results.push({
|
|
3319
|
+
...base,
|
|
3320
|
+
outcome: "skipped",
|
|
3321
|
+
skipReason: plan.skipReason ?? "eval skipped",
|
|
3322
|
+
durationMs: 0
|
|
3323
|
+
});
|
|
3324
|
+
continue;
|
|
3325
|
+
}
|
|
3326
|
+
if (plan.grader === "ai" || plan.grader === "human") {
|
|
3327
|
+
const window = windowFor(trace, plan);
|
|
3328
|
+
if (window.empty) {
|
|
3329
|
+
results.push({
|
|
3330
|
+
...base,
|
|
3331
|
+
outcome: "skipped",
|
|
3332
|
+
skipReason: window.reason ?? "the artifact governed no turns",
|
|
3333
|
+
durationMs: 0
|
|
3334
|
+
});
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
if (plan.grader === "ai") {
|
|
3339
|
+
aiPlans.push(plan);
|
|
3340
|
+
continue;
|
|
3341
|
+
}
|
|
3342
|
+
if (plan.grader === "human") {
|
|
3343
|
+
results.push({
|
|
3344
|
+
...base,
|
|
3345
|
+
outcome: "needs-review",
|
|
3346
|
+
skipReason: "awaiting human review",
|
|
3347
|
+
durationMs: 0
|
|
3348
|
+
});
|
|
3349
|
+
continue;
|
|
3350
|
+
}
|
|
3351
|
+
if (plan.grader === "command" && !config.execution.allow.includes("frontmatter-commands")) {
|
|
3352
|
+
results.push({
|
|
3353
|
+
...base,
|
|
3354
|
+
outcome: "skipped",
|
|
3355
|
+
skipReason: NOT_GRANTED_REASON,
|
|
3356
|
+
durationMs: 0
|
|
3357
|
+
});
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
const grader = graderFor(plan.grader);
|
|
3361
|
+
if (!grader) {
|
|
3362
|
+
results.push({
|
|
3363
|
+
...base,
|
|
3364
|
+
outcome: "error",
|
|
3365
|
+
error: `unknown grader kind "${plan.grader}"; the grader registry knows ${listGraderKinds().sort().join(", ")}, "ai", and "human"`,
|
|
3366
|
+
durationMs: 0
|
|
3367
|
+
});
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
const graded = Date.now();
|
|
3371
|
+
let result;
|
|
3372
|
+
try {
|
|
3373
|
+
result = await grader.grade({
|
|
3374
|
+
trace,
|
|
3375
|
+
plan,
|
|
3376
|
+
...options.projectDir !== void 0 ? { projectRoot: options.projectDir } : {}
|
|
3377
|
+
});
|
|
3378
|
+
} catch (err) {
|
|
3379
|
+
results.push({
|
|
3380
|
+
...base,
|
|
3381
|
+
outcome: "error",
|
|
3382
|
+
error: `${plan.grader}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3383
|
+
durationMs: Date.now() - graded
|
|
3384
|
+
});
|
|
3385
|
+
continue;
|
|
3386
|
+
}
|
|
3387
|
+
if (result.error !== void 0) {
|
|
3388
|
+
results.push({
|
|
3389
|
+
...base,
|
|
3390
|
+
outcome: "error",
|
|
3391
|
+
error: result.error,
|
|
3392
|
+
durationMs: Date.now() - graded
|
|
3393
|
+
});
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
if (result.skipped !== void 0) {
|
|
3397
|
+
results.push({
|
|
3398
|
+
...base,
|
|
3399
|
+
outcome: "skipped",
|
|
3400
|
+
skipReason: result.skipped,
|
|
3401
|
+
durationMs: Date.now() - graded
|
|
3402
|
+
});
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
const failing2 = result.findings.some((f) => f.severity === "error");
|
|
3406
|
+
results.push({
|
|
3407
|
+
...base,
|
|
3408
|
+
outcome: failing2 ? "fail" : "pass",
|
|
3409
|
+
findings: result.findings,
|
|
3410
|
+
durationMs: Date.now() - graded
|
|
3411
|
+
});
|
|
3412
|
+
}
|
|
3413
|
+
if (aiPlans.length > 0) {
|
|
3414
|
+
if (options.deterministicOnly || options.judge === void 0) {
|
|
3415
|
+
const skipReason = options.deterministicOnly ? "ai evals skipped (deterministic-only run)" : "ai evals skipped (no judge provided)";
|
|
3416
|
+
for (const plan of aiPlans) {
|
|
3417
|
+
results.push({
|
|
3418
|
+
evalName: plan.evalName,
|
|
3419
|
+
artifact: plan.artifact.path,
|
|
3420
|
+
artifactName: plan.artifact.name,
|
|
3421
|
+
artifactType: plan.artifact.type,
|
|
3422
|
+
grader: plan.grader,
|
|
3423
|
+
implicit: plan.implicit,
|
|
3424
|
+
location: spelled(plan.location, root),
|
|
3425
|
+
outcome: "skipped",
|
|
3426
|
+
skipReason,
|
|
3427
|
+
durationMs: 0
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
} else {
|
|
3431
|
+
const judged = await options.judge(
|
|
3432
|
+
aiPlans,
|
|
3433
|
+
(plan) => renderTrace(
|
|
3434
|
+
trace,
|
|
3435
|
+
{ ...config.render, redact: config.judge.redact },
|
|
3436
|
+
plan
|
|
3437
|
+
),
|
|
3438
|
+
{
|
|
3439
|
+
trace,
|
|
3440
|
+
...options.projectDir !== void 0 ? { projectRoot: options.projectDir } : {}
|
|
3441
|
+
}
|
|
3442
|
+
);
|
|
3443
|
+
judged.forEach((j, i) => {
|
|
3444
|
+
const plan = aiPlans[i];
|
|
3445
|
+
results.push({
|
|
3446
|
+
...j.selfPreference ? { selfPreference: j.selfPreference } : {},
|
|
3447
|
+
evalName: j.evalName,
|
|
3448
|
+
artifact: j.artifact,
|
|
3449
|
+
artifactName: j.artifactName,
|
|
3450
|
+
artifactType: plan?.artifact.type ?? "skill",
|
|
3451
|
+
grader: j.grader,
|
|
3452
|
+
implicit: j.implicit,
|
|
3453
|
+
location: spelled(plan?.location ?? { file: j.artifact }, root),
|
|
3454
|
+
outcome: j.outcome,
|
|
3455
|
+
...j.consensus !== void 0 ? { consensus: j.consensus } : {},
|
|
3456
|
+
...j.skipReason !== void 0 ? { skipReason: j.skipReason } : {},
|
|
3457
|
+
...j.error !== void 0 ? { error: j.error } : {},
|
|
3458
|
+
turns: j.turns,
|
|
3459
|
+
durationMs: j.durationMs
|
|
3460
|
+
});
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
const weightOf = /* @__PURE__ */ new Map();
|
|
3465
|
+
for (const plan of plans) {
|
|
3466
|
+
weightOf.set(weightKey(plan.artifact.path, plan.evalName), plan.weight ?? 1);
|
|
3467
|
+
}
|
|
3468
|
+
for (const r of results) {
|
|
3469
|
+
r.weight = weightOf.get(weightKey(r.artifact, r.evalName)) ?? 1;
|
|
3470
|
+
}
|
|
3471
|
+
const gradedRows = results.filter(
|
|
3472
|
+
(r) => r.outcome === "pass" || r.outcome === "fail" || r.outcome === "error"
|
|
3473
|
+
);
|
|
3474
|
+
const gradedWeight = gradedRows.reduce((n, r) => n + (r.weight ?? 1), 0);
|
|
3475
|
+
const summary = {
|
|
3476
|
+
total: results.length,
|
|
3477
|
+
pass: results.filter((r) => r.outcome === "pass").length,
|
|
3478
|
+
fail: results.filter((r) => r.outcome === "fail").length,
|
|
3479
|
+
error: results.filter((r) => r.outcome === "error").length,
|
|
3480
|
+
needsReview: results.filter((r) => r.outcome === "needs-review").length,
|
|
3481
|
+
skipped: results.filter((r) => r.outcome === "skipped").length,
|
|
3482
|
+
passRate: gradedWeight > 0 ? gradedRows.filter((r) => r.outcome === "pass").reduce((n, r) => n + (r.weight ?? 1), 0) / gradedWeight : 1
|
|
3483
|
+
};
|
|
3484
|
+
const failing = summary.fail > 0 || summary.error > 0 || config.failOnReview && summary.needsReview > 0;
|
|
3485
|
+
return {
|
|
3486
|
+
trace: {
|
|
3487
|
+
file: trace.file,
|
|
3488
|
+
source: trace.source,
|
|
3489
|
+
...trace.sessionId !== void 0 ? { sessionId: trace.sessionId } : {},
|
|
3490
|
+
cwd: trace.cwd,
|
|
3491
|
+
...trace.model !== void 0 ? { model: trace.model } : {},
|
|
3492
|
+
turnCount: trace.turnCount
|
|
3493
|
+
},
|
|
3494
|
+
warnings: [
|
|
3495
|
+
...options.warnings ?? [],
|
|
3496
|
+
...trace.warnings,
|
|
3497
|
+
...manifestWarnings,
|
|
3498
|
+
...resolved.warnings,
|
|
3499
|
+
...vanityMetricWarnings(plans),
|
|
3500
|
+
...truncatedArtifactWarnings(plans)
|
|
3501
|
+
],
|
|
3502
|
+
coverage: resolved.coverage,
|
|
3503
|
+
// An observation about the session's configuration, never a verdict: it is
|
|
3504
|
+
// deliberately not part of `summary` and never moves `exitCode`.
|
|
3505
|
+
availability: resolved.availability,
|
|
3506
|
+
...found !== null ? { manifest: summariseManifest(found, resolved.coverage) } : {},
|
|
3507
|
+
evalResults: results,
|
|
3508
|
+
summary,
|
|
3509
|
+
exitCode: failing ? 1 : 0,
|
|
3510
|
+
turns: results.reduce((sum, r) => sum + (r.turns ?? 0), 0),
|
|
3511
|
+
durationMs: Date.now() - start
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function spelled(location, root) {
|
|
3515
|
+
if (classifyRef(location.file).kind === "url") return location;
|
|
3516
|
+
const posix = toPosix(location.file);
|
|
3517
|
+
return { ...location, file: underRoot(posix, root) ?? posix };
|
|
3518
|
+
}
|
|
3519
|
+
function weightKey(artifact, evalName) {
|
|
3520
|
+
return `${artifact}\0${evalName}`;
|
|
3521
|
+
}
|
|
3522
|
+
function vanityMetricWarnings(plans) {
|
|
3523
|
+
const byArtifact = /* @__PURE__ */ new Map();
|
|
3524
|
+
for (const plan of plans) {
|
|
3525
|
+
if (plan.implicit) continue;
|
|
3526
|
+
const list = byArtifact.get(plan.artifact.path) ?? [];
|
|
3527
|
+
list.push(plan);
|
|
3528
|
+
byArtifact.set(plan.artifact.path, list);
|
|
3529
|
+
}
|
|
3530
|
+
const warnings = [];
|
|
3531
|
+
for (const [path, group] of byArtifact) {
|
|
3532
|
+
if (group.length > 0 && group.every((p) => p.grader === "skill-invoked")) {
|
|
3533
|
+
warnings.push(
|
|
3534
|
+
`${path}: every eval here is skill-invoked, which checks that the skill fired, not that the session followed it. Add at least one eval about what the session actually did.`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
return warnings;
|
|
3539
|
+
}
|
|
3540
|
+
function truncatedArtifactWarnings(plans) {
|
|
3541
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3542
|
+
const warnings = [];
|
|
3543
|
+
for (const plan of plans) {
|
|
3544
|
+
if (plan.grader !== "ai") continue;
|
|
3545
|
+
const path = plan.artifact.path;
|
|
3546
|
+
if (seen.has(path)) continue;
|
|
3547
|
+
if (!artifactWasTruncated(plan.artifact.content)) continue;
|
|
3548
|
+
seen.add(path);
|
|
3549
|
+
warnings.push(
|
|
3550
|
+
`${path}: only the first ${String(MAX_ARTIFACT_CHARS)} characters were shown to the judge (the artifact is ${String(plan.artifact.content.length)}). Verdicts about instructions beyond that point rest on text the judge never read.`
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
return warnings;
|
|
3554
|
+
}
|
|
3555
|
+
function summariseManifest(found, coverage) {
|
|
3556
|
+
const checked = coverage.filter((c) => c.contentCheck !== void 0);
|
|
3557
|
+
return {
|
|
3558
|
+
path: found.path,
|
|
3559
|
+
sessionId: found.manifest.sessionId,
|
|
3560
|
+
capturedAt: found.manifest.capturedAt,
|
|
3561
|
+
...found.manifest.git !== void 0 ? { gitSha: found.manifest.git.sha } : {},
|
|
3562
|
+
matched: checked.filter((c) => c.contentCheck?.status === "match").length,
|
|
3563
|
+
changed: checked.filter((c) => c.contentCheck?.status === "mismatch").length,
|
|
3564
|
+
unrecorded: checked.filter((c) => c.contentCheck?.status === "skipped").length
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
// src/tracevals/evals/external.ts
|
|
3569
|
+
import { relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
3570
|
+
var METADATA_KEY = "metadata";
|
|
3571
|
+
var MARKED = /* @__PURE__ */ new Set([METADATA_KEY]);
|
|
3572
|
+
var toPosix2 = (path) => path.split(sep2).join("/");
|
|
3573
|
+
function twoManifestsRefusal(label, a, b) {
|
|
3574
|
+
return `${label} is in collections ${a} and ${b}, and both keep ${METADATA_KEY} in a manifest.`;
|
|
3575
|
+
}
|
|
3576
|
+
function urlManifestRefusal(label, file) {
|
|
3577
|
+
return `${label}: ${METADATA_KEY} is owned by ${file}, and a URL manifest cannot be written. Vendor it to a path, or run fill with --dry-run.`;
|
|
3578
|
+
}
|
|
3579
|
+
function ownsMetadata(collection, manifest) {
|
|
3580
|
+
return ownsKey(collection, manifest, METADATA_KEY, MARKED);
|
|
3581
|
+
}
|
|
3582
|
+
function loadExternalEvals(opts) {
|
|
3583
|
+
const scoped = [];
|
|
3584
|
+
for (const collection of opts.collections) {
|
|
3585
|
+
const owning = collection.externalMetadata.filter((m) => ownsMetadata(collection, m));
|
|
3586
|
+
if (owning.length === 0) continue;
|
|
3587
|
+
scoped.push({ ...collection, externalMetadata: owning });
|
|
3588
|
+
}
|
|
3589
|
+
return Promise.resolve(scoped.length === 0 ? null : reader(scoped, opts));
|
|
3590
|
+
}
|
|
3591
|
+
function reader(scoped, opts) {
|
|
3592
|
+
const { configDir } = opts;
|
|
3593
|
+
const base = configDir;
|
|
3594
|
+
const declared = opts.collections;
|
|
3595
|
+
const marks = familyMarks({ configPath: opts.configPath, configDir, cwd: base });
|
|
3596
|
+
const perPage = scoped.some(
|
|
3597
|
+
(c) => c.externalMetadata.some((m) => hasPagePlaceholder(m.file))
|
|
3598
|
+
);
|
|
3599
|
+
const load = async (pages) => {
|
|
3600
|
+
try {
|
|
3601
|
+
return await loadExternalMetadata(scoped, {
|
|
3602
|
+
configDir,
|
|
3603
|
+
base,
|
|
3604
|
+
...opts.offline === void 0 ? {} : { offline: opts.offline },
|
|
3605
|
+
...pages === void 0 ? {} : { pages }
|
|
3606
|
+
});
|
|
3607
|
+
} catch (err) {
|
|
3608
|
+
if (err instanceof DocmetaError) throw new TracevalsError(err.message);
|
|
3609
|
+
throw err;
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
let shared;
|
|
3613
|
+
const own = /* @__PURE__ */ new Map();
|
|
3614
|
+
const indexFor = (abs) => {
|
|
3615
|
+
if (!perPage) return shared ??= load();
|
|
3616
|
+
let index = own.get(abs);
|
|
3617
|
+
if (index === void 0) {
|
|
3618
|
+
index = load([abs]);
|
|
3619
|
+
own.set(abs, index);
|
|
3620
|
+
}
|
|
3621
|
+
return index;
|
|
3622
|
+
};
|
|
3623
|
+
const forArtifact = async (artifact) => {
|
|
3624
|
+
const label = artifact.path;
|
|
3625
|
+
const extracted = extractFrontmatter(artifact.content, "markdown");
|
|
3626
|
+
const members = memberOf(declared, configDir, base, label);
|
|
3627
|
+
const [first, second] = scoped.filter((c) => members.includes(c.name));
|
|
3628
|
+
if (first === void 0) return { extracted };
|
|
3629
|
+
if (second !== void 0) {
|
|
3630
|
+
throw new TracevalsError(twoManifestsRefusal(label, first.name, second.name));
|
|
3631
|
+
}
|
|
3632
|
+
const merged = await mergeWithMarks(
|
|
3633
|
+
label,
|
|
3634
|
+
extracted,
|
|
3635
|
+
await indexFor(resolve3(base, label)),
|
|
3636
|
+
[first.name],
|
|
3637
|
+
base,
|
|
3638
|
+
{ encryptionKey: () => opts.key, marks: await marks() }
|
|
3639
|
+
);
|
|
3640
|
+
const locate2 = (pointer) => {
|
|
3641
|
+
const found = merged.locate(pointer);
|
|
3642
|
+
if (found === void 0 || classifyRef(found.file).kind === "url") return found;
|
|
3643
|
+
return { ...found, file: resolve3(base, found.file) };
|
|
3644
|
+
};
|
|
3645
|
+
const at = merged.locate(`/${METADATA_KEY}`);
|
|
3646
|
+
const [declaration] = first.externalMetadata;
|
|
3647
|
+
if (at === void 0 || declaration === void 0) {
|
|
3648
|
+
return { extracted: merged.extracted, locate: locate2 };
|
|
3649
|
+
}
|
|
3650
|
+
const url = classifyRef(at.file).kind === "url";
|
|
3651
|
+
const owner = {
|
|
3652
|
+
collection: first.name,
|
|
3653
|
+
path: url ? at.file : resolve3(base, at.file),
|
|
3654
|
+
file: at.file,
|
|
3655
|
+
join: externalMetadataJoin(declaration),
|
|
3656
|
+
url
|
|
3657
|
+
};
|
|
3658
|
+
const out = { extracted: merged.extracted, locate: locate2, owner };
|
|
3659
|
+
if (owner.join === PATH_JOIN) {
|
|
3660
|
+
out.entry = toPosix2(relative3(configDir, resolve3(base, label)));
|
|
3661
|
+
} else {
|
|
3662
|
+
const value = merged.joins.find((j) => j.field === owner.join)?.value ?? extracted.data[owner.join];
|
|
3663
|
+
if (typeof value === "string" && value !== "") out.entry = value;
|
|
3664
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
3665
|
+
out.entry = String(value);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
return out;
|
|
3669
|
+
};
|
|
3670
|
+
return { configDir, forArtifact };
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// src/tracevals/graders/plugins.ts
|
|
3674
|
+
import { createRequire } from "node:module";
|
|
3675
|
+
import { isAbsolute as isAbsolute3, join as join2, resolve as resolve4 } from "node:path";
|
|
3676
|
+
import { pathToFileURL } from "node:url";
|
|
3677
|
+
var alreadyLoaded = /* @__PURE__ */ new Set();
|
|
3678
|
+
var RELATIVE = /^\.\.?[/\\]/;
|
|
3679
|
+
function locate(specifier, configDir) {
|
|
3680
|
+
if (specifier.startsWith("file:")) return { href: specifier };
|
|
3681
|
+
if (RELATIVE.test(specifier) || isAbsolute3(specifier)) {
|
|
3682
|
+
const path = resolve4(configDir, specifier);
|
|
3683
|
+
return { href: pathToFileURL(path).href, shown: path };
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
const path = createRequire(join2(resolve4(configDir), "package.json")).resolve(
|
|
3687
|
+
specifier
|
|
3688
|
+
);
|
|
3689
|
+
return { href: pathToFileURL(path).href, shown: path };
|
|
3690
|
+
} catch {
|
|
3691
|
+
return { href: specifier };
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
function registrarIn(mod) {
|
|
3695
|
+
if (typeof mod.register === "function") {
|
|
3696
|
+
return mod.register;
|
|
3697
|
+
}
|
|
3698
|
+
if (typeof mod.default === "function") {
|
|
3699
|
+
return mod.default;
|
|
3700
|
+
}
|
|
3701
|
+
return void 0;
|
|
3702
|
+
}
|
|
3703
|
+
function snapshot() {
|
|
3704
|
+
const registered = /* @__PURE__ */ new Map();
|
|
3705
|
+
for (const kind of listGraderKinds()) {
|
|
3706
|
+
const grader = graderFor(kind);
|
|
3707
|
+
if (grader !== void 0) registered.set(kind, grader);
|
|
3708
|
+
}
|
|
3709
|
+
return registered;
|
|
3710
|
+
}
|
|
3711
|
+
function changesSince(specifier, before) {
|
|
3712
|
+
const added = [];
|
|
3713
|
+
const replaced = [];
|
|
3714
|
+
for (const kind of listGraderKinds()) {
|
|
3715
|
+
const previous = before.get(kind);
|
|
3716
|
+
if (previous === void 0) added.push(kind);
|
|
3717
|
+
else if (previous !== graderFor(kind)) replaced.push(kind);
|
|
3718
|
+
}
|
|
3719
|
+
const warnings = [];
|
|
3720
|
+
if (added.length === 0 && replaced.length === 0) {
|
|
3721
|
+
warnings.push(
|
|
3722
|
+
`grader plugin "${specifier}" registered no grader kinds \u2014 export a \`register({ registerGrader })\` function, or call registerGrader while the module is imported. Any eval naming a custom grader will report \`unknown grader kind\`.`
|
|
3723
|
+
);
|
|
3724
|
+
}
|
|
3725
|
+
for (const kind of replaced) {
|
|
3726
|
+
warnings.push(
|
|
3727
|
+
BUILTIN_GRADER_KINDS.has(kind) ? `grader plugin "${specifier}" replaced the built-in grader "${kind}"; every eval declaring that kind now runs the plugin's implementation` : `grader plugin "${specifier}" replaced the grader "${kind}" registered by an earlier plugin`
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3730
|
+
return warnings;
|
|
3731
|
+
}
|
|
3732
|
+
async function loadGraderPlugins(options) {
|
|
3733
|
+
const loaded = [];
|
|
3734
|
+
const warnings = [];
|
|
3735
|
+
for (const specifier of options.plugins) {
|
|
3736
|
+
const { href, shown } = locate(specifier, options.configDir);
|
|
3737
|
+
if (alreadyLoaded.has(href)) continue;
|
|
3738
|
+
const before = snapshot();
|
|
3739
|
+
let mod;
|
|
3740
|
+
try {
|
|
3741
|
+
mod = await import(href);
|
|
3742
|
+
} catch (err) {
|
|
3743
|
+
throw new TracevalsError(
|
|
3744
|
+
`could not load grader plugin "${specifier}"` + (shown !== void 0 ? ` (resolved to ${shown})` : "") + `: ${err instanceof Error ? err.message : String(err)}`
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
const register = registrarIn(mod);
|
|
3748
|
+
if (register !== void 0) {
|
|
3749
|
+
try {
|
|
3750
|
+
await register({ registerGrader });
|
|
3751
|
+
} catch (err) {
|
|
3752
|
+
throw new TracevalsError(
|
|
3753
|
+
`grader plugin "${specifier}" threw while registering: ` + (err instanceof Error ? err.message : String(err))
|
|
3754
|
+
);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
alreadyLoaded.add(href);
|
|
3758
|
+
warnings.push(...changesSince(specifier, before));
|
|
3759
|
+
loaded.push(specifier);
|
|
3760
|
+
}
|
|
3761
|
+
return { loaded, warnings };
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
// src/tracevals/history.ts
|
|
3765
|
+
import { appendFile, mkdir, readFile as readFile2 } from "node:fs/promises";
|
|
3766
|
+
import { dirname as dirname2 } from "node:path";
|
|
3767
|
+
function entryFor(report) {
|
|
3768
|
+
return {
|
|
3769
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3770
|
+
traceFile: report.trace.file,
|
|
3771
|
+
...report.trace.sessionId !== void 0 ? { sessionId: report.trace.sessionId } : {},
|
|
3772
|
+
project: report.trace.cwd,
|
|
3773
|
+
summary: report.summary,
|
|
3774
|
+
exitCode: report.exitCode,
|
|
3775
|
+
turns: report.turns,
|
|
3776
|
+
evals: report.evalResults.map((r) => ({
|
|
3777
|
+
artifact: r.artifact,
|
|
3778
|
+
evalName: r.evalName,
|
|
3779
|
+
outcome: r.outcome
|
|
3780
|
+
}))
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
async function appendHistory(file, report) {
|
|
3784
|
+
try {
|
|
3785
|
+
await mkdir(dirname2(file), { recursive: true });
|
|
3786
|
+
await appendFile(file, `${JSON.stringify(entryFor(report))}
|
|
3787
|
+
`, "utf-8");
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
warn(
|
|
3790
|
+
`could not write history at ${file} (${errorMessage(err)}). Continuing.`
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
async function loadHistory(file) {
|
|
3795
|
+
let content;
|
|
3796
|
+
try {
|
|
3797
|
+
content = await readFile2(file, "utf-8");
|
|
3798
|
+
} catch {
|
|
3799
|
+
return [];
|
|
3800
|
+
}
|
|
3801
|
+
const entries = [];
|
|
3802
|
+
for (const line of content.split("\n")) {
|
|
3803
|
+
const trimmed = line.trim();
|
|
3804
|
+
if (!trimmed) continue;
|
|
3805
|
+
try {
|
|
3806
|
+
entries.push(JSON.parse(trimmed));
|
|
3807
|
+
} catch {
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return entries;
|
|
3812
|
+
}
|
|
3813
|
+
var GOOD = /* @__PURE__ */ new Set(["pass"]);
|
|
3814
|
+
var BAD = /* @__PURE__ */ new Set(["fail", "error"]);
|
|
3815
|
+
function compareToLast(history, report) {
|
|
3816
|
+
const key = report.trace.sessionId ?? report.trace.file;
|
|
3817
|
+
const previous = [...history].reverse().find((e) => (e.sessionId ?? e.traceFile) === key);
|
|
3818
|
+
if (!previous) return null;
|
|
3819
|
+
const keyOf = (e) => `${e.artifact}::${e.evalName}`;
|
|
3820
|
+
const before = new Map(previous.evals.map((e) => [keyOf(e), e]));
|
|
3821
|
+
const current = entryFor(report).evals;
|
|
3822
|
+
const currentKeys = new Set(current.map(keyOf));
|
|
3823
|
+
const regressions = [];
|
|
3824
|
+
const improvements = [];
|
|
3825
|
+
const added = [];
|
|
3826
|
+
for (const e of current) {
|
|
3827
|
+
const prior = before.get(keyOf(e));
|
|
3828
|
+
if (!prior) {
|
|
3829
|
+
added.push(e.evalName);
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3832
|
+
if (GOOD.has(prior.outcome) && BAD.has(e.outcome)) regressions.push(e);
|
|
3833
|
+
if (BAD.has(prior.outcome) && GOOD.has(e.outcome)) improvements.push(e);
|
|
3834
|
+
}
|
|
3835
|
+
const removed = previous.evals.filter((e) => !currentKeys.has(keyOf(e))).map((e) => e.evalName);
|
|
3836
|
+
return {
|
|
3837
|
+
previousTimestamp: previous.timestamp,
|
|
3838
|
+
regressions,
|
|
3839
|
+
improvements,
|
|
3840
|
+
added,
|
|
3841
|
+
removed
|
|
3842
|
+
};
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
// src/tracevals/judge/provider.ts
|
|
3846
|
+
import {
|
|
3847
|
+
makeProvider as makeInferenceProvider,
|
|
3848
|
+
mockVerdict
|
|
3849
|
+
} from "@hawkeyexl/inference";
|
|
3850
|
+
var CONFIG_KEY = "tracevals.provider";
|
|
3851
|
+
var toTracevalsError = (message) => new TracevalsError(message);
|
|
3852
|
+
function selectProvider2(config, flags = {}, ev = {}) {
|
|
3853
|
+
assertLocalFlag(flags, toTracevalsError);
|
|
3854
|
+
return selectProvider(flags, [
|
|
3855
|
+
{
|
|
3856
|
+
...ev.provider !== void 0 ? { provider: ev.provider } : {},
|
|
3857
|
+
...ev.model !== void 0 ? { model: ev.model } : {},
|
|
3858
|
+
...ev.origin !== void 0 ? { origin: ev.origin } : {}
|
|
3859
|
+
},
|
|
3860
|
+
{
|
|
3861
|
+
...config.provider !== null ? { provider: config.provider } : {},
|
|
3862
|
+
...config.model !== null ? { model: config.model } : {},
|
|
3863
|
+
origin: CONFIG_KEY
|
|
3864
|
+
},
|
|
3865
|
+
{
|
|
3866
|
+
...config.providers.provider !== void 0 ? { provider: config.providers.provider } : {},
|
|
3867
|
+
...config.providers.model !== void 0 ? { model: config.providers.model } : {},
|
|
3868
|
+
origin: "providers.provider"
|
|
3869
|
+
}
|
|
3870
|
+
]);
|
|
3871
|
+
}
|
|
3872
|
+
function announceSelection(selection) {
|
|
3873
|
+
const message = localNotice(selection);
|
|
3874
|
+
if (message !== void 0) noticeOnce(message);
|
|
3875
|
+
}
|
|
3876
|
+
function assertProviderSelection(selection) {
|
|
3877
|
+
assertKnownProvider(selection.provider, toTracevalsError);
|
|
3878
|
+
assertModelHasProvider(selection.provider, selection.model, CONFIG_KEY, toTracevalsError);
|
|
3879
|
+
}
|
|
3880
|
+
function providerSpecFor2(config, selection, options = {}) {
|
|
3881
|
+
const spec = providerSpecFor(config.providers, selection);
|
|
3882
|
+
switch (selection.provider) {
|
|
3883
|
+
case "anthropic":
|
|
3884
|
+
return { ...spec, anthropic: { toolName: "record_verdict" } };
|
|
3885
|
+
case "openai":
|
|
3886
|
+
return { ...spec, openai: { schemaName: "verdict" } };
|
|
3887
|
+
case "mock":
|
|
3888
|
+
return {
|
|
3889
|
+
...spec,
|
|
3890
|
+
mockResponses: options.mockResponses ?? [mockVerdict("pass", 0.95)]
|
|
3891
|
+
};
|
|
3892
|
+
default:
|
|
3893
|
+
return spec;
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
async function resolveProviderIdentity(config, flags = {}, ev = {}) {
|
|
3897
|
+
const selection = selectProvider2(config, flags, ev);
|
|
3898
|
+
const { provider, model } = selection;
|
|
3899
|
+
assertKnownProvider(provider, toTracevalsError);
|
|
3900
|
+
assertModelHasProvider(provider, model, CONFIG_KEY, toTracevalsError);
|
|
3901
|
+
announceSelection(selection);
|
|
3902
|
+
return resolveIdentity(
|
|
3903
|
+
providerSpecFor2(config, { provider, model: model ?? null }),
|
|
3904
|
+
toTracevalsError
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
function constructProvider(config, identity, options = {}) {
|
|
3908
|
+
try {
|
|
3909
|
+
return makeInferenceProvider(providerSpecFor2(config, identity, options));
|
|
3910
|
+
} catch (e) {
|
|
3911
|
+
throw new TracevalsError(errorMessage(e));
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
async function makeJudgeProvider(config, flags = {}, ev = {}, options = {}) {
|
|
3915
|
+
return constructProvider(
|
|
3916
|
+
config,
|
|
3917
|
+
await resolveProviderIdentity(config, flags, ev),
|
|
3918
|
+
options
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
// src/tracevals/judge/trace-judge.ts
|
|
3923
|
+
import {
|
|
3924
|
+
computeConsensus,
|
|
3925
|
+
runEnsemble,
|
|
3926
|
+
zoneFor
|
|
3927
|
+
} from "@hawkeyexl/inference";
|
|
3928
|
+
|
|
3929
|
+
// src/tracevals/judge/verdict-schema.json
|
|
3930
|
+
var verdict_schema_default = {
|
|
3931
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
3932
|
+
$id: "manni:tracevals:verdict",
|
|
3933
|
+
title: "manni tracevals judge verdict",
|
|
3934
|
+
type: "object",
|
|
3935
|
+
required: ["claim", "observed", "match", "confidence", "reasoning"],
|
|
3936
|
+
properties: {
|
|
3937
|
+
claim: {
|
|
3938
|
+
type: "string",
|
|
3939
|
+
description: "The specific instruction or assertion under evaluation."
|
|
3940
|
+
},
|
|
3941
|
+
observed: {
|
|
3942
|
+
type: "string",
|
|
3943
|
+
description: "What actually happened in the session, citing specific messages, tool calls, or instructions followed/violated."
|
|
3944
|
+
},
|
|
3945
|
+
match: {
|
|
3946
|
+
enum: ["pass", "fail", "partial"],
|
|
3947
|
+
description: "pass only if the session fully adhered; partial for partial adherence."
|
|
3948
|
+
},
|
|
3949
|
+
confidence: {
|
|
3950
|
+
type: "number",
|
|
3951
|
+
minimum: 0,
|
|
3952
|
+
maximum: 1,
|
|
3953
|
+
description: "Self-reported confidence in this verdict."
|
|
3954
|
+
},
|
|
3955
|
+
reasoning: {
|
|
3956
|
+
type: "string",
|
|
3957
|
+
description: "Why this conclusion was reached."
|
|
3958
|
+
}
|
|
3959
|
+
},
|
|
3960
|
+
additionalProperties: false
|
|
3961
|
+
};
|
|
3962
|
+
|
|
3963
|
+
// src/tracevals/judge/cache.ts
|
|
3964
|
+
import { JsonCache, buildCacheKey, sha256 } from "@hawkeyexl/inference";
|
|
3965
|
+
var VerdictCache = class _VerdictCache extends JsonCache {
|
|
3966
|
+
/** An ensemble is cacheable only if every run produced a verdict. */
|
|
3967
|
+
static usable(runs) {
|
|
3968
|
+
return runs.every((r) => r.error === void 0 && r.verdict !== void 0);
|
|
3969
|
+
}
|
|
3970
|
+
set(key, value) {
|
|
3971
|
+
if (!_VerdictCache.usable(value)) return;
|
|
3972
|
+
super.set(key, value);
|
|
3973
|
+
}
|
|
3974
|
+
get(key) {
|
|
3975
|
+
const hit = super.get(key);
|
|
3976
|
+
if (hit === void 0) return void 0;
|
|
3977
|
+
return _VerdictCache.usable(hit) ? hit : void 0;
|
|
3978
|
+
}
|
|
3979
|
+
};
|
|
3980
|
+
function cacheKey(provider, model, runs, temperature, renderedTrace, plan, promptVersion = PROMPT_VERSION) {
|
|
3981
|
+
const planFingerprint = JSON.stringify({
|
|
3982
|
+
assertion: plan.assertion,
|
|
3983
|
+
evidence: plan.evidence,
|
|
3984
|
+
examples: plan.examples,
|
|
3985
|
+
artifact: plan.artifact.content
|
|
3986
|
+
});
|
|
3987
|
+
return buildCacheKey([
|
|
3988
|
+
provider,
|
|
3989
|
+
model,
|
|
3990
|
+
`v${promptVersion}`,
|
|
3991
|
+
`r${runs}`,
|
|
3992
|
+
`t${temperature}`,
|
|
3993
|
+
// Pre-hashed: traces and artifacts are large, and key parts stay short.
|
|
3994
|
+
sha256(renderedTrace),
|
|
3995
|
+
sha256(planFingerprint)
|
|
3996
|
+
]);
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
// src/tracevals/judge/trace-judge.ts
|
|
4000
|
+
var verdictSchema = verdict_schema_default;
|
|
4001
|
+
function makeTraceJudge(options) {
|
|
4002
|
+
const provider = options.provider;
|
|
4003
|
+
const runsFor = (plan) => options.runs ?? plan.runs ?? 3;
|
|
4004
|
+
const temperature = options.temperature ?? 0;
|
|
4005
|
+
const zones = options.zones ?? { autoPass: 0.8, autoFail: 0.8 };
|
|
4006
|
+
const cache = new VerdictCache(
|
|
4007
|
+
options.cacheDir ?? ".manni/tracevals/cache",
|
|
4008
|
+
options.noCache !== true && options.cacheDir !== void 0,
|
|
4009
|
+
"manni-tracevals"
|
|
4010
|
+
);
|
|
4011
|
+
const maxTurns = options.maxTurns ?? null;
|
|
4012
|
+
let turnsSpent = 0;
|
|
4013
|
+
return async (plans, renderFor, context) => {
|
|
4014
|
+
const trace = context?.trace;
|
|
4015
|
+
const sessionModel = trace?.model;
|
|
4016
|
+
const results = [];
|
|
4017
|
+
for (const plan of plans) {
|
|
4018
|
+
const start = Date.now();
|
|
4019
|
+
const base = {
|
|
4020
|
+
evalName: plan.evalName,
|
|
4021
|
+
artifact: plan.artifact.path,
|
|
4022
|
+
artifactName: plan.artifact.name,
|
|
4023
|
+
grader: plan.grader,
|
|
4024
|
+
implicit: plan.implicit
|
|
4025
|
+
};
|
|
4026
|
+
let evalProvider = provider;
|
|
4027
|
+
if (plan.provider !== void 0 || plan.model !== void 0) {
|
|
4028
|
+
const resolved = await resolveOverride(plan, options);
|
|
4029
|
+
if ("error" in resolved) {
|
|
4030
|
+
results.push({
|
|
4031
|
+
...base,
|
|
4032
|
+
outcome: "error",
|
|
4033
|
+
error: resolved.error,
|
|
4034
|
+
turns: 0,
|
|
4035
|
+
durationMs: Date.now() - start
|
|
4036
|
+
});
|
|
4037
|
+
continue;
|
|
4038
|
+
}
|
|
4039
|
+
evalProvider = resolved.provider;
|
|
4040
|
+
}
|
|
4041
|
+
const renderedTrace = renderFor(plan);
|
|
4042
|
+
const selected = trace === void 0 ? plan.target === void 0 || plan.target === "transcript" || plan.target === "artifact" ? {
|
|
4043
|
+
ok: true,
|
|
4044
|
+
text: plan.target === "artifact" ? plan.artifact.content : renderedTrace,
|
|
4045
|
+
label: plan.target === "artifact" ? "artifact" : "transcript"
|
|
4046
|
+
} : {
|
|
4047
|
+
ok: false,
|
|
4048
|
+
reason: `target "${describeTarget(plan.target)}" needs the parsed session, which this run did not supply`
|
|
4049
|
+
} : readTarget(plan.target, {
|
|
4050
|
+
trace,
|
|
4051
|
+
renderedTrace,
|
|
4052
|
+
artifactContent: plan.artifact.content,
|
|
4053
|
+
root: context?.projectRoot ?? trace.cwd
|
|
4054
|
+
});
|
|
4055
|
+
if (!selected.ok) {
|
|
4056
|
+
results.push({
|
|
4057
|
+
...base,
|
|
4058
|
+
outcome: "error",
|
|
4059
|
+
error: selected.reason,
|
|
4060
|
+
turns: 0,
|
|
4061
|
+
durationMs: Date.now() - start
|
|
4062
|
+
});
|
|
4063
|
+
continue;
|
|
4064
|
+
}
|
|
4065
|
+
const runsPerEval = runsFor(plan);
|
|
4066
|
+
const key = cacheKey(
|
|
4067
|
+
evalProvider.provider(),
|
|
4068
|
+
evalProvider.modelName(),
|
|
4069
|
+
runsPerEval,
|
|
4070
|
+
temperature,
|
|
4071
|
+
// The selected bytes, not always the transcript: two targets on one
|
|
4072
|
+
// session are two different questions and must not share a verdict.
|
|
4073
|
+
selected.text,
|
|
4074
|
+
plan
|
|
4075
|
+
);
|
|
4076
|
+
const cached = cache.get(key) !== void 0;
|
|
4077
|
+
if (maxTurns !== null && !cached) {
|
|
4078
|
+
if (turnsSpent + runsPerEval > maxTurns) {
|
|
4079
|
+
results.push({
|
|
4080
|
+
...base,
|
|
4081
|
+
outcome: "skipped",
|
|
4082
|
+
skipReason: turnBudgetSkipReason(maxTurns),
|
|
4083
|
+
turns: 0,
|
|
4084
|
+
durationMs: 0
|
|
4085
|
+
});
|
|
4086
|
+
continue;
|
|
4087
|
+
}
|
|
4088
|
+
turnsSpent += runsPerEval;
|
|
4089
|
+
}
|
|
4090
|
+
const runs = await runEnsemble({
|
|
4091
|
+
provider: evalProvider,
|
|
4092
|
+
system: JUDGE_SYSTEM_PROMPT,
|
|
4093
|
+
user: buildUserContent(plan, selected.text, selected.label),
|
|
4094
|
+
runs: runsPerEval,
|
|
4095
|
+
temperature,
|
|
4096
|
+
schema: verdictSchema,
|
|
4097
|
+
cache,
|
|
4098
|
+
cacheKey: key,
|
|
4099
|
+
label: "manni-tracevals"
|
|
4100
|
+
});
|
|
4101
|
+
const consensusBase = computeConsensus(runs);
|
|
4102
|
+
const zone = zoneFor(consensusBase, zones);
|
|
4103
|
+
const consensus = { ...consensusBase, zone };
|
|
4104
|
+
const judgeModel = evalProvider.modelName();
|
|
4105
|
+
const selfPreference = sessionModel !== void 0 && sessionModel === judgeModel ? { axis: "session", model: judgeModel } : plan.proposedBy?.includes(judgeModel) === true ? { axis: "criterion", model: judgeModel } : void 0;
|
|
4106
|
+
results.push({
|
|
4107
|
+
...base,
|
|
4108
|
+
outcome: zone === "auto-pass" ? "pass" : zone === "auto-fail" ? "fail" : "needs-review",
|
|
4109
|
+
consensus,
|
|
4110
|
+
turns: cached ? 0 : runsPerEval,
|
|
4111
|
+
...selfPreference ? { selfPreference } : {},
|
|
4112
|
+
durationMs: Date.now() - start
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
return results;
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
async function resolveOverride(plan, options) {
|
|
4119
|
+
const named = plan.provider !== void 0 ? `provider "${plan.provider}"` : `model "${plan.model ?? ""}"`;
|
|
4120
|
+
if (options.providerFor === void 0) {
|
|
4121
|
+
return {
|
|
4122
|
+
error: `eval names ${named}, but this run cannot construct providers by name`
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
try {
|
|
4126
|
+
return {
|
|
4127
|
+
provider: await options.providerFor({
|
|
4128
|
+
...plan.provider !== void 0 ? { provider: plan.provider } : {},
|
|
4129
|
+
...plan.model !== void 0 ? { model: plan.model } : {},
|
|
4130
|
+
origin: `eval "${plan.evalName}" in ${plan.artifact.path}`
|
|
4131
|
+
})
|
|
4132
|
+
};
|
|
4133
|
+
} catch (err) {
|
|
4134
|
+
return {
|
|
4135
|
+
error: `could not construct ${named}${plan.provider === void 0 || plan.model === void 0 ? "" : ` at model "${plan.model}"`} for this eval: ${err instanceof Error ? err.message : String(err)}`
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
// src/tracevals/commands/run.ts
|
|
4141
|
+
function asGrants(values) {
|
|
4142
|
+
const unknown = values.filter((v) => !isExecutionGrant(v));
|
|
4143
|
+
if (unknown.length > 0) throw new TracevalsError(unknownGrantsMessage(unknown));
|
|
4144
|
+
return values.filter(isExecutionGrant);
|
|
4145
|
+
}
|
|
4146
|
+
async function prepareRun(options) {
|
|
4147
|
+
const {
|
|
4148
|
+
config: loaded,
|
|
4149
|
+
dir: configDir,
|
|
4150
|
+
collections,
|
|
4151
|
+
path: configPath
|
|
4152
|
+
} = await discoverConfig(
|
|
4153
|
+
options.configDir ?? process.cwd(),
|
|
4154
|
+
{
|
|
4155
|
+
...options.config === void 0 ? {} : { configPath: options.config },
|
|
4156
|
+
...options.noConfig === void 0 ? {} : { noConfig: options.noConfig }
|
|
4157
|
+
}
|
|
4158
|
+
);
|
|
4159
|
+
const config = {
|
|
4160
|
+
...loaded,
|
|
4161
|
+
failOnReview: options.failOnReview ?? loaded.failOnReview,
|
|
4162
|
+
execution: {
|
|
4163
|
+
allow: [
|
|
4164
|
+
...grantsFor(loaded.execution.allow, {
|
|
4165
|
+
...options.allowExecution === void 0 ? {} : { allowExecution: asGrants(options.allowExecution) }
|
|
4166
|
+
})
|
|
4167
|
+
]
|
|
4168
|
+
},
|
|
4169
|
+
// A set-valued knob, so `--require` *adds* instead of replacing. A one-off
|
|
4170
|
+
// flag must not silently unregister the house graders a repo's config
|
|
4171
|
+
// names — every eval declaring one would flip to `unknown grader kind`,
|
|
4172
|
+
// which reads as a typo in an artifact nobody touched. Config entries load
|
|
4173
|
+
// first, so a deliberate `--require` still wins a colliding kind
|
|
4174
|
+
// (ADR 01017).
|
|
4175
|
+
plugins: [...loaded.plugins, ...options.require ?? []],
|
|
4176
|
+
reportUnusedArtifacts: options.reportUnusedArtifacts ?? loaded.reportUnusedArtifacts
|
|
4177
|
+
};
|
|
4178
|
+
const plugins = await loadGraderPlugins({
|
|
4179
|
+
plugins: config.plugins,
|
|
4180
|
+
configDir
|
|
4181
|
+
});
|
|
4182
|
+
const flags = {
|
|
4183
|
+
...options.provider !== void 0 ? { provider: options.provider } : {},
|
|
4184
|
+
...options.model !== void 0 ? { model: options.model } : {},
|
|
4185
|
+
...options.local !== void 0 ? { local: options.local } : {}
|
|
4186
|
+
};
|
|
4187
|
+
const selection = selectProvider2(config, flags);
|
|
4188
|
+
assertProviderSelection(selection);
|
|
4189
|
+
let judge = options.judge;
|
|
4190
|
+
if (judge === void 0 && options.deterministicOnly !== true) {
|
|
4191
|
+
const provider = constructProvider(
|
|
4192
|
+
config,
|
|
4193
|
+
await resolveProviderIdentity(config, flags)
|
|
4194
|
+
);
|
|
4195
|
+
const overridden = /* @__PURE__ */ new Map();
|
|
4196
|
+
const keyOf = (s) => `${s.provider}:${s.model ?? ""}`;
|
|
4197
|
+
const defaultKey = keyOf(selection);
|
|
4198
|
+
judge = makeTraceJudge({
|
|
4199
|
+
provider,
|
|
4200
|
+
// An eval may name its own provider or model. It is selected by the same
|
|
4201
|
+
// rule the run's own was — flag, then eval, then `tracevals.provider`,
|
|
4202
|
+
// then the family's `providers:` — so an eval cannot quietly outrank a
|
|
4203
|
+
// flag someone just typed.
|
|
4204
|
+
providerFor: async (ev) => {
|
|
4205
|
+
const chosen = selectProvider2(config, flags, ev);
|
|
4206
|
+
announceSelection(chosen);
|
|
4207
|
+
const key = keyOf(chosen);
|
|
4208
|
+
if (key === defaultKey) return provider;
|
|
4209
|
+
let built = overridden.get(key);
|
|
4210
|
+
if (built === void 0) {
|
|
4211
|
+
built = (async () => {
|
|
4212
|
+
assertProviderSelection(chosen);
|
|
4213
|
+
return constructProvider(
|
|
4214
|
+
config,
|
|
4215
|
+
await resolveProviderIdentity(config, flags, ev)
|
|
4216
|
+
);
|
|
4217
|
+
})();
|
|
4218
|
+
overridden.set(key, built);
|
|
4219
|
+
}
|
|
4220
|
+
return built;
|
|
4221
|
+
},
|
|
4222
|
+
runs: options.runs ?? config.judge.ensembleRuns,
|
|
4223
|
+
temperature: config.judge.temperature,
|
|
4224
|
+
zones: config.judge.zones,
|
|
4225
|
+
cacheDir: resolve5(configDir, config.judge.cacheDir),
|
|
4226
|
+
...options.noCache !== void 0 ? { noCache: options.noCache } : {},
|
|
4227
|
+
maxTurns: options.maxTurns ?? config.judge.maxTurns
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
const external = await loadExternalEvals({
|
|
4231
|
+
collections,
|
|
4232
|
+
configDir,
|
|
4233
|
+
...configPath === void 0 ? {} : { configPath },
|
|
4234
|
+
...options.offline === void 0 ? {} : { offline: options.offline }
|
|
4235
|
+
});
|
|
4236
|
+
return {
|
|
4237
|
+
config,
|
|
4238
|
+
configDir,
|
|
4239
|
+
warnings: plugins.warnings,
|
|
4240
|
+
...judge !== void 0 ? { judge } : {},
|
|
4241
|
+
...external === null ? {} : {
|
|
4242
|
+
metadataFor: (artifact) => external.forArtifact(artifact)
|
|
4243
|
+
}
|
|
4244
|
+
};
|
|
4245
|
+
}
|
|
4246
|
+
async function runOne(options, context) {
|
|
4247
|
+
const { config } = context;
|
|
4248
|
+
const report = await runEvals({
|
|
4249
|
+
tracePath: options.tracePath,
|
|
4250
|
+
...options.project !== void 0 ? { projectDir: options.project } : {},
|
|
4251
|
+
...options.env !== void 0 ? { env: options.env } : {},
|
|
4252
|
+
config,
|
|
4253
|
+
...context.judge !== void 0 ? { judge: context.judge } : {},
|
|
4254
|
+
...options.deterministicOnly !== void 0 ? { deterministicOnly: options.deterministicOnly } : {},
|
|
4255
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {},
|
|
4256
|
+
...context.warnings.length > 0 ? { warnings: context.warnings } : {},
|
|
4257
|
+
...context.metadataFor !== void 0 ? { metadataFor: context.metadataFor } : {}
|
|
4258
|
+
});
|
|
4259
|
+
let comparison;
|
|
4260
|
+
if (options.history) {
|
|
4261
|
+
const historyFile = resolve5(context.configDir, config.history.file);
|
|
4262
|
+
comparison = compareToLast(await loadHistory(historyFile), report) ?? void 0;
|
|
4263
|
+
await appendHistory(historyFile, report);
|
|
4264
|
+
}
|
|
4265
|
+
return { report, ...comparison ? { comparison } : {} };
|
|
4266
|
+
}
|
|
4267
|
+
async function runRun(options) {
|
|
4268
|
+
const context = await prepareRun(options);
|
|
4269
|
+
const { report, comparison } = await runOne(options, context);
|
|
4270
|
+
const format = options.format ?? "pretty";
|
|
4271
|
+
let rendered = render(report, format, { color: options.color === true });
|
|
4272
|
+
if (comparison && (format === "pretty" || format === "markdown" || format === "github")) {
|
|
4273
|
+
rendered += `
|
|
4274
|
+
|
|
4275
|
+
${renderComparison(comparison)}`;
|
|
4276
|
+
}
|
|
4277
|
+
if (options.output) {
|
|
4278
|
+
await writeFile(options.output, rendered, "utf-8");
|
|
4279
|
+
}
|
|
4280
|
+
return { report, rendered, ...comparison ? { comparison } : {} };
|
|
4281
|
+
}
|
|
4282
|
+
function renderComparison(comparison) {
|
|
4283
|
+
const lines = [`History vs ${comparison.previousTimestamp}:`];
|
|
4284
|
+
for (const r of comparison.regressions) {
|
|
4285
|
+
lines.push(` regression: ${r.evalName} (${r.outcome})`);
|
|
4286
|
+
}
|
|
4287
|
+
for (const i of comparison.improvements) {
|
|
4288
|
+
lines.push(` improvement: ${i.evalName} (${i.outcome})`);
|
|
4289
|
+
}
|
|
4290
|
+
if (comparison.added.length) lines.push(` added: ${comparison.added.join(", ")}`);
|
|
4291
|
+
if (comparison.removed.length) {
|
|
4292
|
+
lines.push(` removed: ${comparison.removed.join(", ")}`);
|
|
4293
|
+
}
|
|
4294
|
+
if (lines.length === 1) lines.push(" no changes");
|
|
4295
|
+
return lines.join("\n");
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
// src/tracevals/commands/batch.ts
|
|
4299
|
+
function parseNewerThan(text) {
|
|
4300
|
+
return parseDuration(text, (message) => new TracevalsError(message));
|
|
4301
|
+
}
|
|
4302
|
+
function keepNewerThan(listings, newerThan) {
|
|
4303
|
+
if (newerThan === void 0) return listings;
|
|
4304
|
+
const floor = Date.now() - parseNewerThan(newerThan);
|
|
4305
|
+
return listings.filter((l) => l.mtimeMs >= floor);
|
|
4306
|
+
}
|
|
4307
|
+
function usesDiscovery(options) {
|
|
4308
|
+
return options.allProjects === true || options.newerThan !== void 0 || options.limit !== void 0;
|
|
4309
|
+
}
|
|
4310
|
+
async function resolveBatchTraces(options) {
|
|
4311
|
+
const named = options.traces ?? [];
|
|
4312
|
+
if (named.length > 0 && usesDiscovery(options)) {
|
|
4313
|
+
throw new TracevalsError(
|
|
4314
|
+
"name traces or select them with --all-projects/--newer-than/--limit, not both"
|
|
4315
|
+
);
|
|
4316
|
+
}
|
|
4317
|
+
if (named.length > 0) return named;
|
|
4318
|
+
const listings = await discoverTraces({
|
|
4319
|
+
...options.allProjects !== void 0 ? { allProjects: options.allProjects } : {},
|
|
4320
|
+
...options.project !== void 0 ? { project: options.project } : {},
|
|
4321
|
+
...options.limit !== void 0 ? { limit: options.limit } : {},
|
|
4322
|
+
...options.env !== void 0 ? { env: options.env } : {}
|
|
4323
|
+
});
|
|
4324
|
+
const kept = keepNewerThan(listings, options.newerThan);
|
|
4325
|
+
if (kept.length === 0) {
|
|
4326
|
+
const how = [
|
|
4327
|
+
options.allProjects === true ? "--all-projects" : `project ${options.project ?? process.cwd()}`,
|
|
4328
|
+
options.newerThan !== void 0 ? `--newer-than ${options.newerThan}` : void 0,
|
|
4329
|
+
options.limit !== void 0 ? `--limit ${options.limit}` : void 0
|
|
4330
|
+
].filter((s) => s !== void 0).join(", ");
|
|
4331
|
+
throw new TracevalsError(
|
|
4332
|
+
`no traces matched (${how}); run \`manni tracevals list\` to see what the session store holds`
|
|
4333
|
+
);
|
|
4334
|
+
}
|
|
4335
|
+
return kept.map((l) => l.file);
|
|
4336
|
+
}
|
|
4337
|
+
async function runBatch(options) {
|
|
4338
|
+
const start = Date.now();
|
|
4339
|
+
const files = await resolveBatchTraces(options);
|
|
4340
|
+
const context = await prepareRun(options);
|
|
4341
|
+
const outcomes = [];
|
|
4342
|
+
const reports = [];
|
|
4343
|
+
const comparisons = [];
|
|
4344
|
+
for (const file of files) {
|
|
4345
|
+
const traceStart = Date.now();
|
|
4346
|
+
try {
|
|
4347
|
+
const { report: report2, comparison } = await runOne(
|
|
4348
|
+
{ ...options, tracePath: file },
|
|
4349
|
+
context
|
|
4350
|
+
);
|
|
4351
|
+
outcomes.push({ file, report: report2 });
|
|
4352
|
+
reports.push(report2);
|
|
4353
|
+
if (comparison) comparisons.push(comparison);
|
|
4354
|
+
} catch (err) {
|
|
4355
|
+
outcomes.push({
|
|
4356
|
+
file,
|
|
4357
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4358
|
+
durationMs: Date.now() - traceStart
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
const report = aggregate(outcomes, {
|
|
4363
|
+
warnings: context.warnings,
|
|
4364
|
+
durationMs: Date.now() - start
|
|
4365
|
+
});
|
|
4366
|
+
const rendered = renderBatch(report, options.format ?? "pretty", reports, {
|
|
4367
|
+
color: options.color === true
|
|
4368
|
+
});
|
|
4369
|
+
if (options.output) {
|
|
4370
|
+
await writeFile2(options.output, rendered, "utf-8");
|
|
4371
|
+
}
|
|
4372
|
+
return { report, rendered, reports, outcomes, comparisons };
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
export {
|
|
4376
|
+
outputTail,
|
|
4377
|
+
realExec,
|
|
4378
|
+
parseDuration,
|
|
4379
|
+
TURN_BUDGET_SKIP,
|
|
4380
|
+
turnBudgetSkipReason,
|
|
4381
|
+
isTurnBudgetSkip,
|
|
4382
|
+
URI_BASE_ID,
|
|
4383
|
+
fileUri,
|
|
4384
|
+
normalizeRoot,
|
|
4385
|
+
artifactLocation,
|
|
4386
|
+
aggregate,
|
|
4387
|
+
renderBatchPretty,
|
|
4388
|
+
renderBatchMarkdown,
|
|
4389
|
+
renderCalibrationPretty,
|
|
4390
|
+
renderCalibrationMarkdown,
|
|
4391
|
+
REPORT_FORMATS,
|
|
4392
|
+
CALIBRATE_FORMATS,
|
|
4393
|
+
SUMMARY_FORMATS,
|
|
4394
|
+
parseFormat,
|
|
4395
|
+
render,
|
|
4396
|
+
renderBatch,
|
|
4397
|
+
renderCalibration,
|
|
4398
|
+
detectFormat,
|
|
4399
|
+
detectContentFormat,
|
|
4400
|
+
parseTraceFile,
|
|
4401
|
+
parseTraceContent,
|
|
4402
|
+
IMPLICIT_EVAL_NAME,
|
|
4403
|
+
planEvals,
|
|
4404
|
+
PROMPT_VERSION,
|
|
4405
|
+
JUDGE_SYSTEM_PROMPT,
|
|
4406
|
+
buildUserContent,
|
|
4407
|
+
globToRegExp,
|
|
4408
|
+
matchesGlob,
|
|
4409
|
+
WHEN_CONDITIONS,
|
|
4410
|
+
validateWhen,
|
|
4411
|
+
evaluateWhen,
|
|
4412
|
+
windowFor,
|
|
4413
|
+
skippedWindow,
|
|
4414
|
+
BUILTIN_GRADER_KINDS,
|
|
4415
|
+
registerGrader,
|
|
4416
|
+
graderFor,
|
|
4417
|
+
listGraderKinds,
|
|
4418
|
+
renderTrace,
|
|
4419
|
+
runEvals,
|
|
4420
|
+
METADATA_KEY,
|
|
4421
|
+
urlManifestRefusal,
|
|
4422
|
+
loadExternalEvals,
|
|
4423
|
+
loadGraderPlugins,
|
|
4424
|
+
entryFor,
|
|
4425
|
+
appendHistory,
|
|
4426
|
+
loadHistory,
|
|
4427
|
+
compareToLast,
|
|
4428
|
+
selectProvider2 as selectProvider,
|
|
4429
|
+
announceSelection,
|
|
4430
|
+
assertProviderSelection,
|
|
4431
|
+
providerSpecFor2 as providerSpecFor,
|
|
4432
|
+
resolveProviderIdentity,
|
|
4433
|
+
constructProvider,
|
|
4434
|
+
makeJudgeProvider,
|
|
4435
|
+
sha256,
|
|
4436
|
+
VerdictCache,
|
|
4437
|
+
cacheKey,
|
|
4438
|
+
makeTraceJudge,
|
|
4439
|
+
prepareRun,
|
|
4440
|
+
runOne,
|
|
4441
|
+
runRun,
|
|
4442
|
+
parseNewerThan,
|
|
4443
|
+
keepNewerThan,
|
|
4444
|
+
resolveBatchTraces,
|
|
4445
|
+
runBatch
|
|
4446
|
+
};
|
|
4447
|
+
//# sourceMappingURL=chunk-CI2GOU3A.js.map
|