@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.
@@ -1,5 +1,6 @@
1
- import { n as assertNoRemovedOptions } from "./upgrade-60SIRius.mjs";
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
- ...policy.timeoutMs !== void 0 ? { timeoutMs: policy.timeoutMs } : {},
226
- ...policy.failfast !== void 0 ? { failfast: policy.failfast } : {}
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.kind === "predicate" && rule.passAt !== void 0 ? { passAt: rule.passAt } : {},
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" ? { rubric: rule.rubric } : {},
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 catalogStep(step, at) {
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(definition) {
366
- authoringId(definition);
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(`Select axis ${key}; do not override it`);
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/aut-events.ts
497
- /** Converts an adapter event into a recorded `AutEvent`. */
498
- function normalizeAutEvent(input, now) {
499
- if (typeof input !== "object" || input === null) throw new Error("AUT events must be objects");
500
- const timestamp = input.timestamp ?? now().toISOString();
501
- switch (input.kind) {
502
- case "message": return {
503
- kind: "message",
504
- role: input.role,
505
- content: toJson(input.content, "event.content"),
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
- * The events a returned turn result stands for: each tool call (and its
539
- * result), then the reply, then the usage as a `turn-completed` event.
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 turnResultEvents(result, turn, now) {
542
- if (typeof result === "string") result = { reply: result };
543
- if (typeof result !== "object" || result === null) throw new Error("send() must return nothing, a string, or a turn result");
544
- const events = [];
545
- for (const [index, call] of (result.toolCalls ?? []).entries()) {
546
- const id = call.id ?? `turn-${turn}-call-${index + 1}`;
547
- events.push(normalizeAutEvent({
548
- kind: "tool-call",
549
- id,
550
- name: call.name,
551
- arguments: call.arguments
552
- }, now));
553
- if (call.result !== void 0) events.push(normalizeAutEvent({
554
- kind: "tool-result",
555
- id,
556
- name: call.name,
557
- result: call.result
558
- }, now));
559
- }
560
- if (result.reply !== void 0) events.push(normalizeAutEvent({
561
- kind: "message",
562
- role: "assistant",
563
- content: result.reply
564
- }, now));
565
- if (result.usage) events.push(normalizeAutEvent({
566
- kind: "turn-completed",
567
- turn,
568
- usage: result.usage
569
- }, now));
570
- return events;
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 = 6;
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(6),
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
- inputTokens: Schema.optional(Schema.Number),
652
- outputTokens: Schema.optional(Schema.Number),
653
- cachedInputTokens: Schema.optional(Schema.Number),
654
- totalTokens: Schema.optional(Schema.Number)
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
- events: Schema.optional(Schema.mutable(Schema.Array(JsonValueSchema)))
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
- eventIndex: Schema.Number,
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 autEvent = {
731
- started: Schema.Struct({
732
- source: Schema.Literal("aut"),
733
- kind: Schema.Literal("started"),
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
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
- timestamp: Schema.String
1239
+ ...runEventFields
863
1240
  })
864
1241
  };
865
- const TrajectoryEventSchema = Schema.Union(...Object.values(autEvent), ...Object.values(runnerEvent));
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
- if (options.passAt !== void 0 && !(Number.isFinite(options.passAt) && options.passAt >= 0 && options.passAt <= 1)) throw new Error(`Predicate ${name} passAt must be a number from 0 to 1; received ${options.passAt}`);
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
- ...options
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.trim()) throw new Error("Judge name and rubric must not be empty");
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
- if ((definition.transcript.some((step) => step.kind === "judge") || definition.scoring.some((rule) => rule.kind === "judge")) && !definition.judge) throw new Error("Judge rules require an eval judge agent");
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 run's first event to its last one before
1065
- * final scoring.
1066
- * - `tokens`: the sum of `turnTokens(usage)` over the turns that reported
1067
- * usage, cached input included. Absent when no turn reported any.
1068
- * - `tool calls`: the agent's `tool-call` events.
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
- "tool calls"
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 { relativeDatasetPath as $, RunManifestSchema as A, turnResultEvents as B, CheckpointResultSchema as C, REPORT_SCHEMA_VERSION as D, MetricResultSchema as E, RunSummarySchema as F, DEFAULT_ATTACHMENT_LIMITS as G, canonicalParameters as H, ScoreResultSchema as I, NotJsonError as J, defineConfig as K, TrajectoryEventSchema as L, RunScoringSchema as M, RunSourceSchema as N, RecordedErrorSchema as O, RunStatusSchema as P, isDataset as Q, UsageSchema as R, BatchSchema as S, JudgeRunInfoSchema as T, turnTokens as U, defineEvalMatrix as V, DEFAULT_ARTIFACT_EXCLUDE as W, toJsonObject as X, toJson as Y, defineDataset as Z, parseTrajectoryJsonl as _, authoringId as a, parseResourceUri as at, AttachmentEntrySchema as b, defineSuite as c, resourceUuid as ct, metric as d, definitionSite as et, predicate as f, user as g, skip as h, agentRuntimes as i, authorId as it, RunMetadataSchema as j, RunDatasetSchema as k, expectToolCall as l, registerEvals as m, DEFAULT_AGENT_RUNTIMES as n, ResourceUriSchema as nt, defineAgent as o, resourceUri as ot, recordError as p, catalogEntry as q, agent as r, UuidSchema as rt, defineEval as s, resourceUriSchema as st, BUILT_IN_METRICS as t, ResourceKindSchema as tt, judge as u, ApiErrorSchema as v, JsonValueSchema as w, AutIdentitySchema as x, ArtifactEntrySchema as y, normalizeAutEvent as z };
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 };