@leostera/evalkit 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -1
- package/README.md +3 -2
- package/dist/cli.mjs +311 -92
- package/dist/dashboard/assets/index-CFJzcw3o.css +2 -0
- package/dist/dashboard/assets/{index-By1SJ1on.js → index-DgoX2x_M.js} +34 -31
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-BbVblaHz.mjs → index-db-DI7xKMSX.mjs} +237 -47
- package/dist/index.d.mts +1141 -220
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +241 -52
- package/dist/runner.d.mts +782 -225
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +1 -1
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.d.mts +9 -6
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +3 -3
- package/dist/skills/evalkit-agents/SKILL.md +3 -3
- package/dist/skills/evalkit-datasets/SKILL.md +1 -1
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +4 -4
- package/dist/skills/evalkit-results/SKILL.md +2 -2
- package/dist/{src-Bn6fnZnF.mjs → src-CXzbHAKN.mjs} +1 -1
- package/dist/{src-BrfIXfO2.mjs → src-D6Ax2t9b.mjs} +636 -220
- package/dist/src-Dc3GXacB.mjs +3637 -0
- package/dist/upgrade-rMsGzUEJ.mjs +230 -0
- package/dist/upgrade-transcript-C8DUykLd.mjs +402 -0
- package/package.json +2 -1
- package/dist/dashboard/assets/index-geXMXCx9.css +0 -2
- package/dist/src-CcdLZqFk.mjs +0 -1714
- package/dist/upgrade-60SIRius.mjs +0 -110
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { n as assertNoRemovedOptions } from "./upgrade-
|
|
1
|
+
import { n as assertNoRemovedOptions, u as transcriptUsage } from "./upgrade-rMsGzUEJ.mjs";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
+
import { buildContext } from "@leostera/atxf";
|
|
3
4
|
//#region ../core/src/identity.ts
|
|
4
5
|
const ResourceKindSchema = Schema.Literal("run", "artifact");
|
|
5
6
|
/** Human-authored project-local identifier; never a generated resource URI. */
|
|
@@ -153,6 +154,188 @@ function isDataset(value) {
|
|
|
153
154
|
return typeof value === "object" && value !== null && value.kind === "dataset" && typeof value.source === "object";
|
|
154
155
|
}
|
|
155
156
|
//#endregion
|
|
157
|
+
//#region ../core/src/limits.ts
|
|
158
|
+
const UNITS = {
|
|
159
|
+
ms: 1,
|
|
160
|
+
s: 1e3,
|
|
161
|
+
m: 6e4,
|
|
162
|
+
h: 36e5
|
|
163
|
+
};
|
|
164
|
+
/** `'20m'` → 1_200_000. A number is milliseconds. */
|
|
165
|
+
function parseDuration(value, owner) {
|
|
166
|
+
if (typeof value === "number") return positive(value, owner);
|
|
167
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h)\s*$/.exec(value);
|
|
168
|
+
if (!match) throw new Error(`${owner} must be milliseconds or a duration such as 90s, 20m or 1h; received ${JSON.stringify(value)}`);
|
|
169
|
+
return positive(Number(match[1]) * UNITS[match[2]], owner);
|
|
170
|
+
}
|
|
171
|
+
function positive(value, owner) {
|
|
172
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${owner} must be a positive number; received ${value}`);
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
/** Checks and normalizes limits; `owner` names where they were written. */
|
|
176
|
+
function parseLimits(input, owner) {
|
|
177
|
+
if (input === void 0) return {};
|
|
178
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) throw new Error(`${owner} limits must be an object`);
|
|
179
|
+
const limits = {};
|
|
180
|
+
for (const [key, value] of Object.entries(input)) {
|
|
181
|
+
if (value === void 0) continue;
|
|
182
|
+
if (key === "time") limits.time = parseDuration(value, `${owner} limits.time`);
|
|
183
|
+
else if (key === "steps" || key === "tokens") {
|
|
184
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`${owner} limits.${key} must be a positive whole number; received ${String(value)}`);
|
|
185
|
+
limits[key] = value;
|
|
186
|
+
} else throw new Error(`${owner} has an unknown limit ${JSON.stringify(key)}; limits are steps, tokens and time`);
|
|
187
|
+
}
|
|
188
|
+
return limits;
|
|
189
|
+
}
|
|
190
|
+
/** Later layers win, kind by kind: config, then scenario, then matrix cell, then the command line. */
|
|
191
|
+
function resolveLimits(...layers) {
|
|
192
|
+
return Object.assign({}, ...layers);
|
|
193
|
+
}
|
|
194
|
+
/** A matrix's `limits` axes are parameters named `limits.<kind>`. */
|
|
195
|
+
const LIMIT_PARAMETER_PREFIX = "limits.";
|
|
196
|
+
/** The limits a matrix cell's `limits.<kind>` parameters set. */
|
|
197
|
+
function limitsFromParameters(parameters) {
|
|
198
|
+
const input = {};
|
|
199
|
+
for (const [key, value] of Object.entries(parameters ?? {})) if (key.startsWith("limits.")) input[key.slice(7)] = value;
|
|
200
|
+
return parseLimits(input, "The matrix cell");
|
|
201
|
+
}
|
|
202
|
+
/** `steps=80` or `time=1h`, from `--limit`. */
|
|
203
|
+
function parseLimitFlag(flag) {
|
|
204
|
+
const index = flag.indexOf("=");
|
|
205
|
+
if (index < 1) throw new Error(`--limit takes kind=value, such as steps=80 or time=20m; received ${JSON.stringify(flag)}`);
|
|
206
|
+
const kind = flag.slice(0, index).trim();
|
|
207
|
+
const raw = flag.slice(index + 1).trim();
|
|
208
|
+
return parseLimits({ [kind]: kind === "time" && !/^\d+$/.test(raw) ? raw : Number(raw) }, "--limit");
|
|
209
|
+
}
|
|
210
|
+
/** `40 steps`, `2,000,000 tokens`, `20m`. */
|
|
211
|
+
function formatLimit(kind, value) {
|
|
212
|
+
if (kind === "time") {
|
|
213
|
+
if (value % 36e5 === 0) return `${value / 36e5}h`;
|
|
214
|
+
if (value % 6e4 === 0) return `${value / 6e4}m`;
|
|
215
|
+
if (value % 1e3 === 0) return `${value / 1e3}s`;
|
|
216
|
+
return `${value}ms`;
|
|
217
|
+
}
|
|
218
|
+
return `${value.toLocaleString("en-US")} ${kind}`;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region ../core/src/transcript.ts
|
|
222
|
+
const TREE = /* @__PURE__ */ new Set([
|
|
223
|
+
"message",
|
|
224
|
+
"context",
|
|
225
|
+
"settings",
|
|
226
|
+
"compaction",
|
|
227
|
+
"context_edit",
|
|
228
|
+
"branch_summary",
|
|
229
|
+
"turn",
|
|
230
|
+
"tool_status",
|
|
231
|
+
"usage",
|
|
232
|
+
"checkpoint",
|
|
233
|
+
"agent_link",
|
|
234
|
+
"custom"
|
|
235
|
+
]);
|
|
236
|
+
function isTreeRecord(record) {
|
|
237
|
+
return TREE.has(record.type);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The records from the transcript's root to its active leaf, in order: what
|
|
241
|
+
* the session is now, without abandoned branches (ATXF §4).
|
|
242
|
+
*/
|
|
243
|
+
function activePath(transcript) {
|
|
244
|
+
const byId = /* @__PURE__ */ new Map();
|
|
245
|
+
let leaf = null;
|
|
246
|
+
for (const record of transcript.records) if (isTreeRecord(record)) {
|
|
247
|
+
byId.set(record.id, record);
|
|
248
|
+
leaf = record.id;
|
|
249
|
+
} else if (record.type === "leaf") leaf = record.targetId;
|
|
250
|
+
const path = [];
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
for (let id = leaf; id !== null && !seen.has(id);) {
|
|
253
|
+
seen.add(id);
|
|
254
|
+
const record = byId.get(id);
|
|
255
|
+
if (!record) break;
|
|
256
|
+
path.push(record);
|
|
257
|
+
id = record.parentId;
|
|
258
|
+
}
|
|
259
|
+
return path.reverse();
|
|
260
|
+
}
|
|
261
|
+
/** Messages on the active path, optionally of one role. */
|
|
262
|
+
function transcriptMessages(transcript, role) {
|
|
263
|
+
return activePath(transcript).filter((record) => record.type === "message" && (role === void 0 || record.role === role));
|
|
264
|
+
}
|
|
265
|
+
/** Every tool call on the active path, in order, with its result. */
|
|
266
|
+
function transcriptToolCalls(transcript, name) {
|
|
267
|
+
return callsIn(activePath(transcript)).filter((entry) => name === void 0 || entry.call.name === name).map(({ message, call, result }) => ({
|
|
268
|
+
id: call.id,
|
|
269
|
+
name: call.name,
|
|
270
|
+
arguments: call.arguments ?? {},
|
|
271
|
+
...call.input !== void 0 ? { input: call.input } : {},
|
|
272
|
+
...result ? {
|
|
273
|
+
result: toolResultValue(result),
|
|
274
|
+
resultId: result.id
|
|
275
|
+
} : {},
|
|
276
|
+
isError: result?.isError === true,
|
|
277
|
+
messageId: message.id
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
/** Tool calls among `records`, each paired with the first result for it. */
|
|
281
|
+
function callsIn(records) {
|
|
282
|
+
const calls = [];
|
|
283
|
+
const open = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const record of records) {
|
|
285
|
+
if (record.type !== "message") continue;
|
|
286
|
+
if (record.role === "assistant") {
|
|
287
|
+
for (const block of record.content) if (block.type === "tool_call") {
|
|
288
|
+
const entry = {
|
|
289
|
+
message: record,
|
|
290
|
+
call: block
|
|
291
|
+
};
|
|
292
|
+
calls.push(entry);
|
|
293
|
+
open.set(block.id, entry);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (record.role === "tool") {
|
|
297
|
+
const entry = open.get(record.toolCallId);
|
|
298
|
+
if (entry && !entry.result) entry.result = record;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return calls;
|
|
302
|
+
}
|
|
303
|
+
/** The text of a message's content: text blocks joined, other blocks left out. */
|
|
304
|
+
function contentText(content) {
|
|
305
|
+
if (typeof content === "string") return content;
|
|
306
|
+
return content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
|
|
307
|
+
}
|
|
308
|
+
/** The text of the last assistant message on the active path, or `''`. */
|
|
309
|
+
function transcriptReply(transcript) {
|
|
310
|
+
const last = transcriptMessages(transcript, "assistant").at(-1);
|
|
311
|
+
return last ? contentText(last.content) : "";
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* A tool result's value: the JSON an adapter returned (kept in
|
|
315
|
+
* `details.result`), else the result's text.
|
|
316
|
+
*/
|
|
317
|
+
function toolResultValue(result) {
|
|
318
|
+
if (result.details && "result" in result.details) return result.details.result;
|
|
319
|
+
return contentText(result.content);
|
|
320
|
+
}
|
|
321
|
+
/** `provider/id`, or `id` when the provider is unknown. */
|
|
322
|
+
function modelName(model) {
|
|
323
|
+
if (!model) return void 0;
|
|
324
|
+
return model.provider === "unknown" ? model.id : `${model.provider}/${model.id}`;
|
|
325
|
+
}
|
|
326
|
+
/** `openai/gpt-5` → `{ provider: 'openai', id: 'gpt-5' }`; without a provider, `unknown`. */
|
|
327
|
+
function parseModel(model) {
|
|
328
|
+
if (typeof model !== "string") return model;
|
|
329
|
+
const slash = model.indexOf("/");
|
|
330
|
+
return slash > 0 && slash < model.length - 1 ? {
|
|
331
|
+
provider: model.slice(0, slash),
|
|
332
|
+
id: model.slice(slash + 1)
|
|
333
|
+
} : {
|
|
334
|
+
provider: "unknown",
|
|
335
|
+
id: model
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
156
339
|
//#region ../core/src/json.ts
|
|
157
340
|
/** A value that cannot be stored in a report, with the path of the offending part. */
|
|
158
341
|
var NotJsonError = class extends Error {
|
|
@@ -206,11 +389,194 @@ function toJsonObject(value, path) {
|
|
|
206
389
|
return converted;
|
|
207
390
|
}
|
|
208
391
|
//#endregion
|
|
392
|
+
//#region ../core/src/judge.ts
|
|
393
|
+
const KEY = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
394
|
+
function unitInterval(value) {
|
|
395
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* A judge that answers with one of these choices, in this order:
|
|
399
|
+
* `choices({ A: ['Accurate and helpful', 1], B: ['Partly right', 0.5], C: ['Wrong', 0] })`.
|
|
400
|
+
*/
|
|
401
|
+
function choices(options) {
|
|
402
|
+
const entries = Object.entries(options);
|
|
403
|
+
if (entries.length < 2) throw new Error("choices(...) needs at least two choices");
|
|
404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
405
|
+
return {
|
|
406
|
+
kind: "choices",
|
|
407
|
+
choices: entries.map(([key, [label, value]]) => {
|
|
408
|
+
if (!KEY.test(key)) throw new Error(`Choice ${JSON.stringify(key)} must be a short word of letters, digits, "-" or "_"`);
|
|
409
|
+
if (seen.has(key.toUpperCase())) throw new Error(`Choice ${key} is listed twice`);
|
|
410
|
+
seen.add(key.toUpperCase());
|
|
411
|
+
if (!label.trim()) throw new Error(`Choice ${key} needs a label`);
|
|
412
|
+
if (!unitInterval(value)) throw new Error(`Choice ${key} must be worth a number from 0 to 1; received ${String(value)}`);
|
|
413
|
+
return {
|
|
414
|
+
key,
|
|
415
|
+
label,
|
|
416
|
+
value
|
|
417
|
+
};
|
|
418
|
+
})
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* A judge that scores each criterion from 0 to 1; the score is the weighted
|
|
423
|
+
* mean. A number is the weight; `{ weight, description }` also says what the
|
|
424
|
+
* criterion means. Weights default to 1.
|
|
425
|
+
*/
|
|
426
|
+
function criteria(options) {
|
|
427
|
+
return {
|
|
428
|
+
kind: "criteria",
|
|
429
|
+
criteria: declaredCriteria(options)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/** Checked criteria declarations, for `criteria()` and `predicate(..., { criteria })`. */
|
|
433
|
+
function declaredCriteria(options) {
|
|
434
|
+
const entries = Object.entries(options);
|
|
435
|
+
if (!entries.length) throw new Error("criteria(...) needs a criterion");
|
|
436
|
+
return entries.map(([name, option]) => {
|
|
437
|
+
if (!name.trim()) throw new Error("A criterion needs a name");
|
|
438
|
+
const weight = typeof option === "number" ? option : option.weight ?? 1;
|
|
439
|
+
if (!Number.isFinite(weight) || weight <= 0) throw new Error(`Criterion ${name} must have a positive weight; received ${String(weight)}`);
|
|
440
|
+
const description = typeof option === "number" ? void 0 : option.description;
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
weight,
|
|
444
|
+
...description ? { description } : {}
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** What a criterion counts for in the weighted mean. */
|
|
449
|
+
const criterionValue = (result) => "value" in result ? result.value : 0;
|
|
450
|
+
/** The weighted mean of criteria; weights default to 1, a criterion not reached counts 0. */
|
|
451
|
+
function weightedMean(results) {
|
|
452
|
+
let total = 0;
|
|
453
|
+
let weights = 0;
|
|
454
|
+
for (const result of results) {
|
|
455
|
+
const weight = result.weight ?? 1;
|
|
456
|
+
total += weight * criterionValue(result);
|
|
457
|
+
weights += weight;
|
|
458
|
+
}
|
|
459
|
+
if (!weights) throw new Error("Criteria need a positive total weight");
|
|
460
|
+
return total / weights;
|
|
461
|
+
}
|
|
462
|
+
/** The inputs a judge sees when the rule names none. */
|
|
463
|
+
const DEFAULT_JUDGE_INPUTS = ["conversation", "reply"];
|
|
464
|
+
/** The names of the inputs a judge sees, in prompt order. */
|
|
465
|
+
function judgeInputNames(rule) {
|
|
466
|
+
return rule.inputs ? Object.keys(rule.inputs) : [...DEFAULT_JUDGE_INPUTS];
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The prompt a judge agent receives. `values` holds each input's text;
|
|
470
|
+
* an input without one shows as a `{placeholder}`, which is how the
|
|
471
|
+
* dashboard shows the prompt before any run.
|
|
472
|
+
*/
|
|
473
|
+
function judgePrompt(options) {
|
|
474
|
+
const inputs = options.inputs.map((name) => `${name}:\n${options.values?.[name] ?? `{${name}}`}`).join("\n\n");
|
|
475
|
+
return [
|
|
476
|
+
"You are judging one run of an AI agent. Judge only by the rubric, using only the inputs below.",
|
|
477
|
+
`Rubric:\n${options.rubric.trim()}`,
|
|
478
|
+
inputs,
|
|
479
|
+
answerInstruction(options.answer)
|
|
480
|
+
].join("\n\n");
|
|
481
|
+
}
|
|
482
|
+
function answerInstruction(answer) {
|
|
483
|
+
switch (answer.kind) {
|
|
484
|
+
case "verdict": return "Answer with only a JSON object: {\"value\": <a number from 0 to 1>, \"explanation\": \"<one or two sentences why>\"}";
|
|
485
|
+
case "choices": return [`Answer with one of ${answer.choices.map((choice) => choice.key).join(", ")} on the first line, then one sentence why on the next.`, ...answer.choices.map((choice) => `${choice.key}) ${choice.label}`)].join("\n");
|
|
486
|
+
case "criteria": return [
|
|
487
|
+
"Score each criterion from 0 to 1:",
|
|
488
|
+
...answer.criteria.map((criterion) => `- ${criterion.name}${criterion.description ? `: ${criterion.description}` : ""}`),
|
|
489
|
+
`Answer with only a JSON object: {"criteria": {${answer.criteria.map((criterion) => `${JSON.stringify(criterion.name)}: {"value": <0 to 1>, "explanation": "<why>"}`).join(", ")}}, "explanation": "<one sentence overall>"}`
|
|
490
|
+
].join("\n");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/** Thrown when a judge's reply is not in the format asked for; the message says why. */
|
|
494
|
+
var UnreadableAnswer = class extends Error {};
|
|
495
|
+
/**
|
|
496
|
+
* Reads a judge's reply in the format its rule asked for. Throws
|
|
497
|
+
* `UnreadableAnswer` with a reason the judge can be told, so it can answer again.
|
|
498
|
+
*/
|
|
499
|
+
function readJudgeAnswer(answer, reply) {
|
|
500
|
+
if (answer.kind === "choices") return readChoice(answer.choices, reply);
|
|
501
|
+
const object = jsonObject(reply);
|
|
502
|
+
if (answer.kind === "verdict") {
|
|
503
|
+
if (!unitInterval(object.value)) throw new UnreadableAnswer("\"value\" must be a number from 0 to 1");
|
|
504
|
+
return {
|
|
505
|
+
value: object.value,
|
|
506
|
+
...typeof object.explanation === "string" && object.explanation.trim() ? { explanation: object.explanation.trim() } : {},
|
|
507
|
+
...object.evidence !== void 0 ? { evidence: object.evidence } : {}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
const scored = object.criteria;
|
|
511
|
+
if (typeof scored !== "object" || scored === null || Array.isArray(scored)) throw new UnreadableAnswer("\"criteria\" must be an object with one entry per criterion");
|
|
512
|
+
const results = answer.criteria.map((criterion) => {
|
|
513
|
+
const entry = scored[criterion.name];
|
|
514
|
+
const value = typeof entry === "number" ? entry : typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry.value : void 0;
|
|
515
|
+
if (!unitInterval(value)) throw new UnreadableAnswer(`criterion ${JSON.stringify(criterion.name)} needs a "value" from 0 to 1`);
|
|
516
|
+
const explanation = typeof entry === "object" && entry !== null && !Array.isArray(entry) && typeof entry.explanation === "string" && entry.explanation.trim() ? entry.explanation.trim() : void 0;
|
|
517
|
+
return {
|
|
518
|
+
name: criterion.name,
|
|
519
|
+
value,
|
|
520
|
+
weight: criterion.weight,
|
|
521
|
+
...explanation ? { explanation } : {}
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
return {
|
|
525
|
+
value: weightedMean(results),
|
|
526
|
+
criteria: results,
|
|
527
|
+
...typeof object.explanation === "string" && object.explanation.trim() ? { explanation: object.explanation.trim() } : {}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function readChoice(options, reply) {
|
|
531
|
+
if (typeof reply !== "string") throw new UnreadableAnswer("answer with plain text, not JSON");
|
|
532
|
+
const lines = reply.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
533
|
+
const first = lines[0] ?? "";
|
|
534
|
+
const match = /^(?:answer\s*[:-]?\s*)?[*_("'[\s]*([A-Za-z0-9][A-Za-z0-9_-]*)/i.exec(first);
|
|
535
|
+
const said = match?.[1]?.toUpperCase();
|
|
536
|
+
const choice = options.find((option) => option.key.toUpperCase() === said);
|
|
537
|
+
if (!choice) throw new UnreadableAnswer(`the first line must be one of ${options.map((option) => option.key).join(", ")}`);
|
|
538
|
+
const explanation = [first.slice(match[0].length).replace(/^[*_)"'\].:\s-]+/, "").trim(), ...lines.slice(1)].filter(Boolean).join(" ");
|
|
539
|
+
return {
|
|
540
|
+
value: choice.value,
|
|
541
|
+
choice: choice.key,
|
|
542
|
+
...explanation ? { explanation } : {}
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/** The JSON object in a reply: the reply itself, or the first `{…}` in its text (code fences allowed). */
|
|
546
|
+
function jsonObject(reply) {
|
|
547
|
+
if (typeof reply === "object" && reply !== null && !Array.isArray(reply)) return reply;
|
|
548
|
+
if (typeof reply !== "string") throw new UnreadableAnswer("answer with a JSON object");
|
|
549
|
+
const start = reply.indexOf("{");
|
|
550
|
+
const end = reply.lastIndexOf("}");
|
|
551
|
+
if (start === -1 || end < start) throw new UnreadableAnswer("answer with only a JSON object");
|
|
552
|
+
let parsed;
|
|
553
|
+
try {
|
|
554
|
+
parsed = JSON.parse(reply.slice(start, end + 1));
|
|
555
|
+
} catch {
|
|
556
|
+
throw new UnreadableAnswer("the JSON object could not be parsed");
|
|
557
|
+
}
|
|
558
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new UnreadableAnswer("answer with a JSON object");
|
|
559
|
+
return parsed;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* The pass mark in words: `only 1`, `≥ 0.5`, or for choices the ones that
|
|
563
|
+
* pass, such as `A or B`.
|
|
564
|
+
*/
|
|
565
|
+
function passMark(passAt, answer) {
|
|
566
|
+
const at = passAt ?? 1;
|
|
567
|
+
if (answer?.kind === "choices") {
|
|
568
|
+
const passing = answer.choices.filter((choice) => choice.value >= at).map((choice) => choice.key);
|
|
569
|
+
if (!passing.length) return "no choice passes";
|
|
570
|
+
if (passing.length === answer.choices.length) return "any choice";
|
|
571
|
+
return passing.length === 1 ? `only ${passing[0]}` : `${passing.slice(0, -1).join(", ")} or ${passing.at(-1)}`;
|
|
572
|
+
}
|
|
573
|
+
return at === 1 ? "only 1" : `≥ ${at}`;
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
209
576
|
//#region ../core/src/catalog.ts
|
|
210
577
|
function catalogEntry(evaluation, suite, options = {}) {
|
|
211
578
|
const at = (value) => relativeSite(definitionSite(value), options.root);
|
|
212
579
|
const definedAt = at(evaluation);
|
|
213
|
-
const judgeAgent = evaluation.judge ? catalogAgent(evaluation.judge, at) : void 0;
|
|
214
580
|
const policy = evaluation.policy ?? {};
|
|
215
581
|
return {
|
|
216
582
|
id: evaluation.id,
|
|
@@ -218,15 +584,12 @@ function catalogEntry(evaluation, suite, options = {}) {
|
|
|
218
584
|
...evaluation.name ? { name: evaluation.name } : {},
|
|
219
585
|
...suite ? { suiteId: authoringId(suite) } : {},
|
|
220
586
|
agent: catalogAgent(evaluation.agent, at),
|
|
221
|
-
...judgeAgent ? { judgeAgent } : {},
|
|
222
587
|
datasets: (evaluation.datasets ?? []).map((dataset) => catalogDataset(dataset, at)),
|
|
223
588
|
repeat: evaluation.repeat ?? 1,
|
|
224
|
-
policy: {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
transcript: evaluation.transcript.map((step) => catalogStep(step, at)),
|
|
229
|
-
scorers: evaluation.scoring.map((rule) => catalogScorer(rule, at)),
|
|
589
|
+
policy: policy.failfast !== void 0 ? { failfast: policy.failfast } : {},
|
|
590
|
+
...evaluation.limits ? { limits: parseLimits(evaluation.limits, `Eval ${evaluation.id}`) } : {},
|
|
591
|
+
transcript: evaluation.transcript.map((step) => catalogStep(step, at, options.judge)),
|
|
592
|
+
scorers: evaluation.scoring.map((rule) => catalogScorer(rule, at, options.judge)),
|
|
230
593
|
...evaluation.metadata !== void 0 ? { metadata: toJson(evaluation.metadata, `${evaluation.id}.metadata`) } : {},
|
|
231
594
|
...definedAt ? { definedAt } : {}
|
|
232
595
|
};
|
|
@@ -270,18 +633,37 @@ function catalogDataset(dataset, at) {
|
|
|
270
633
|
...source.valueFile !== void 0 ? { value: { file: source.valueFile } } : source.value !== void 0 ? { value: { shape: shapeOf(source.value) } } : {}
|
|
271
634
|
};
|
|
272
635
|
}
|
|
273
|
-
function catalogScorer(rule, at) {
|
|
636
|
+
function catalogScorer(rule, at, defaultJudge) {
|
|
274
637
|
const definedAt = at(rule);
|
|
275
638
|
return {
|
|
276
639
|
name: rule.name,
|
|
277
640
|
kind: rule.kind,
|
|
278
|
-
...rule.
|
|
641
|
+
...rule.description ? { description: rule.description } : {},
|
|
642
|
+
...rule.passAt !== void 0 ? { passAt: rule.passAt } : {},
|
|
279
643
|
...rule.supportsPartial !== void 0 ? { supportsPartial: rule.supportsPartial } : {},
|
|
280
|
-
...rule.kind === "judge" ?
|
|
644
|
+
...rule.kind === "judge" ? catalogJudge(rule, at, defaultJudge) : rule.criteria ? { answer: {
|
|
645
|
+
kind: "criteria",
|
|
646
|
+
criteria: rule.criteria
|
|
647
|
+
} } : {},
|
|
281
648
|
...definedAt ? { definedAt } : {}
|
|
282
649
|
};
|
|
283
650
|
}
|
|
284
|
-
function
|
|
651
|
+
function catalogJudge(rule, at, defaultJudge) {
|
|
652
|
+
const agent = rule.agent ?? defaultJudge;
|
|
653
|
+
const inputs = judgeInputNames(rule);
|
|
654
|
+
return {
|
|
655
|
+
rubric: rule.rubric,
|
|
656
|
+
...agent ? { agent: catalogAgent(agent, at) } : {},
|
|
657
|
+
inputs,
|
|
658
|
+
answer: rule.answer,
|
|
659
|
+
prompt: judgePrompt({
|
|
660
|
+
rubric: rule.rubric,
|
|
661
|
+
answer: rule.answer,
|
|
662
|
+
inputs
|
|
663
|
+
})
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function catalogStep(step, at, defaultJudge) {
|
|
285
667
|
switch (step.kind) {
|
|
286
668
|
case "user": return typeof step.message === "string" ? {
|
|
287
669
|
kind: "user",
|
|
@@ -298,7 +680,7 @@ function catalogStep(step, at) {
|
|
|
298
680
|
};
|
|
299
681
|
default: return {
|
|
300
682
|
kind: "checkpoint",
|
|
301
|
-
scorer: catalogScorer(step, at)
|
|
683
|
+
scorer: catalogScorer(step, at, defaultJudge)
|
|
302
684
|
};
|
|
303
685
|
}
|
|
304
686
|
}
|
|
@@ -336,18 +718,6 @@ function defineConfig(configuration) {
|
|
|
336
718
|
return configuration;
|
|
337
719
|
}
|
|
338
720
|
//#endregion
|
|
339
|
-
//#region ../core/src/usage.ts
|
|
340
|
-
/**
|
|
341
|
-
* The tokens a turn counts toward the `tokens` metric: `inputTokens +
|
|
342
|
-
* outputTokens` (cached input included), or `totalTokens` when neither is
|
|
343
|
-
* reported. Undefined when the turn reported no usage.
|
|
344
|
-
*/
|
|
345
|
-
function turnTokens(usage) {
|
|
346
|
-
if (!usage) return void 0;
|
|
347
|
-
if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0) return (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
348
|
-
return usage.totalTokens;
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
721
|
//#region ../core/src/canonical-json.ts
|
|
352
722
|
/** Canonical JSON: keys sorted recursively, array order preserved. Rejects non-JSON inputs. */
|
|
353
723
|
function canonicalParameters(value) {
|
|
@@ -360,10 +730,27 @@ function canonicalParameters(value) {
|
|
|
360
730
|
//#endregion
|
|
361
731
|
//#region ../core/src/matrix.ts
|
|
362
732
|
const COUNT_BUDGET = 1e5;
|
|
733
|
+
/** The matrix's parameters with its `limits` as `limits.<kind>` axes. */
|
|
734
|
+
function withLimitAxes(definition) {
|
|
735
|
+
if (definition.limits === void 0) return definition.parameters;
|
|
736
|
+
const parameters = { ...definition.parameters };
|
|
737
|
+
for (const [kind, values] of Object.entries(definition.limits)) {
|
|
738
|
+
if (values === void 0) continue;
|
|
739
|
+
const axis = `${LIMIT_PARAMETER_PREFIX}${kind}`;
|
|
740
|
+
if (Object.hasOwn(parameters, axis)) throw new Error(`Matrix declares ${axis} as both a parameter and a limit`);
|
|
741
|
+
for (const value of values) parseLimits({ [kind]: value }, `Matrix ${definition.id}`);
|
|
742
|
+
parameters[axis] = values;
|
|
743
|
+
}
|
|
744
|
+
return parameters;
|
|
745
|
+
}
|
|
363
746
|
const equal = (a, b) => canonicalParameters(a) === canonicalParameters(b);
|
|
364
747
|
const matches = (pattern, values) => Object.entries(pattern).every(([key, value]) => Object.hasOwn(values, key) && equal(values[key], value));
|
|
365
|
-
function defineEvalMatrix(
|
|
366
|
-
authoringId(
|
|
748
|
+
function defineEvalMatrix(input) {
|
|
749
|
+
authoringId(input);
|
|
750
|
+
const definition = {
|
|
751
|
+
...input,
|
|
752
|
+
parameters: withLimitAxes(input)
|
|
753
|
+
};
|
|
367
754
|
const identities = /* @__PURE__ */ new Set();
|
|
368
755
|
for (const evaluation of definition.evals) {
|
|
369
756
|
const id = authoringId(evaluation);
|
|
@@ -400,7 +787,7 @@ function defineEvalMatrix(definition) {
|
|
|
400
787
|
const caseChoices = new Map(caseKeys.map((key) => [key, cases.map((assignment) => assignment[key])]));
|
|
401
788
|
function select(selection = {}) {
|
|
402
789
|
for (const key of Object.keys(selection.parameters ?? {})) if (!declared.has(key)) throw new Error(`Unknown matrix axis: ${key}`);
|
|
403
|
-
for (const key of Object.keys(selection.overrides ?? {})) if (declared.has(key)) throw new Error(
|
|
790
|
+
for (const key of Object.keys(selection.overrides ?? {})) if (declared.has(key)) throw new Error(`${key} is a matrix axis: pick its value with -a ${key}=<value> (--select), not --param`);
|
|
404
791
|
canonicalParameters(selection.overrides ?? {});
|
|
405
792
|
for (const name of selection.evals ?? []) if (!definition.evals.some((e) => e.id === name)) throw new Error(`Unknown matrix eval: ${name}`);
|
|
406
793
|
const evals = definition.evals.filter((e) => !selection.evals || selection.evals.includes(e.id));
|
|
@@ -493,88 +880,76 @@ function defineEvalMatrix(definition) {
|
|
|
493
880
|
};
|
|
494
881
|
}
|
|
495
882
|
//#endregion
|
|
496
|
-
//#region ../core/src/
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
timestamp
|
|
507
|
-
};
|
|
508
|
-
case "tool-call": return {
|
|
509
|
-
kind: "tool-call",
|
|
510
|
-
id: input.id,
|
|
511
|
-
name: input.name,
|
|
512
|
-
arguments: input.arguments === void 0 ? {} : toJson(input.arguments, "event.arguments"),
|
|
513
|
-
timestamp
|
|
514
|
-
};
|
|
515
|
-
case "tool-result": return {
|
|
516
|
-
kind: "tool-result",
|
|
517
|
-
id: input.id,
|
|
518
|
-
...input.name === void 0 ? {} : { name: input.name },
|
|
519
|
-
result: input.result === void 0 ? null : toJson(input.result, "event.result"),
|
|
520
|
-
timestamp
|
|
521
|
-
};
|
|
522
|
-
case "completed": return {
|
|
523
|
-
kind: "completed",
|
|
524
|
-
...input.output === void 0 ? {} : { output: toJson(input.output, "event.output") },
|
|
525
|
-
timestamp
|
|
526
|
-
};
|
|
527
|
-
case "started":
|
|
528
|
-
case "turn-started":
|
|
529
|
-
case "turn-completed":
|
|
530
|
-
case "error": return {
|
|
531
|
-
...input,
|
|
532
|
-
timestamp
|
|
533
|
-
};
|
|
534
|
-
default: throw new Error(`Unknown AUT event kind: ${String(input.kind)}`);
|
|
535
|
-
}
|
|
883
|
+
//#region ../core/src/isolation.ts
|
|
884
|
+
const NETWORKS = [
|
|
885
|
+
"none",
|
|
886
|
+
"fixtures",
|
|
887
|
+
"full"
|
|
888
|
+
];
|
|
889
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
890
|
+
/** No isolation: commands run on the host in the run's workspace. */
|
|
891
|
+
function none() {
|
|
892
|
+
return { kind: "none" };
|
|
536
893
|
}
|
|
537
894
|
/**
|
|
538
|
-
*
|
|
539
|
-
*
|
|
895
|
+
* Container isolation through the `docker` CLI, or a compatible one such as
|
|
896
|
+
* podman's: a container per run, the candidate workspace mounted at
|
|
897
|
+
* `/workspace`, the evaluator workspace never mounted, and its own process
|
|
898
|
+
* table, so a `pkill` reaches only that run.
|
|
540
899
|
*/
|
|
541
|
-
function
|
|
542
|
-
if (typeof
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
900
|
+
function container(options) {
|
|
901
|
+
if (typeof options?.image !== "string" || !options.image.trim()) throw new Error("container() needs an image: a name such as node:22, or a Dockerfile path");
|
|
902
|
+
const network = options.network ?? "none";
|
|
903
|
+
if (!NETWORKS.includes(network)) throw new Error(`container() network must be ${NETWORKS.join(", ")}; received ${String(network)}`);
|
|
904
|
+
const env = [...options.env ?? []];
|
|
905
|
+
for (const name of env) if (!ENV_NAME.test(name)) throw new Error(`container() env takes variable names, such as API_KEY; received ${JSON.stringify(name)}`);
|
|
906
|
+
const mounts = (options.mounts ?? []).map(parseMount);
|
|
907
|
+
return {
|
|
908
|
+
kind: "container",
|
|
909
|
+
image: options.image.trim(),
|
|
910
|
+
network,
|
|
911
|
+
runtime: options.runtime ?? "docker",
|
|
912
|
+
env,
|
|
913
|
+
mounts
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function parseMount(mount) {
|
|
917
|
+
const parsed = typeof mount === "string" ? (() => {
|
|
918
|
+
const at = mount.lastIndexOf(":");
|
|
919
|
+
return at > 0 ? {
|
|
920
|
+
host: mount.slice(0, at),
|
|
921
|
+
path: mount.slice(at + 1)
|
|
922
|
+
} : void 0;
|
|
923
|
+
})() : mount;
|
|
924
|
+
if (!parsed?.host || !parsed.path?.startsWith("/")) throw new Error(`container() mounts take { host, path } with an absolute container path, or 'host:/path'; received ${JSON.stringify(mount)}`);
|
|
925
|
+
if (parsed.path === "/workspace" || parsed.path.startsWith("/workspace/")) throw new Error(`container() cannot mount over /workspace, the run's workspace: ${parsed.path}`);
|
|
926
|
+
return {
|
|
927
|
+
host: parsed.host,
|
|
928
|
+
path: parsed.path
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* macOS `sandbox-exec` isolation, for projects that cannot use containers:
|
|
933
|
+
* the run cannot read or write the project, the home directory or other
|
|
934
|
+
* runs' workspaces, except its own workspace and `allow`, and can signal
|
|
935
|
+
* only processes it started.
|
|
936
|
+
*/
|
|
937
|
+
function sandboxExec(options = {}) {
|
|
938
|
+
for (const name of options.env ?? []) if (!ENV_NAME.test(name)) throw new Error(`sandboxExec() env takes variable names, such as API_KEY; received ${JSON.stringify(name)}`);
|
|
939
|
+
return {
|
|
940
|
+
kind: "sandbox-exec",
|
|
941
|
+
allow: [...options.allow ?? []],
|
|
942
|
+
read: [...options.read ?? []],
|
|
943
|
+
deny: [...options.deny ?? []],
|
|
944
|
+
...options.env ? { env: [...options.env] } : {}
|
|
945
|
+
};
|
|
571
946
|
}
|
|
572
947
|
//#endregion
|
|
573
948
|
//#region ../core/src/schema.ts
|
|
574
949
|
/** Runtime schemas for serialized EvalKit report and API boundaries. */
|
|
575
|
-
const RunStatusSchema = Schema.Literal("running", "passed", "failed", "errored", "cancelled");
|
|
950
|
+
const RunStatusSchema = Schema.Literal("running", "passed", "failed", "errored", "cancelled", "limited");
|
|
576
951
|
/** The report schema version this EvalKit writes and reads. */
|
|
577
|
-
const REPORT_SCHEMA_VERSION =
|
|
952
|
+
const REPORT_SCHEMA_VERSION = 7;
|
|
578
953
|
const RecordedErrorSchema = Schema.Struct({
|
|
579
954
|
name: Schema.String,
|
|
580
955
|
message: Schema.String,
|
|
@@ -623,8 +998,23 @@ const RunDatasetSchema = Schema.Struct({
|
|
|
623
998
|
source: Schema.Literal("static", "computed"),
|
|
624
999
|
digest: Schema.String
|
|
625
1000
|
});
|
|
1001
|
+
const LimitsSchema = Schema.Struct({
|
|
1002
|
+
steps: Schema.optional(Schema.Number),
|
|
1003
|
+
tokens: Schema.optional(Schema.Number),
|
|
1004
|
+
time: Schema.optional(Schema.Number)
|
|
1005
|
+
});
|
|
1006
|
+
const LimitReachedSchema = Schema.Struct({
|
|
1007
|
+
limit: Schema.Literal("steps", "tokens", "time"),
|
|
1008
|
+
value: Schema.Number,
|
|
1009
|
+
used: Schema.Number
|
|
1010
|
+
});
|
|
1011
|
+
const RunIsolationSchema = Schema.Union(Schema.Struct({
|
|
1012
|
+
kind: Schema.Literal("container"),
|
|
1013
|
+
image: Schema.String,
|
|
1014
|
+
network: Schema.Literal("none", "fixtures", "full")
|
|
1015
|
+
}), Schema.Struct({ kind: Schema.Literal("sandbox-exec") }));
|
|
626
1016
|
const RunMetadataSchema = Schema.Struct({
|
|
627
|
-
schemaVersion: Schema.Literal(
|
|
1017
|
+
schemaVersion: Schema.Literal(7),
|
|
628
1018
|
runUri: resourceUriSchema("run"),
|
|
629
1019
|
evalId: Schema.String,
|
|
630
1020
|
suiteId: Schema.optional(Schema.String),
|
|
@@ -640,19 +1030,37 @@ const RunMetadataSchema = Schema.Struct({
|
|
|
640
1030
|
batch: Schema.optional(BatchSchema),
|
|
641
1031
|
/** The effective snapshot ignore patterns: the project's, then the scenario's. */
|
|
642
1032
|
workspaceIgnore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
|
643
|
-
datasets: Schema.optional(Schema.mutable(Schema.Array(RunDatasetSchema)))
|
|
1033
|
+
datasets: Schema.optional(Schema.mutable(Schema.Array(RunDatasetSchema))),
|
|
1034
|
+
limits: Schema.optional(LimitsSchema),
|
|
1035
|
+
isolation: Schema.optional(RunIsolationSchema)
|
|
644
1036
|
});
|
|
645
1037
|
/** On-disk manifests include a lifecycle status. */
|
|
646
1038
|
const RunManifestSchema = Schema.Struct({
|
|
647
1039
|
...RunMetadataSchema.fields,
|
|
648
1040
|
status: Schema.optional(RunStatusSchema)
|
|
649
1041
|
});
|
|
1042
|
+
/** ATXF usage: disjoint buckets, `input` without cached tokens. */
|
|
650
1043
|
const UsageSchema = Schema.Struct({
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1044
|
+
input: Schema.Number,
|
|
1045
|
+
output: Schema.Number,
|
|
1046
|
+
cacheRead: Schema.optional(Schema.Number),
|
|
1047
|
+
cacheWrite: Schema.optional(Schema.Number),
|
|
1048
|
+
reasoning: Schema.optional(Schema.Number),
|
|
1049
|
+
total: Schema.optional(Schema.Number),
|
|
1050
|
+
cost: Schema.optional(Schema.Struct({
|
|
1051
|
+
currency: Schema.optional(Schema.String),
|
|
1052
|
+
input: Schema.optional(Schema.Number),
|
|
1053
|
+
output: Schema.optional(Schema.Number),
|
|
1054
|
+
cacheRead: Schema.optional(Schema.Number),
|
|
1055
|
+
cacheWrite: Schema.optional(Schema.Number),
|
|
1056
|
+
total: Schema.Number
|
|
1057
|
+
}))
|
|
655
1058
|
});
|
|
1059
|
+
/**
|
|
1060
|
+
* An ATXF transcript in JSON (`{ header, records }`). Its records are checked
|
|
1061
|
+
* by the ATXF validator where they are written, not here.
|
|
1062
|
+
*/
|
|
1063
|
+
const TranscriptSchema = Schema.declare((value) => typeof value === "object" && value !== null && typeof value.header === "object" && value.header?.type === "transcript" && Array.isArray(value.records), { identifier: "Transcript" });
|
|
656
1064
|
const JudgeRunInfoSchema = Schema.Struct({
|
|
657
1065
|
agent: Schema.optional(Schema.Struct({
|
|
658
1066
|
id: Schema.String,
|
|
@@ -660,9 +1068,33 @@ const JudgeRunInfoSchema = Schema.Struct({
|
|
|
660
1068
|
name: Schema.optional(Schema.String),
|
|
661
1069
|
version: Schema.optional(Schema.String)
|
|
662
1070
|
})),
|
|
1071
|
+
model: Schema.optional(Schema.String),
|
|
663
1072
|
usage: Schema.optional(UsageSchema),
|
|
664
|
-
|
|
1073
|
+
prompt: Schema.optional(Schema.String),
|
|
1074
|
+
reply: Schema.optional(JsonValueSchema),
|
|
1075
|
+
attempts: Schema.optional(Schema.Number),
|
|
1076
|
+
/** The judge's own session, an ATXF transcript (checked when written). */
|
|
1077
|
+
transcript: Schema.optional(TranscriptSchema)
|
|
665
1078
|
});
|
|
1079
|
+
const CriterionResultSchema = Schema.Union(Schema.Struct({
|
|
1080
|
+
name: Schema.String,
|
|
1081
|
+
value: Schema.Number,
|
|
1082
|
+
weight: Schema.optional(Schema.Number),
|
|
1083
|
+
explanation: Schema.optional(Schema.String),
|
|
1084
|
+
evidence: Schema.optional(JsonValueSchema)
|
|
1085
|
+
}), Schema.Struct({
|
|
1086
|
+
name: Schema.String,
|
|
1087
|
+
reached: Schema.Literal(false),
|
|
1088
|
+
weight: Schema.optional(Schema.Number),
|
|
1089
|
+
explanation: Schema.optional(Schema.String)
|
|
1090
|
+
}));
|
|
1091
|
+
/** What every score and checkpoint result can carry about its verdict. */
|
|
1092
|
+
const verdictFields = {
|
|
1093
|
+
passAt: Schema.optional(Schema.Number),
|
|
1094
|
+
choice: Schema.optional(Schema.String),
|
|
1095
|
+
criteria: Schema.optional(Schema.mutable(Schema.Array(CriterionResultSchema))),
|
|
1096
|
+
attachments: Schema.optional(Schema.mutable(Schema.Array(Schema.String)))
|
|
1097
|
+
};
|
|
666
1098
|
const ScoreResultSchema = Schema.Struct({
|
|
667
1099
|
name: Schema.String,
|
|
668
1100
|
kind: Schema.Literal("predicate", "judge"),
|
|
@@ -670,6 +1102,7 @@ const ScoreResultSchema = Schema.Struct({
|
|
|
670
1102
|
passed: Schema.optional(Schema.Boolean),
|
|
671
1103
|
explanation: Schema.optional(Schema.String),
|
|
672
1104
|
evidence: Schema.optional(JsonValueSchema),
|
|
1105
|
+
...verdictFields,
|
|
673
1106
|
judge: Schema.optional(JudgeRunInfoSchema),
|
|
674
1107
|
durationMs: Schema.Number,
|
|
675
1108
|
error: Schema.optional(RecordedErrorSchema),
|
|
@@ -684,11 +1117,12 @@ const CheckpointResultSchema = Schema.Struct({
|
|
|
684
1117
|
passed: Schema.optional(Schema.Boolean),
|
|
685
1118
|
explanation: Schema.optional(Schema.String),
|
|
686
1119
|
evidence: Schema.optional(JsonValueSchema),
|
|
1120
|
+
...verdictFields,
|
|
687
1121
|
judge: Schema.optional(JudgeRunInfoSchema),
|
|
688
1122
|
durationMs: Schema.optional(Schema.Number),
|
|
689
1123
|
error: Schema.optional(RecordedErrorSchema),
|
|
690
1124
|
matchedToolCall: Schema.optional(Schema.Struct({
|
|
691
|
-
|
|
1125
|
+
messageId: Schema.String,
|
|
692
1126
|
id: Schema.String
|
|
693
1127
|
}))
|
|
694
1128
|
});
|
|
@@ -725,164 +1159,92 @@ const RunSummarySchema = Schema.Struct({
|
|
|
725
1159
|
scoring: Schema.optional(RunScoringSchema),
|
|
726
1160
|
artifacts: Schema.optional(Schema.mutable(Schema.Array(ArtifactEntrySchema))),
|
|
727
1161
|
attachments: Schema.optional(Schema.mutable(Schema.Array(AttachmentEntrySchema))),
|
|
728
|
-
error: Schema.optional(RecordedErrorSchema)
|
|
1162
|
+
error: Schema.optional(RecordedErrorSchema),
|
|
1163
|
+
limit: Schema.optional(LimitReachedSchema)
|
|
729
1164
|
});
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
timestamp: Schema.String
|
|
735
|
-
}),
|
|
736
|
-
message: Schema.Struct({
|
|
737
|
-
source: Schema.Literal("aut"),
|
|
738
|
-
kind: Schema.Literal("message"),
|
|
739
|
-
role: Schema.Literal("system", "user", "assistant", "tool"),
|
|
740
|
-
content: JsonValueSchema,
|
|
741
|
-
timestamp: Schema.String
|
|
742
|
-
}),
|
|
743
|
-
toolCall: Schema.Struct({
|
|
744
|
-
source: Schema.Literal("aut"),
|
|
745
|
-
kind: Schema.Literal("tool-call"),
|
|
746
|
-
id: Schema.String,
|
|
747
|
-
name: Schema.String,
|
|
748
|
-
arguments: JsonValueSchema,
|
|
749
|
-
timestamp: Schema.String
|
|
750
|
-
}),
|
|
751
|
-
toolResult: Schema.Struct({
|
|
752
|
-
source: Schema.Literal("aut"),
|
|
753
|
-
kind: Schema.Literal("tool-result"),
|
|
754
|
-
id: Schema.String,
|
|
755
|
-
name: Schema.optional(Schema.String),
|
|
756
|
-
result: JsonValueSchema,
|
|
757
|
-
timestamp: Schema.String
|
|
758
|
-
}),
|
|
759
|
-
turnStarted: Schema.Struct({
|
|
760
|
-
source: Schema.Literal("aut"),
|
|
761
|
-
kind: Schema.Literal("turn-started"),
|
|
762
|
-
turn: Schema.Number,
|
|
763
|
-
timestamp: Schema.String
|
|
764
|
-
}),
|
|
765
|
-
turnCompleted: Schema.Struct({
|
|
766
|
-
source: Schema.Literal("aut"),
|
|
767
|
-
kind: Schema.Literal("turn-completed"),
|
|
768
|
-
turn: Schema.Number,
|
|
769
|
-
timestamp: Schema.String,
|
|
770
|
-
latencyMs: Schema.optional(Schema.Number),
|
|
771
|
-
usage: Schema.optional(UsageSchema)
|
|
772
|
-
}),
|
|
773
|
-
completed: Schema.Struct({
|
|
774
|
-
source: Schema.Literal("aut"),
|
|
775
|
-
kind: Schema.Literal("completed"),
|
|
776
|
-
output: Schema.optional(JsonValueSchema),
|
|
777
|
-
timestamp: Schema.String
|
|
778
|
-
}),
|
|
779
|
-
error: Schema.Struct({
|
|
780
|
-
source: Schema.Literal("aut"),
|
|
781
|
-
kind: Schema.Literal("error"),
|
|
782
|
-
error: RecordedErrorSchema,
|
|
783
|
-
timestamp: Schema.String
|
|
784
|
-
})
|
|
1165
|
+
const runEventFields = {
|
|
1166
|
+
/** The transcript record it happened after; null before the first. */
|
|
1167
|
+
at: Schema.NullOr(Schema.String),
|
|
1168
|
+
timestamp: Schema.String
|
|
785
1169
|
};
|
|
786
1170
|
const runnerEvent = {
|
|
787
1171
|
runStarted: Schema.Struct({
|
|
788
|
-
source: Schema.Literal("runner"),
|
|
789
1172
|
kind: Schema.Literal("run-started"),
|
|
790
|
-
|
|
1173
|
+
...runEventFields
|
|
791
1174
|
}),
|
|
792
1175
|
stepStarted: Schema.Struct({
|
|
793
|
-
source: Schema.Literal("runner"),
|
|
794
1176
|
kind: Schema.Literal("transcript-step-started"),
|
|
795
1177
|
step: Schema.Number,
|
|
796
1178
|
message: Schema.optional(Schema.String),
|
|
797
|
-
|
|
1179
|
+
...runEventFields
|
|
798
1180
|
}),
|
|
799
1181
|
stepCompleted: Schema.Struct({
|
|
800
|
-
source: Schema.Literal("runner"),
|
|
801
1182
|
kind: Schema.Literal("transcript-step-completed"),
|
|
802
1183
|
step: Schema.Number,
|
|
803
|
-
|
|
1184
|
+
...runEventFields
|
|
804
1185
|
}),
|
|
805
1186
|
stepSkipped: Schema.Struct({
|
|
806
|
-
source: Schema.Literal("runner"),
|
|
807
1187
|
kind: Schema.Literal("transcript-step-skipped"),
|
|
808
1188
|
step: Schema.Number,
|
|
809
|
-
|
|
1189
|
+
...runEventFields
|
|
810
1190
|
}),
|
|
811
1191
|
checkpointStarted: Schema.Struct({
|
|
812
|
-
source: Schema.Literal("runner"),
|
|
813
1192
|
kind: Schema.Literal("checkpoint-started"),
|
|
814
1193
|
step: Schema.Number,
|
|
815
1194
|
name: Schema.String,
|
|
816
|
-
|
|
1195
|
+
...runEventFields
|
|
817
1196
|
}),
|
|
818
1197
|
checkpointCompleted: Schema.Struct({
|
|
819
|
-
source: Schema.Literal("runner"),
|
|
820
1198
|
kind: Schema.Literal("checkpoint-completed"),
|
|
821
1199
|
step: Schema.Number,
|
|
822
1200
|
status: Schema.Literal("passed", "failed", "skipped"),
|
|
823
|
-
|
|
1201
|
+
...runEventFields
|
|
824
1202
|
}),
|
|
825
1203
|
checkpointError: Schema.Struct({
|
|
826
|
-
source: Schema.Literal("runner"),
|
|
827
1204
|
kind: Schema.Literal("checkpoint-error"),
|
|
828
1205
|
step: Schema.Number,
|
|
829
1206
|
error: RecordedErrorSchema,
|
|
830
|
-
|
|
1207
|
+
...runEventFields
|
|
831
1208
|
}),
|
|
832
1209
|
scorerStarted: Schema.Struct({
|
|
833
|
-
source: Schema.Literal("runner"),
|
|
834
1210
|
kind: Schema.Literal("scorer-started"),
|
|
835
1211
|
scorer: Schema.String,
|
|
836
|
-
|
|
1212
|
+
...runEventFields
|
|
837
1213
|
}),
|
|
838
1214
|
scorerCompleted: Schema.Struct({
|
|
839
|
-
source: Schema.Literal("runner"),
|
|
840
1215
|
kind: Schema.Literal("scorer-completed"),
|
|
841
1216
|
scorer: Schema.String,
|
|
842
1217
|
value: Schema.optional(Schema.Number),
|
|
843
1218
|
skipped: Schema.optional(Schema.Literal(true)),
|
|
844
|
-
|
|
1219
|
+
...runEventFields
|
|
845
1220
|
}),
|
|
846
1221
|
scorerFailed: Schema.Struct({
|
|
847
|
-
source: Schema.Literal("runner"),
|
|
848
1222
|
kind: Schema.Literal("scorer-failed"),
|
|
849
1223
|
scorer: Schema.String,
|
|
850
1224
|
error: RecordedErrorSchema,
|
|
851
|
-
|
|
1225
|
+
...runEventFields
|
|
1226
|
+
}),
|
|
1227
|
+
limitReached: Schema.Struct({
|
|
1228
|
+
kind: Schema.Literal("limit-reached"),
|
|
1229
|
+
...LimitReachedSchema.fields,
|
|
1230
|
+
...runEventFields
|
|
852
1231
|
}),
|
|
853
1232
|
runCompleted: Schema.Struct({
|
|
854
|
-
source: Schema.Literal("runner"),
|
|
855
1233
|
kind: Schema.Literal("run-completed"),
|
|
856
|
-
|
|
1234
|
+
...runEventFields
|
|
857
1235
|
}),
|
|
858
1236
|
error: Schema.Struct({
|
|
859
|
-
source: Schema.Literal("runner"),
|
|
860
1237
|
kind: Schema.Literal("error"),
|
|
861
1238
|
error: RecordedErrorSchema,
|
|
862
|
-
|
|
1239
|
+
...runEventFields
|
|
863
1240
|
})
|
|
864
1241
|
};
|
|
865
|
-
const
|
|
1242
|
+
const RunEventSchema = Schema.Union(...Object.values(runnerEvent));
|
|
866
1243
|
const ApiErrorSchema = Schema.Struct({
|
|
867
1244
|
error: Schema.String,
|
|
868
1245
|
message: Schema.String
|
|
869
1246
|
});
|
|
870
1247
|
//#endregion
|
|
871
|
-
//#region ../core/src/trajectory.ts
|
|
872
|
-
/** Decode one private report's JSONL trajectory; reject malformed/non-event lines with their line number. */
|
|
873
|
-
function parseTrajectoryJsonl(contents) {
|
|
874
|
-
const events = [];
|
|
875
|
-
for (const [index, line] of contents.split(/\r?\n/).entries()) {
|
|
876
|
-
if (!line.trim()) continue;
|
|
877
|
-
try {
|
|
878
|
-
events.push(Schema.decodeUnknownSync(TrajectoryEventSchema)(JSON.parse(line)));
|
|
879
|
-
} catch (cause) {
|
|
880
|
-
throw new Error(`Invalid trajectory JSONL at line ${index + 1}`, { cause });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
return events;
|
|
884
|
-
}
|
|
885
|
-
//#endregion
|
|
886
1248
|
//#region ../core/src/index.ts
|
|
887
1249
|
internalModule(import.meta.url);
|
|
888
1250
|
/** Used when an agent declares no runtimes. */
|
|
@@ -938,12 +1300,14 @@ function skip(reason) {
|
|
|
938
1300
|
/** A scorer in code. Its `evidence` type carries through to `checkpoint(rule)`. */
|
|
939
1301
|
function predicate(name, run, options = {}) {
|
|
940
1302
|
if (!name.trim()) throw new Error("Predicate name must not be empty");
|
|
941
|
-
|
|
1303
|
+
assertPassAt(`Predicate ${name}`, options.passAt);
|
|
1304
|
+
const { criteria: declared, ...rest } = options;
|
|
942
1305
|
return markDefinitionSite({
|
|
943
1306
|
kind: "predicate",
|
|
944
1307
|
name,
|
|
945
1308
|
run,
|
|
946
|
-
...
|
|
1309
|
+
...rest,
|
|
1310
|
+
...declared ? { criteria: declaredCriteria(Object.fromEntries(Object.entries(declared).map(([key, option]) => [key, typeof option === "string" ? { description: option } : option]))) } : {}
|
|
947
1311
|
});
|
|
948
1312
|
}
|
|
949
1313
|
/** A built-in metric's name is a type error here, as well as at definition time. */
|
|
@@ -956,18 +1320,28 @@ function metric(name, run, options = {}) {
|
|
|
956
1320
|
...options
|
|
957
1321
|
});
|
|
958
1322
|
}
|
|
1323
|
+
const INPUT_NAME = /^[a-z][a-zA-Z0-9_-]{0,31}$/;
|
|
959
1324
|
function judge(name, options) {
|
|
960
|
-
if (!name.trim() || !options.rubric
|
|
1325
|
+
if (!name.trim() || !options.rubric?.trim()) throw new Error("Judge name and rubric must not be empty");
|
|
1326
|
+
assertPassAt(`Judge ${name}`, options.passAt);
|
|
1327
|
+
for (const input of Object.keys(options.inputs ?? {})) if (!INPUT_NAME.test(input)) throw new Error(`Judge ${name} input ${JSON.stringify(input)} must start with a lowercase letter and use letters, digits, "-" or "_"`);
|
|
1328
|
+
if (options.inputs && !Object.keys(options.inputs).length) throw new Error(`Judge ${name} inputs must name at least one input`);
|
|
1329
|
+
if (options.agent && typeof options.agent.start !== "function") throw new Error(`Judge ${name} agent must be made with defineAgent()`);
|
|
961
1330
|
return markDefinitionSite({
|
|
962
1331
|
kind: "judge",
|
|
963
1332
|
name,
|
|
964
|
-
...options
|
|
1333
|
+
...options,
|
|
1334
|
+
answer: options.answer ?? { kind: "verdict" }
|
|
965
1335
|
});
|
|
966
1336
|
}
|
|
1337
|
+
function assertPassAt(owner, passAt) {
|
|
1338
|
+
if (passAt !== void 0 && !(Number.isFinite(passAt) && passAt >= 0 && passAt <= 1)) throw new Error(`${owner} passAt must be a number from 0 to 1; received ${passAt}`);
|
|
1339
|
+
}
|
|
967
1340
|
function defineEval(definition) {
|
|
968
1341
|
authoringId(definition);
|
|
969
1342
|
assertNoRemovedOptions(definition);
|
|
970
1343
|
if (definition.metadata !== void 0) toJsonObject(definition.metadata, `${definition.id}.metadata`);
|
|
1344
|
+
parseLimits(definition.limits, `Eval ${definition.id}`);
|
|
971
1345
|
for (const [index, pattern] of (definition.workspace?.ignore ?? []).entries()) if (typeof pattern !== "string" || pattern.replace(/^!/, "").trim() === "") throw new Error(`Eval ${definition.id} workspace.ignore[${index}] must be a non-empty glob, got ${JSON.stringify(pattern)}`);
|
|
972
1346
|
const datasetIds = /* @__PURE__ */ new Set();
|
|
973
1347
|
for (const [index, dataset] of (definition.datasets ?? []).entries()) {
|
|
@@ -984,10 +1358,13 @@ function defineEval(definition) {
|
|
|
984
1358
|
let hasUser = false;
|
|
985
1359
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
986
1360
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
987
|
-
|
|
988
|
-
if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
|
|
1361
|
+
assertJudgesIndependent(definition);
|
|
989
1362
|
return markDefinitionSite(definition);
|
|
990
1363
|
}
|
|
1364
|
+
/** A judge never grades with the agent it is judging. */
|
|
1365
|
+
function assertJudgesIndependent(definition, defaultJudge) {
|
|
1366
|
+
for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && (rule.agent ?? defaultJudge) === definition.agent) throw new Error(`Judge ${rule.name} in ${definition.id} uses the agent under test; a judge needs its own agent`);
|
|
1367
|
+
}
|
|
991
1368
|
function defineSuite(suite) {
|
|
992
1369
|
authoringId(suite);
|
|
993
1370
|
return suite;
|
|
@@ -1061,16 +1438,25 @@ function registerEvals(registrations) {
|
|
|
1061
1438
|
* Metrics EvalKit records for every run; scenario metrics may not reuse
|
|
1062
1439
|
* these names.
|
|
1063
1440
|
*
|
|
1064
|
-
* - `agent time` (ms): from the
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* - `
|
|
1441
|
+
* - `agent time` (ms): from the session's start to the end of its last turn.
|
|
1442
|
+
* - `tokens`: uncached input, cache writes and output, summed over the
|
|
1443
|
+
* transcript (`uncachedTokens`). Absent when nothing reported usage.
|
|
1444
|
+
* - `cached tokens`: input tokens read from a prompt cache.
|
|
1445
|
+
* - `output tokens`: output, reasoning included.
|
|
1446
|
+
* - `cache hit rate`: the share of input tokens read from a cache, 0 to 1.
|
|
1447
|
+
* - `steps`: assistant messages, one per model call.
|
|
1448
|
+
* - `tool calls`: tool calls in the transcript.
|
|
1449
|
+
* - `judge tokens`: `tokens` over the judges' transcripts, never the agent's.
|
|
1069
1450
|
*/
|
|
1070
1451
|
const BUILT_IN_METRICS = [
|
|
1071
1452
|
"agent time",
|
|
1072
1453
|
"tokens",
|
|
1073
|
-
"
|
|
1454
|
+
"cached tokens",
|
|
1455
|
+
"output tokens",
|
|
1456
|
+
"cache hit rate",
|
|
1457
|
+
"steps",
|
|
1458
|
+
"tool calls",
|
|
1459
|
+
"judge tokens"
|
|
1074
1460
|
];
|
|
1075
1461
|
function recordError(error) {
|
|
1076
1462
|
if (error instanceof Error) return {
|
|
@@ -1083,5 +1469,35 @@ function recordError(error) {
|
|
|
1083
1469
|
message: String(error)
|
|
1084
1470
|
};
|
|
1085
1471
|
}
|
|
1472
|
+
/** Messages on the active path of what the agent did, optionally of one role. */
|
|
1473
|
+
function messages(scoring, role) {
|
|
1474
|
+
return transcriptMessages(scoring.transcript, role).map((record) => ({
|
|
1475
|
+
id: record.id,
|
|
1476
|
+
role: record.role,
|
|
1477
|
+
text: contentText(record.content),
|
|
1478
|
+
time: record.time,
|
|
1479
|
+
record
|
|
1480
|
+
}));
|
|
1481
|
+
}
|
|
1482
|
+
/** The agent's tool calls, in order, each with its result when recorded; optionally of one tool. */
|
|
1483
|
+
function toolCalls(scoring, name) {
|
|
1484
|
+
return transcriptToolCalls(scoring.transcript, name);
|
|
1485
|
+
}
|
|
1486
|
+
/** The text of the agent's last reply, or `''`. */
|
|
1487
|
+
function lastReply(scoring) {
|
|
1488
|
+
return transcriptReply(scoring.transcript);
|
|
1489
|
+
}
|
|
1490
|
+
/** Everything the agent's transcript reported using; undefined when nothing reported usage. */
|
|
1491
|
+
function usage(scoring) {
|
|
1492
|
+
return transcriptUsage(scoring.transcript);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* What the model saw at the end of the agent's transcript, rebuilt by ATXF
|
|
1496
|
+
* (compactions and context edits applied): its instructions, tools and
|
|
1497
|
+
* messages.
|
|
1498
|
+
*/
|
|
1499
|
+
function modelContext(scoring) {
|
|
1500
|
+
return buildContext(scoring.transcript);
|
|
1501
|
+
}
|
|
1086
1502
|
//#endregion
|
|
1087
|
-
export {
|
|
1503
|
+
export { DEFAULT_ARTIFACT_EXCLUDE as $, JudgeRunInfoSchema as A, definitionSite as At, RunMetadataSchema as B, ApiErrorSchema as C, limitsFromParameters as Ct, BatchSchema as D, defineDataset as Dt, AutIdentitySchema as E, resolveLimits as Et, RecordedErrorSchema as F, parseResourceUri as Ft, ScoreResultSchema as G, RunSourceSchema as H, RunDatasetSchema as I, resourceUri as It, container as J, TranscriptSchema as K, RunEventSchema as L, resourceUriSchema as Lt, LimitsSchema as M, ResourceUriSchema as Mt, MetricResultSchema as N, UuidSchema as Nt, CheckpointResultSchema as O, isDataset as Ot, REPORT_SCHEMA_VERSION as P, authorId as Pt, canonicalParameters as Q, RunIsolationSchema as R, resourceUuid as Rt, user as S, formatLimit as St, AttachmentEntrySchema as T, parseLimits as Tt, RunStatusSchema as U, RunScoringSchema as V, RunSummarySchema as W, sandboxExec as X, none as Y, defineEvalMatrix as Z, recordError as _, contentText as _t, assertJudgesIndependent as a, choices as at, toolCalls as b, parseModel as bt, defineEval as c, judgePrompt as ct, judge as d, weightedMean as dt, DEFAULT_ATTACHMENT_LIMITS as et, lastReply as f, NotJsonError as ft, predicate as g, callsIn as gt, modelContext as h, activePath as ht, agentRuntimes as i, answerInstruction as it, LimitReachedSchema as j, ResourceKindSchema as jt, JsonValueSchema as k, relativeDatasetPath as kt, defineSuite as l, passMark as lt, metric as m, toJsonObject as mt, DEFAULT_AGENT_RUNTIMES as n, catalogEntry as nt, authoringId as o, criteria as ot, messages as p, toJson as pt, UsageSchema as q, agent as r, UnreadableAnswer as rt, defineAgent as s, judgeInputNames as st, BUILT_IN_METRICS as t, defineConfig as tt, expectToolCall as u, readJudgeAnswer as ut, registerEvals as v, isTreeRecord as vt, ArtifactEntrySchema as w, parseLimitFlag as wt, usage as x, toolResultValue as xt, skip as y, modelName as yt, RunManifestSchema as z };
|