@leostera/evalkit 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +93 -2
  2. package/README.md +5 -4
  3. package/dist/cli.mjs +510 -379
  4. package/dist/dashboard/assets/index-DNR4shrM.js +67 -0
  5. package/dist/dashboard/assets/index-TPYprg75.css +2 -0
  6. package/dist/dashboard/index.html +2 -2
  7. package/dist/{files-C-U0-PWW.mjs → files-DkDQs2JL.mjs} +3 -0
  8. package/dist/{index-db-vbqEGFaU.mjs → index-db-B1FeXzmS.mjs} +333 -51
  9. package/dist/index.d.mts +529 -183
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -2
  12. package/dist/runner.d.mts +447 -173
  13. package/dist/runner.d.mts.map +1 -1
  14. package/dist/runner.mjs +2 -2
  15. package/dist/share-worker.d.mts +10 -3
  16. package/dist/share-worker.d.mts.map +1 -1
  17. package/dist/share-worker.mjs +6 -3
  18. package/dist/share.d.mts +49 -7
  19. package/dist/share.d.mts.map +1 -1
  20. package/dist/share.mjs +4 -4
  21. package/dist/skills/evalkit/SKILL.md +5 -5
  22. package/dist/skills/evalkit-agents/SKILL.md +5 -3
  23. package/dist/skills/evalkit-datasets/SKILL.md +5 -5
  24. package/dist/skills/evalkit-evals/SKILL.md +5 -5
  25. package/dist/skills/evalkit-judges/SKILL.md +8 -6
  26. package/dist/skills/evalkit-matrices/SKILL.md +2 -2
  27. package/dist/skills/evalkit-results/SKILL.md +6 -6
  28. package/dist/{src-LeGSyDD2.mjs → src-B14fXAGp.mjs} +334 -73
  29. package/dist/{src-CZnDXP1K.mjs → src-Bccv9igQ.mjs} +8 -3
  30. package/dist/{src-mm8L_IxM.mjs → src-BfSQKOJP.mjs} +676 -321
  31. package/dist/upgrade-G-uFPJKg.mjs +111 -0
  32. package/package.json +1 -1
  33. package/dist/dashboard/assets/index-C12IeG2y.css +0 -2
  34. package/dist/dashboard/assets/index-xlVsW6ZR.js +0 -37
@@ -1,8 +1,9 @@
1
- import { A as RunSummarySchema, D as RunMetadataSchema, E as RunManifestSchema, F as TrialScoringSchema, H as DEFAULT_ARTIFACT_EXCLUDE, I as TrialSummarySchema, K as toJson, M as TrajectoryEventSchema, N as TrialManifestSchema, P as TrialMetadataSchema, R as normalizeAutEvent, S as JsonValueSchema, V as canonicalParameters, X as relativeDatasetPath, _ as parseTrajectoryJsonl, a as authoringId, i as agentRuntimes, j as ScoreResultSchema, p as recordError, q as toJsonObject, rt as resourceUri, x as CheckpointResultSchema, z as turnResultEvents } from "./src-LeGSyDD2.mjs";
1
+ import { $ as judgeInputNames, B as normalizeAutEvent, G as DEFAULT_ARTIFACT_EXCLUDE, I as RunSummarySchema, K as DEFAULT_ATTACHMENT_LIMITS, L as ScoreResultSchema, M as RunMetadataSchema, N as RunScoringSchema, R as TrajectoryEventSchema, T as JsonValueSchema, U as canonicalParameters, V as turnResultEvents, W as turnTokens, X as answerInstruction, Y as UnreadableAnswer, a as assertJudgesIndependent, at as toJson, et as judgePrompt, gt as resourceUri, i as agentRuntimes, j as RunManifestSchema, lt as relativeDatasetPath, m as recordError, nt as readJudgeAnswer, o as authoringId, ot as toJsonObject, rt as weightedMean, v as parseTrajectoryJsonl, w as CheckpointResultSchema } from "./src-B14fXAGp.mjs";
2
+ import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-G-uFPJKg.mjs";
2
3
  import * as Schema from "effect/Schema";
3
4
  import { Data, Effect, Either, Exit, Scope } from "effect";
4
5
  import os from "node:os";
5
- import { appendFile, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { appendFile, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
7
  import path, { join } from "node:path";
7
8
  import { createHash, randomUUID } from "node:crypto";
8
9
  //#region ../runner/src/errors.ts
@@ -12,7 +13,7 @@ var ReportError = class extends Data.TaggedError("ReportError") {};
12
13
  var ScoringError = class extends Data.TaggedError("ScoringError") {};
13
14
  var RuleExecutionError = class extends Data.TaggedError("RuleExecutionError") {};
14
15
  var CheckpointExecutionError = class extends Data.TaggedError("CheckpointExecutionError") {};
15
- /** The run was cancelled (Ctrl+C); the trial stops and is finalized as cancelled. */
16
+ /** The run was cancelled (Ctrl+C); the run stops and is finalized as cancelled. */
16
17
  var CancelledError = class extends Data.TaggedError("CancelledError") {};
17
18
  function cancelled() {
18
19
  return new CancelledError({ message: "Cancelled" });
@@ -32,6 +33,95 @@ async function unlessAborted(work, signal) {
32
33
  }
33
34
  }
34
35
  //#endregion
36
+ //#region ../runner/src/attachments.ts
37
+ const SEGMENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
38
+ const MAX_NAME = 200;
39
+ /**
40
+ * Checks an attachment name: a relative path whose segments are letters,
41
+ * digits, `.`, `_` and `-`, not starting with a dot. That rules out
42
+ * absolute paths, `..`, hidden files, separators other than `/`, and
43
+ * anything a filesystem or URL would read differently.
44
+ */
45
+ function attachmentName(name) {
46
+ if (typeof name !== "string" || !name || name.length > MAX_NAME || !name.split("/").every((segment) => SEGMENT.test(segment))) throw new Error(`Attachment name ${JSON.stringify(name)} is not allowed: use a relative path of letters, digits, ".", "_" and "-", like "logs/agent.log"`);
47
+ return name;
48
+ }
49
+ /** The last `max` bytes, starting on a whole UTF-8 character when the content is text. */
50
+ function lastBytes(bytes, max, text) {
51
+ let start = bytes.byteLength - max;
52
+ if (text) while (start < bytes.byteLength && (bytes[start] & 192) === 128) start++;
53
+ return bytes.subarray(start);
54
+ }
55
+ /**
56
+ * Saves a run's attachments as `attachments/<name>` below its artifacts.
57
+ * Content is stored as given and never interpreted. A file over
58
+ * `maxFileBytes` keeps its end (where a log's errors are); one that would
59
+ * take the run past `maxRunBytes` is rejected.
60
+ */
61
+ function attachmentStore(writer, limits = {}) {
62
+ const { maxFileBytes, maxRunBytes } = {
63
+ ...DEFAULT_ATTACHMENT_LIMITS,
64
+ ...limits
65
+ };
66
+ const entries = [];
67
+ const names = /* @__PURE__ */ new Set();
68
+ const pending = /* @__PURE__ */ new Set();
69
+ let used = 0;
70
+ let closed = false;
71
+ async function attach(name, content) {
72
+ if (closed) throw new Error(`Cannot attach ${JSON.stringify(name)}: the run has finished`);
73
+ attachmentName(name);
74
+ if (typeof content !== "string" && !(content instanceof Uint8Array)) throw new Error(`Attachment ${name} must be a string or a Uint8Array, not ${content === null ? "null" : typeof content}`);
75
+ if (names.has(name)) throw new Error(`Attachment ${name} was already attached`);
76
+ const text = typeof content === "string";
77
+ const original = text ? new TextEncoder().encode(content) : content;
78
+ const bytes = original.byteLength > maxFileBytes ? lastBytes(original, maxFileBytes, text) : original;
79
+ if (used + bytes.byteLength > maxRunBytes) throw new Error(`Attachment ${name} (${bytes.byteLength} bytes) would take this run past its ${maxRunBytes}-byte attachment limit (${used} used)`);
80
+ names.add(name);
81
+ used += bytes.byteLength;
82
+ const write = writer.writeArtifact(`attachments/${name}`, bytes);
83
+ pending.add(write);
84
+ try {
85
+ await write;
86
+ entries.push({
87
+ name,
88
+ size: bytes.byteLength,
89
+ ...bytes.byteLength < original.byteLength ? { truncatedFrom: original.byteLength } : {}
90
+ });
91
+ } catch (error) {
92
+ names.delete(name);
93
+ used -= bytes.byteLength;
94
+ throw error;
95
+ } finally {
96
+ pending.delete(write);
97
+ }
98
+ }
99
+ return {
100
+ attach,
101
+ async close() {
102
+ closed = true;
103
+ await Promise.allSettled(pending);
104
+ return [...entries].sort((a, b) => a.name.localeCompare(b.name));
105
+ }
106
+ };
107
+ }
108
+ /**
109
+ * An `attach` that remembers what it saved, so a score or checkpoint can
110
+ * link the files its scorer attached, even when the scorer then failed.
111
+ */
112
+ function trackAttachments(attach) {
113
+ const attached = [];
114
+ return {
115
+ attached,
116
+ async attach(name, content) {
117
+ await attach(name, content);
118
+ attached.push(name);
119
+ }
120
+ };
121
+ }
122
+ /** `{ attachments }` when a scorer attached any. */
123
+ const attachedFields = (attached) => attached.length ? { attachments: [...attached] } : {};
124
+ //#endregion
35
125
  //#region ../core/src/glob.ts
36
126
  /**
37
127
  * A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
@@ -104,9 +194,9 @@ function ignoreRules(patterns) {
104
194
  };
105
195
  }
106
196
  /**
107
- * Copies the candidate workspace's regular files into the trial report,
197
+ * Copies the candidate workspace's regular files into the run report,
108
198
  * leaving out ignored paths. Symbolic links and special files are skipped,
109
- * never followed, so a snapshot cannot read outside the trial. An ignored
199
+ * never followed, so a snapshot cannot read outside the run. An ignored
110
200
  * directory is walked only when a `!` pattern could re-include something in
111
201
  * it, and is listed only if it ends up containing a snapshotted file.
112
202
  */
@@ -159,7 +249,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAUL
159
249
  const loads = /* @__PURE__ */ new WeakMap();
160
250
  /**
161
251
  * Loads a dataset at most once per process and project root. A failed load is
162
- * forgotten, so the next trial that needs the dataset tries again.
252
+ * forgotten, so the next run that needs the dataset tries again.
163
253
  */
164
254
  function loadDataset(dataset, root, signal) {
165
255
  let byRoot = loads.get(dataset);
@@ -210,12 +300,12 @@ async function readValueFile(id, file) {
210
300
  }
211
301
  throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
212
302
  }
213
- /** Loads (cached), derives this trial's content with `onRun`, and resolves it. */
214
- async function prepareDatasets(datasets, root, trial) {
303
+ /** Loads (cached), derives this run's content with `onRun`, and resolves it. */
304
+ async function prepareDatasets(datasets, root, run) {
215
305
  const prepared = [];
216
306
  for (const dataset of datasets) {
217
- const loaded = await loadDataset(dataset, root, trial.signal);
218
- const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, trial) : loaded;
307
+ const loaded = await loadDataset(dataset, root, run.signal);
308
+ const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, run) : loaded;
219
309
  if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
220
310
  prepared.push(await resolveContent(dataset, content, root));
221
311
  }
@@ -283,7 +373,7 @@ async function digest(files, value) {
283
373
  if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
284
374
  return `sha256:${hash.digest("hex")}`;
285
375
  }
286
- function trialDatasets(prepared) {
376
+ function runDatasets(prepared) {
287
377
  return prepared.map(({ dataset, digest }) => ({
288
378
  id: dataset.id,
289
379
  visibility: dataset.visibility,
@@ -332,17 +422,17 @@ async function staticDatasetFile(dataset, root, file) {
332
422
  }
333
423
  //#endregion
334
424
  //#region ../runner/src/workspace.ts
335
- /** Where a trial's persistent workspace lives below `root`. */
336
- function trialWorkspacePath(root, trialId) {
337
- return path.join(path.resolve(root), trialId.replace(/[^a-zA-Z0-9._-]/g, "_"));
425
+ /** Where a run's persistent workspace lives below `root`. */
426
+ function runWorkspacePath(root, runId) {
427
+ return path.join(path.resolve(root), runId.replace(/[^a-zA-Z0-9._-]/g, "_"));
338
428
  }
339
429
  /**
340
430
  * Creates isolated candidate and evaluator directories and writes each
341
431
  * dataset's files below its mount, on the side its visibility allows.
342
432
  * Two datasets writing the same file is an error, never an overwrite.
343
433
  */
344
- async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
345
- const root = persistentRoot ? trialWorkspacePath(persistentRoot, trialId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
434
+ async function createRunWorkspace(runId, datasets = [], persistentRoot) {
435
+ const root = persistentRoot ? runWorkspacePath(persistentRoot, runId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-run-"));
346
436
  const persistent = persistentRoot !== void 0;
347
437
  const artifacts = {
348
438
  candidate: { root: path.join(root, "candidate") },
@@ -394,29 +484,100 @@ function collision(datasetId, file, cause) {
394
484
  return cause.code === "EEXIST" ? /* @__PURE__ */ new Error(`Dataset ${datasetId} would overwrite ${file}, which another dataset already wrote`) : cause instanceof Error ? cause : new Error(String(cause));
395
485
  }
396
486
  //#endregion
487
+ //#region ../runner/src/checkpoint-lookup.ts
488
+ /**
489
+ * `checkpoint(rule)` for a scoring context: the latest result of the steps
490
+ * that are this very rule. A rule outside the transcript is a mistake in the
491
+ * scenario, so it throws rather than reading as "not run".
492
+ */
493
+ function checkpointLookup(transcript, results) {
494
+ return ((rule) => {
495
+ const steps = new Set(transcript.flatMap((step, index) => step === rule ? [index] : []));
496
+ if (!steps.size) throw new Error(`checkpoint(${JSON.stringify(rule.name)}) is not a checkpoint in this scenario's transcript; pass the same rule object the transcript uses`);
497
+ for (let index = results.length - 1; index >= 0; index--) if (steps.has(results[index].step)) return results[index];
498
+ });
499
+ }
500
+ //#endregion
397
501
  //#region ../runner/src/score.ts
502
+ const inUnitInterval = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
503
+ /**
504
+ * Criteria must be well formed before they can stand for the score. With
505
+ * declared criteria, each takes its declared weight unless it sets one, a
506
+ * declared one left out is not reached, and an undeclared one is an error,
507
+ * so the checks shown before a run are the checks recorded after it.
508
+ */
509
+ function checkedCriteria(criteria, declared) {
510
+ if (!Array.isArray(criteria) || !criteria.length && !declared?.length) throw new Error("criteria must be a non-empty array");
511
+ const seen = /* @__PURE__ */ new Set();
512
+ const results = criteria.map((entry, index) => {
513
+ if (typeof entry?.name !== "string" || !entry.name.trim()) throw new Error(`criteria[${index}] needs a name`);
514
+ const { name } = entry;
515
+ if (seen.has(name)) throw new Error(`Criterion ${name} is listed twice`);
516
+ seen.add(name);
517
+ const declaration = declared?.find((criterion) => criterion.name === name);
518
+ if (declared && !declaration) throw new Error(`Criterion ${name} is not declared; declared: ${declared.map((criterion) => criterion.name).join(", ")}`);
519
+ const weight = entry.weight ?? declaration?.weight;
520
+ if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) throw new Error(`Criterion ${name} weight must be a positive number; received ${String(weight)}`);
521
+ const explanation = entry.explanation;
522
+ if (explanation !== void 0 && typeof explanation !== "string") throw new Error(`Criterion ${name} explanation must be a string`);
523
+ const common = {
524
+ name,
525
+ ...weight !== void 0 ? { weight } : {},
526
+ ...explanation?.trim() ? { explanation: explanation.trim() } : {}
527
+ };
528
+ if ("reached" in entry) {
529
+ if (entry.reached !== false) throw new Error(`Criterion ${name}: reached can only be false; leave it out for a check that ran`);
530
+ return {
531
+ ...common,
532
+ reached: false
533
+ };
534
+ }
535
+ const value = entry.value;
536
+ if (!inUnitInterval(value)) throw new Error(`Criterion ${name} value must be a number from 0 to 1; received ${String(value)}`);
537
+ const evidence = entry.evidence;
538
+ return {
539
+ ...common,
540
+ value,
541
+ ...evidence !== void 0 ? { evidence: toJson(evidence, `criteria.${name}.evidence`) } : {}
542
+ };
543
+ });
544
+ const missing = (declared ?? []).filter((criterion) => !seen.has(criterion.name)).map((criterion) => ({
545
+ name: criterion.name,
546
+ weight: criterion.weight,
547
+ reached: false
548
+ }));
549
+ return [...results, ...missing];
550
+ }
398
551
  /**
399
552
  * Shared normalization for inline and final predicate and judge rules.
400
- * `passAt` turns a numeric value into a verdict; an explicit `passed` wins.
553
+ * Without a `value`, the weighted mean of `criteria` is the value. `passAt`
554
+ * turns the value into a verdict, and is recorded with it; an explicit
555
+ * `passed` wins.
401
556
  */
402
- function normalizeScore(value, passAt) {
557
+ function normalizeScore(value, passAt, declared) {
403
558
  if (typeof value === "object" && value !== null && "skipped" in value && value.skipped === true) return {
404
559
  skipped: true,
405
560
  explanation: value.reason
406
561
  };
407
562
  const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
408
- if (!normalized || !Number.isFinite(normalized.value) || normalized.value < 0 || normalized.value > 1) throw new Error(`Score value must be a finite number from 0 to 1; received ${normalized?.value}`);
409
- if ("judge" in normalized && normalized.judge) {
563
+ if (!normalized || typeof normalized !== "object") throw new Error(`A scorer must return a boolean, a number from 0 to 1, or a score object; received ${String(value)}`);
564
+ const criteria = normalized.criteria === void 0 ? void 0 : checkedCriteria(normalized.criteria, declared);
565
+ const score = normalized.value ?? (criteria ? weightedMean(criteria) : void 0);
566
+ if (!inUnitInterval(score)) throw new Error(`Score value must be a finite number from 0 to 1; received ${String(score)}`);
567
+ if (normalized.judge?.usage) {
410
568
  const { usage } = normalized.judge;
411
- if (usage && Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
569
+ if (Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
412
570
  }
413
- const passed = "passed" in normalized && normalized.passed !== void 0 ? normalized.passed : normalized.value >= (passAt ?? 1);
571
+ const mark = passAt ?? 1;
414
572
  return {
415
- value: normalized.value,
416
- passed,
417
- ..."explanation" in normalized && normalized.explanation ? { explanation: normalized.explanation } : {},
418
- ..."evidence" in normalized && normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
419
- ..."judge" in normalized && normalized.judge !== void 0 ? { judge: normalized.judge } : {}
573
+ value: score,
574
+ passed: normalized.passed ?? score >= mark,
575
+ passAt: mark,
576
+ ...normalized.explanation ? { explanation: normalized.explanation } : {},
577
+ ...normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
578
+ ...normalized.choice ? { choice: normalized.choice } : {},
579
+ ...criteria ? { criteria } : {},
580
+ ...normalized.judge !== void 0 ? { judge: normalized.judge } : {}
420
581
  };
421
582
  }
422
583
  //#endregion
@@ -426,8 +587,28 @@ const failure$2 = (name, cause) => new RuleExecutionError({
426
587
  cause,
427
588
  message: cause instanceof Error ? cause.message : String(cause)
428
589
  });
429
- /** Drive a distinct agent session for one assessment; do not add its events to AUT trajectory. */
590
+ /** A long tool result or reply is cut to this many characters in a judge's conversation. */
591
+ const MAX_PART = 4e3;
592
+ /**
593
+ * Drives one judge agent session for one assessment: builds the prompt from
594
+ * the rule, sends it, reads the answer in the rule's format, and asks once
595
+ * more if it cannot. Its events stay out of the AUT trajectory.
596
+ */
430
597
  function runJudgeAgent(rule, agent, input, placement) {
598
+ return Effect.tryPromise({
599
+ try: () => judgeOnce(rule, agent, input, placement),
600
+ catch: (cause) => cause instanceof RuleExecutionError ? cause : failure$2(rule.name, cause)
601
+ });
602
+ }
603
+ async function judgeOnce(rule, agent, input, placement) {
604
+ const inputs = judgeInputNames(rule);
605
+ const values = await inputValues(rule, input, placement);
606
+ const prompt = judgePrompt({
607
+ rubric: rule.rubric,
608
+ answer: rule.answer,
609
+ inputs,
610
+ values
611
+ });
431
612
  const events = [];
432
613
  const context = {
433
614
  ...input.context,
@@ -435,63 +616,117 @@ function runJudgeAgent(rule, agent, input, placement) {
435
616
  workspace: input.artifacts.evaluator,
436
617
  evaluatorWorkspace: input.artifacts.evaluator
437
618
  };
438
- const prompt = JSON.stringify({
439
- name: rule.name,
440
- rubric: rule.rubric,
441
- placement,
442
- evidence: placement === "transcript" ? { turn: input.turn?.events ?? [] } : { trajectory: input.trajectory.events },
443
- instruction: "Return only a JSON verdict with value (a number from 0 to 1), optional passed, explanation, and evidence."
619
+ const session = await agent.start({
620
+ context,
621
+ attach: input.attach,
622
+ onEvent: (event) => {
623
+ const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
624
+ ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
625
+ source: "aut"
626
+ });
627
+ if (observed.source !== "aut") throw new Error("Invalid judge event source");
628
+ const { source: _, ...validated } = observed;
629
+ events.push(validated);
630
+ }
444
631
  });
445
- let closeError;
446
- return Effect.gen(function* () {
447
- yield* Effect.acquireUseRelease(Effect.tryPromise({
448
- try: () => agent.start({
449
- context,
450
- onEvent: (event) => {
451
- const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
452
- ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
453
- source: "aut"
454
- });
455
- if (observed.source !== "aut") throw new Error("Invalid judge event source");
456
- const { source: _, ...validated } = observed;
457
- events.push(validated);
458
- }
459
- }),
460
- catch: (cause) => failure$2(rule.name, cause)
461
- }), (session) => Effect.tryPromise({
462
- try: async () => {
463
- const result = await session.send(prompt);
464
- if (result !== void 0) events.push(...turnResultEvents(result, 1, () => /* @__PURE__ */ new Date()));
465
- },
466
- catch: (cause) => failure$2(rule.name, cause)
467
- }), (session) => Effect.tryPromise({
468
- try: async () => session.close?.(),
469
- catch: (cause) => failure$2(rule.name, cause)
470
- }).pipe(Effect.catchAll((error) => Effect.sync(() => {
471
- closeError = error;
472
- }))));
473
- if (closeError) return yield* Effect.fail(closeError);
474
- const completed = events.slice().reverse().find((event) => event.kind === "completed" && event.output !== void 0);
475
- const assistant = events.slice().reverse().find((event) => event.kind === "message" && event.role === "assistant");
476
- const output = completed?.kind === "completed" ? completed.output : assistant?.kind === "message" ? assistant.content : void 0;
477
- if (output === void 0) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge agent did not emit a verdict")));
478
- const value = yield* Effect.try({
479
- try: () => typeof output === "string" ? JSON.parse(output) : output,
480
- catch: (cause) => failure$2(rule.name, cause)
481
- });
482
- const encodedEvents = yield* Schema.decodeUnknown(JsonValueSchema)(events).pipe(Effect.mapError((cause) => failure$2(rule.name, cause)));
483
- if (!Array.isArray(encodedEvents)) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge events must be an array")));
484
- const usage = events.slice().reverse().find((event) => event.kind === "turn-completed" && event.usage);
485
- return {
486
- value,
487
- judge: {
488
- ...agent.identity ? { agent: agent.identity } : {},
489
- ...usage?.kind === "turn-completed" && usage.usage ? { usage: usage.usage } : {},
490
- events: encodedEvents
632
+ let reading;
633
+ let reply;
634
+ let attempts = 0;
635
+ try {
636
+ let message = prompt;
637
+ let problem = "";
638
+ while (attempts < 2 && !reading) {
639
+ attempts += 1;
640
+ const from = events.length;
641
+ const result = await session.send(message);
642
+ if (result !== void 0) events.push(...turnResultEvents(result, attempts, () => /* @__PURE__ */ new Date()));
643
+ reply = replyIn(events.slice(from));
644
+ if (reply === void 0) problem = "it did not reply";
645
+ else try {
646
+ reading = readJudgeAnswer(rule.answer, reply);
647
+ } catch (error) {
648
+ if (!(error instanceof UnreadableAnswer)) throw error;
649
+ problem = error.message;
491
650
  }
492
- };
651
+ message = `Your answer could not be read: ${problem}. Answer again, only in this format.\n\n${answerInstruction(rule.answer)}`;
652
+ }
653
+ if (!reading) throw new Error(`Judge ${rule.name}'s answer could not be read after ${attempts} tries: ${problem}${reply === void 0 ? "" : `. Its last reply: ${preview(reply)}`}`);
654
+ } finally {
655
+ await session.close?.();
656
+ }
657
+ const recorded = Schema.decodeUnknownSync(JsonValueSchema)(events);
658
+ const model = lastOf(events, (event) => event.kind === "turn-completed" && event.model);
659
+ const usage = totalUsage(events);
660
+ const judge = {
661
+ ...agent.identity ? { agent: agent.identity } : {},
662
+ ...model?.kind === "turn-completed" && model.model ? { model: model.model } : {},
663
+ ...usage ? { usage } : {},
664
+ prompt,
665
+ ...reply !== void 0 ? { reply } : {},
666
+ ...attempts > 1 ? { attempts } : {},
667
+ events: recorded
668
+ };
669
+ return { value: {
670
+ ...reading,
671
+ judge
672
+ } };
673
+ }
674
+ /** Each input's text: what the rule's function returned, or the defaults. */
675
+ async function inputValues(rule, input, placement) {
676
+ if (!rule.inputs) return {
677
+ conversation: conversationText(input.trajectory.events, placement),
678
+ reply: text(input.output)
679
+ };
680
+ const values = {};
681
+ for (const [name, read] of Object.entries(rule.inputs)) values[name] = text(await read(input));
682
+ return values;
683
+ }
684
+ /** The run's conversation as plain lines, for a judge to read: this turn at a checkpoint, else all of it. */
685
+ function conversationText(events, placement) {
686
+ let start = 0;
687
+ if (placement === "transcript") events.forEach((event, index) => {
688
+ if (event.kind === "transcript-step-started" && "message" in event) start = index;
493
689
  });
690
+ const lines = [];
691
+ for (const event of events.slice(start)) if (event.kind === "transcript-step-started" && "message" in event) lines.push(`User: ${cut(text(event.message))}`);
692
+ else if (event.source !== "aut") continue;
693
+ else if (event.kind === "message") lines.push(`${capitalize(event.role)}: ${cut(text(event.content))}`);
694
+ else if (event.kind === "tool-call") lines.push(`Tool call ${event.name}: ${cut(text(event.arguments))}`);
695
+ else if (event.kind === "tool-result") lines.push(`Tool result ${event.name ?? event.id}: ${cut(text(event.result))}`);
696
+ return lines.join("\n") || "(nothing happened)";
697
+ }
698
+ /** The judge's answer in one attempt's events: its final output, else its last reply. */
699
+ function replyIn(events) {
700
+ const completed = lastOf(events, (event) => event.kind === "completed" && event.output !== void 0);
701
+ if (completed?.kind === "completed") return completed.output;
702
+ const assistant = lastOf(events, (event) => event.kind === "message" && event.role === "assistant");
703
+ return assistant?.kind === "message" ? assistant.content : void 0;
704
+ }
705
+ function totalUsage(events) {
706
+ let total;
707
+ for (const event of events) {
708
+ if (event.kind !== "turn-completed" || !event.usage) continue;
709
+ total ??= {};
710
+ for (const [key, tokens] of Object.entries(event.usage)) if (tokens !== void 0) total[key] = (total[key] ?? 0) + tokens;
711
+ }
712
+ return total;
713
+ }
714
+ function text(value) {
715
+ if (value === void 0) return "(none)";
716
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
717
+ }
718
+ function cut(value) {
719
+ return value.length > MAX_PART ? `${value.slice(0, MAX_PART)}… (${value.length - MAX_PART} more characters)` : value;
720
+ }
721
+ function preview(value) {
722
+ const shown = text(value).replace(/\s+/g, " ");
723
+ return shown.length > 300 ? `${shown.slice(0, 300)}…` : shown;
494
724
  }
725
+ /** The last element matching, scanning from the end (no ES2023 lib here). */
726
+ function lastOf(items, match) {
727
+ for (let index = items.length - 1; index >= 0; index -= 1) if (match(items[index])) return items[index];
728
+ }
729
+ const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
495
730
  //#endregion
496
731
  //#region ../runner/src/evaluate-rule.ts
497
732
  const failure$1 = (name, cause) => new RuleExecutionError({
@@ -500,38 +735,30 @@ const failure$1 = (name, cause) => new RuleExecutionError({
500
735
  message: cause instanceof Error ? cause.message : String(cause)
501
736
  });
502
737
  /** Both placements call the same rule executor; only input timing and persistence differ. */
503
- function evaluateRule(rule, input, judgeAgent, placement) {
738
+ function evaluateRule(rule, input, defaultJudge, placement) {
504
739
  return Effect.gen(function* () {
505
- if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("Judge rules require an eval judge agent")));
506
- const evaluated = rule.kind === "predicate" ? { value: yield* Effect.tryPromise({
740
+ const judgeAgent = rule.kind === "judge" ? rule.agent ?? defaultJudge : void 0;
741
+ if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("This judge has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts")));
742
+ const evaluated = rule.kind === "predicate" ? yield* Effect.tryPromise({
507
743
  try: async () => rule.run(input),
508
744
  catch: (cause) => failure$1(rule.name, cause)
509
- }) } : yield* runJudgeAgent(rule, judgeAgent, input, placement);
745
+ }) : (yield* runJudgeAgent(rule, judgeAgent, input, placement)).value;
510
746
  const score = yield* Effect.try({
511
- try: () => normalizeScore(evaluated.value, rule.kind === "predicate" ? rule.passAt : void 0),
747
+ try: () => normalizeScore(evaluated, rule.passAt, rule.kind === "predicate" ? rule.criteria : void 0),
512
748
  catch: (cause) => failure$1(rule.name, cause)
513
749
  });
514
- const withJudge = rule.kind === "judge" && "judge" in evaluated ? {
515
- ...score,
516
- judge: evaluated.judge
517
- } : score;
518
750
  const validated = yield* Schema.decodeUnknown(ScoreResultSchema)({
519
751
  name: rule.name,
520
752
  kind: rule.kind,
521
753
  durationMs: 0,
522
- ...withJudge
754
+ ...score
523
755
  }).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
524
756
  if (validated.skipped) return {
525
757
  skipped: true,
526
758
  ...validated.explanation === void 0 ? {} : { explanation: validated.explanation }
527
759
  };
528
- return {
529
- value: validated.value,
530
- passed: validated.passed,
531
- ...validated.explanation === void 0 ? {} : { explanation: validated.explanation },
532
- ...validated.evidence === void 0 ? {} : { evidence: validated.evidence },
533
- ...validated.judge === void 0 ? {} : { judge: validated.judge }
534
- };
760
+ const { name: _name, kind: _kind, durationMs: _duration, ...result } = validated;
761
+ return result;
535
762
  });
536
763
  }
537
764
  //#endregion
@@ -583,7 +810,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
583
810
  const annotations = {
584
811
  evalId: input.context.evalId,
585
812
  runId: input.context.runId,
586
- trialId: input.context.trialId,
587
813
  step: index,
588
814
  checkpoint: step.name
589
815
  };
@@ -682,7 +908,7 @@ function userMessage(step, index, { context, randomSeed }) {
682
908
  parameters: context.parameters ?? {},
683
909
  data: context.data,
684
910
  seed: randomSeed,
685
- trialIndex: context.trialIndex
911
+ ...context.batch ? { batch: context.batch } : {}
686
912
  });
687
913
  if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
688
914
  return message;
@@ -706,6 +932,7 @@ async function runCheckpoint(definition, step, index, input) {
706
932
  name: step.name,
707
933
  timestamp: started.toISOString()
708
934
  });
935
+ const tracked = trackAttachments(input.attach);
709
936
  return runBoundary(evaluateCheckpoint(step, index, {
710
937
  context: input.context,
711
938
  artifacts: input.artifacts,
@@ -713,8 +940,13 @@ async function runCheckpoint(definition, step, index, input) {
713
940
  turn: progress.lastTurn,
714
941
  ...turnOutput(progress.lastTurn),
715
942
  data: input.data,
716
- checkpoints: [...progress.checkpoints]
717
- }, now, definition.judge)).catch(async (error) => {
943
+ checkpoints: [...progress.checkpoints],
944
+ checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
945
+ attach: tracked.attach
946
+ }, now, input.defaultJudge)).then((result) => ({
947
+ ...result,
948
+ ...attachedFields(tracked.attached)
949
+ })).catch(async (error) => {
718
950
  const recorded = recordError(error);
719
951
  progress.checkpoints.push({
720
952
  step: index,
@@ -722,7 +954,8 @@ async function runCheckpoint(definition, step, index, input) {
722
954
  name: step.name,
723
955
  status: "error",
724
956
  durationMs: now().getTime() - started.getTime(),
725
- error: recorded
957
+ error: recorded,
958
+ ...attachedFields(tracked.attached)
726
959
  });
727
960
  try {
728
961
  await emitRunner({
@@ -779,7 +1012,9 @@ function scoringContext(input) {
779
1012
  ...input.lastTurn ? { turn: input.lastTurn } : {},
780
1013
  ...turnOutput(input.lastTurn),
781
1014
  data: input.data,
782
- checkpoints: input.checkpoints
1015
+ checkpoints: input.checkpoints,
1016
+ checkpoint: checkpointLookup(input.transcript, input.checkpoints),
1017
+ attach: input.attach
783
1018
  };
784
1019
  }
785
1020
  /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
@@ -797,12 +1032,15 @@ async function evaluateFinalScorers(definition, input) {
797
1032
  timestamp: started.toISOString()
798
1033
  });
799
1034
  let result;
1035
+ const tracked = trackAttachments(input.attach);
800
1036
  try {
801
1037
  const score = await runBoundary(evaluateRule(scorer, scoringContext({
802
1038
  ...input,
1039
+ attach: tracked.attach,
803
1040
  context,
804
- workspace
805
- }), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
1041
+ workspace,
1042
+ transcript: definition.transcript
1043
+ }), input.defaultJudge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
806
1044
  cause,
807
1045
  message: cause.message
808
1046
  }))));
@@ -810,7 +1048,8 @@ async function evaluateFinalScorers(definition, input) {
810
1048
  name: scorer.name,
811
1049
  kind: scorer.kind,
812
1050
  durationMs: now().getTime() - started.getTime(),
813
- ...score
1051
+ ...score,
1052
+ ...attachedFields(tracked.attached)
814
1053
  };
815
1054
  results.push(result);
816
1055
  } catch (error) {
@@ -820,7 +1059,8 @@ async function evaluateFinalScorers(definition, input) {
820
1059
  kind: scorer.kind,
821
1060
  durationMs: now().getTime() - started.getTime(),
822
1061
  error: recorded,
823
- passed: false
1062
+ passed: false,
1063
+ ...attachedFields(tracked.attached)
824
1064
  });
825
1065
  await emitRunner({
826
1066
  kind: "scorer-failed",
@@ -841,11 +1081,12 @@ async function evaluateFinalScorers(definition, input) {
841
1081
  //#endregion
842
1082
  //#region ../runner/src/metrics.ts
843
1083
  /**
844
- * What EvalKit measures for every trial from its trajectory: the agent's time
1084
+ * What EvalKit measures for every run: from its trajectory, the agent's time
845
1085
  * (first event to the last one before final scoring), tokens when the agent
846
- * reported usage, and tool calls.
1086
+ * reported usage, and tool calls; from its scores and checkpoints, the
1087
+ * tokens its judges used, kept apart from the agent's.
847
1088
  */
848
- function builtInMetrics(events) {
1089
+ function builtInMetrics(events, verdicts = []) {
849
1090
  const times = events.filter((event) => !event.kind.startsWith("scorer-")).map((event) => Date.parse(event.timestamp)).filter((time) => !Number.isNaN(time));
850
1091
  const metrics = [];
851
1092
  if (times.length) metrics.push({
@@ -857,8 +1098,7 @@ function builtInMetrics(events) {
857
1098
  });
858
1099
  let tokens;
859
1100
  for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
860
- const { totalTokens, inputTokens, outputTokens } = event.usage;
861
- const turn = totalTokens ?? (inputTokens === void 0 && outputTokens === void 0 ? void 0 : (inputTokens ?? 0) + (outputTokens ?? 0));
1101
+ const turn = turnTokens(event.usage);
862
1102
  if (turn !== void 0) tokens = (tokens ?? 0) + turn;
863
1103
  }
864
1104
  if (tokens !== void 0) metrics.push({
@@ -873,6 +1113,18 @@ function builtInMetrics(events) {
873
1113
  value: events.filter((event) => event.source === "aut" && event.kind === "tool-call").length,
874
1114
  builtIn: true
875
1115
  });
1116
+ let judgeTokens;
1117
+ for (const verdict of verdicts) {
1118
+ const used = verdict.judge?.usage && turnTokens(verdict.judge.usage);
1119
+ if (used !== void 0) judgeTokens = (judgeTokens ?? 0) + used;
1120
+ }
1121
+ if (judgeTokens !== void 0) metrics.push({
1122
+ name: "judge tokens",
1123
+ value: judgeTokens,
1124
+ unit: "tokens",
1125
+ better: "lower",
1126
+ builtIn: true
1127
+ });
876
1128
  return metrics;
877
1129
  }
878
1130
  /** Runs the scenario's metrics; a failing metric records its error and the rest still run. */
@@ -903,7 +1155,7 @@ async function evaluateMetrics(definition, context) {
903
1155
  //#endregion
904
1156
  //#region ../runner/src/local-report-store.ts
905
1157
  const MANIFEST_FILE = "manifest.json";
906
- /** The project's evals and suites when the run started (schema version 4+). */
1158
+ /** The project's evals and suites when the run started. */
907
1159
  const CATALOG_FILE = "catalog.json";
908
1160
  async function writeJson(filePath, value) {
909
1161
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
@@ -922,10 +1174,12 @@ function artifactPath(root, value) {
922
1174
  if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
923
1175
  return path.join(root, normalized);
924
1176
  }
925
- var LocalTrialWriter = class {
1177
+ var LocalRunWriter = class {
1178
+ location;
926
1179
  directory;
927
1180
  metadata;
928
- constructor(directory, metadata) {
1181
+ constructor(location, directory, metadata) {
1182
+ this.location = location;
929
1183
  this.directory = directory;
930
1184
  this.metadata = metadata;
931
1185
  }
@@ -933,40 +1187,13 @@ var LocalTrialWriter = class {
933
1187
  return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
934
1188
  }
935
1189
  writeScores(scoring) {
936
- return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(TrialScoringSchema)(scoring));
1190
+ return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
937
1191
  }
938
1192
  async writeArtifact(relativePath, data) {
939
1193
  const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
940
1194
  await mkdir(path.dirname(target), { recursive: true });
941
1195
  await writeFile(target, data);
942
1196
  }
943
- async finalize(summary) {
944
- await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(TrialSummarySchema)(summary));
945
- await replaceJson(path.join(this.directory, MANIFEST_FILE), {
946
- ...Schema.encodeSync(TrialMetadataSchema)(this.metadata),
947
- status: summary.status
948
- });
949
- }
950
- };
951
- var LocalRunWriter = class {
952
- location;
953
- directory;
954
- metadata;
955
- constructor(location, directory, metadata) {
956
- this.location = location;
957
- this.directory = directory;
958
- this.metadata = metadata;
959
- }
960
- async startTrial(metadata) {
961
- assertPathSegment(metadata.trialId, "trialId");
962
- const directory = path.join(this.directory, "trials", metadata.trialId);
963
- await mkdir(directory, { recursive: true });
964
- await writeJson(path.join(directory, MANIFEST_FILE), {
965
- ...Schema.encodeSync(TrialMetadataSchema)(metadata),
966
- status: "running"
967
- });
968
- return new LocalTrialWriter(directory, metadata);
969
- }
970
1197
  async finalize(summary) {
971
1198
  await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
972
1199
  await replaceJson(path.join(this.directory, MANIFEST_FILE), {
@@ -991,17 +1218,15 @@ function localReportStore(rootDirectory) {
991
1218
  }
992
1219
  //#endregion
993
1220
  //#region ../runner/src/local-report-reader.ts
994
- /** Shared v2/v3 local report boundary for CLI, dashboard, and CI consumers. */
1221
+ /** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
995
1222
  async function decode(file, schema) {
996
1223
  return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
997
1224
  }
998
1225
  const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
999
1226
  const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
1000
- const readTrialManifest = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "manifest.json"), TrialManifestSchema);
1001
- const readTrialSummary = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "summary.json"), TrialSummarySchema);
1002
- const readTrialScoring = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "scoring.json"), TrialScoringSchema);
1003
- async function readTrialEvents(root, runId, trialId) {
1004
- return parseTrajectoryJsonl(await readFile(join(root, runId, "trials", trialId, "trajectory.jsonl"), "utf8"));
1227
+ const readRunScoring = (root, runId) => decode(join(root, runId, "scoring.json"), RunScoringSchema);
1228
+ async function readRunEvents(root, runId) {
1229
+ return parseTrajectoryJsonl(await readFile(join(root, runId, "trajectory.jsonl"), "utf8"));
1005
1230
  }
1006
1231
  /**
1007
1232
  * True when a run's owning process is known to be gone: it ran on this host
@@ -1028,6 +1253,7 @@ function runMatrix(matrix, options) {
1028
1253
  const iterator = matrix.cells(options.selection);
1029
1254
  const summary = {
1030
1255
  cells: 0,
1256
+ runs: 0,
1031
1257
  passed: 0,
1032
1258
  failed: 0,
1033
1259
  cancelled: 0
@@ -1043,7 +1269,7 @@ function runMatrix(matrix, options) {
1043
1269
  "sandbox"
1044
1270
  ].find((name) => agentRuntimes(cell.eval.agent)[name]);
1045
1271
  const suiteId = options.suiteId ?? options.suiteFor?.(cell);
1046
- const result = yield* runEval(cell.eval, {
1272
+ const runs = yield* runEval(cell.eval, {
1047
1273
  ...options,
1048
1274
  ...suiteId ? { suiteId } : {},
1049
1275
  concurrency: 1,
@@ -1055,18 +1281,232 @@ function runMatrix(matrix, options) {
1055
1281
  }
1056
1282
  });
1057
1283
  summary.cells++;
1058
- if (result.status === "cancelled") summary.cancelled++;
1059
- else if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
1060
- else summary.failed++;
1061
- if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, result));
1284
+ for (const run of runs) {
1285
+ summary.runs++;
1286
+ if (run.status === "cancelled") summary.cancelled++;
1287
+ else if (run.status === "passed") summary.passed++;
1288
+ else summary.failed++;
1289
+ }
1290
+ if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, runs));
1062
1291
  }
1063
1292
  })), { concurrency });
1064
1293
  return summary;
1065
1294
  });
1066
1295
  }
1067
1296
  //#endregion
1297
+ //#region ../runner/src/upgrade.ts
1298
+ /** A new run being written. */
1299
+ const TEMPORARY = ".upgrade-";
1300
+ /** An old run set aside while its replacement, with the same ID, moves in. */
1301
+ const RETIRED = ".upgraded-";
1302
+ const PER_RUN_FILES = ["archived.json"];
1303
+ /**
1304
+ * Rewrites results from report schema versions 2 to 5 into version 6, in
1305
+ * place (RFD0008). An old run of one trial (most of them) becomes a run with
1306
+ * the same ID, so links to it keep working. An old run of several trials
1307
+ * becomes runs with the trials' IDs, in a batch with the old run's ID.
1308
+ *
1309
+ * Each new run is written under a temporary name and renamed into place;
1310
+ * artifacts are moved, not copied, as the last step before that rename. An
1311
+ * old run is removed only after its new runs exist, and one replaced under
1312
+ * the same ID is set aside first, so a crash at any point leaves the old run
1313
+ * or complete new ones, and re-running finishes the job. Version 6 runs are
1314
+ * left alone.
1315
+ */
1316
+ async function upgradeReports(root, options = {}) {
1317
+ const report = {
1318
+ upgraded: 0,
1319
+ runs: 0,
1320
+ batches: 0,
1321
+ running: []
1322
+ };
1323
+ let entries;
1324
+ try {
1325
+ entries = await readdir(root);
1326
+ } catch {
1327
+ return report;
1328
+ }
1329
+ if (!options.dryRun) await finishReplacements(root, entries);
1330
+ for (const entry of entries) {
1331
+ if (entry.startsWith(".")) continue;
1332
+ const directory = join(root, entry);
1333
+ const manifest = await readJson(join(directory, "manifest.json"));
1334
+ if (!manifest || !UPGRADABLE_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) continue;
1335
+ const summary = await readJson(join(directory, "summary.json"));
1336
+ if (!summary && !ownerIsGone(manifest.process)) {
1337
+ report.running.push(entry);
1338
+ continue;
1339
+ }
1340
+ const trials = await trialIds(directory);
1341
+ report.upgraded += 1;
1342
+ report.runs += Math.max(trials.length, 1);
1343
+ if (trials.length > 1) report.batches += 1;
1344
+ if (options.dryRun) continue;
1345
+ await upgradeRun(root, entry, manifest, summary, trials);
1346
+ }
1347
+ if (!options.dryRun) await removeLeftovers(root);
1348
+ return report;
1349
+ }
1350
+ /**
1351
+ * Finishes same-ID replacements a crash interrupted: the old run was set
1352
+ * aside, and its replacement may or may not have moved in.
1353
+ */
1354
+ async function finishReplacements(root, entries) {
1355
+ for (const entry of entries) {
1356
+ if (!entry.startsWith(RETIRED)) continue;
1357
+ const id = entry.slice(10);
1358
+ const retired = join(root, entry);
1359
+ const directory = join(root, id);
1360
+ const replacement = join(root, `${TEMPORARY}${id}`);
1361
+ if (!await exists(directory)) await rename(await exists(replacement) ? replacement : retired, directory);
1362
+ await rm(retired, {
1363
+ recursive: true,
1364
+ force: true
1365
+ });
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Temporary folders nothing will finish. One holding artifacts is kept: it
1370
+ * could be the only copy, and an old run would have moved them back.
1371
+ */
1372
+ async function removeLeftovers(root) {
1373
+ for (const entry of await readdir(root)) if (entry.startsWith(TEMPORARY) && !await exists(join(root, entry, "artifacts"))) await rm(join(root, entry), {
1374
+ recursive: true,
1375
+ force: true
1376
+ });
1377
+ }
1378
+ /** Swaps a fully written replacement in for the old run of the same ID. */
1379
+ async function replaceInPlace(root, id, replacement) {
1380
+ const retired = join(root, `${RETIRED}${id}`);
1381
+ await rename(join(root, id), retired);
1382
+ await rename(replacement, join(root, id));
1383
+ await rm(retired, {
1384
+ recursive: true,
1385
+ force: true
1386
+ });
1387
+ }
1388
+ async function upgradeRun(root, oldRunId, runManifest, runSummary, trials) {
1389
+ const directory = join(root, oldRunId);
1390
+ const catalog = await readJson(join(directory, "catalog.json"));
1391
+ const extras = await presentFiles(directory, PER_RUN_FILES);
1392
+ if (!trials.length) {
1393
+ const target = join(root, `${TEMPORARY}${oldRunId}`);
1394
+ await mkdir(target, { recursive: true });
1395
+ const status = runSummary ? "cancelled" : "running";
1396
+ await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1397
+ run: runManifest,
1398
+ runId: oldRunId,
1399
+ status
1400
+ }));
1401
+ if (runSummary) await writeJsonFile(join(target, "summary.json"), {
1402
+ status,
1403
+ endedAt: runSummary.endedAt,
1404
+ ...runSummary.durationMs === void 0 ? {} : { durationMs: runSummary.durationMs },
1405
+ ...runSummary.error ? { error: runSummary.error } : {}
1406
+ });
1407
+ await copyShared(directory, target, catalog, extras);
1408
+ await replaceInPlace(root, oldRunId, target);
1409
+ return;
1410
+ }
1411
+ const inPlace = trials.length === 1;
1412
+ for (const trialId of trials) {
1413
+ const runId = inPlace ? oldRunId : trialId;
1414
+ const destination = join(root, runId);
1415
+ if (!inPlace && await exists(join(destination, "manifest.json"))) continue;
1416
+ const source = join(directory, "trials", trialId);
1417
+ const trialManifest = await readJson(join(source, "manifest.json")) ?? {};
1418
+ const trialSummary = await readJson(join(source, "summary.json"));
1419
+ const passed = (trialSummary?.scoring)?.passed;
1420
+ const status = upgradeStatus(trialSummary?.status ?? trialManifest.status, passed);
1421
+ const index = typeof trialManifest.trialIndex === "number" ? trialManifest.trialIndex : trials.indexOf(trialId);
1422
+ const target = join(root, `${TEMPORARY}${runId}`);
1423
+ if (!await exists(join(source, "artifacts")) && await exists(join(target, "artifacts"))) await rename(join(target, "artifacts"), join(source, "artifacts"));
1424
+ await rm(target, {
1425
+ recursive: true,
1426
+ force: true
1427
+ });
1428
+ await mkdir(target, { recursive: true });
1429
+ await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1430
+ run: runManifest,
1431
+ trial: trialManifest,
1432
+ runId,
1433
+ ...!inPlace ? { batch: {
1434
+ id: oldRunId,
1435
+ index,
1436
+ size: trials.length
1437
+ } } : {},
1438
+ status
1439
+ }));
1440
+ if (trialSummary) await writeJsonFile(join(target, "summary.json"), {
1441
+ ...trialSummary,
1442
+ status
1443
+ });
1444
+ const scoring = await readText(join(source, "scoring.json"));
1445
+ if (scoring !== void 0) await writeFile(join(target, "scoring.json"), scoring);
1446
+ const trajectory = await readText(join(source, "trajectory.jsonl"));
1447
+ if (trajectory !== void 0) await writeFile(join(target, "trajectory.jsonl"), upgradeTrajectory(trajectory));
1448
+ await copyShared(directory, target, catalog, extras);
1449
+ if (await exists(join(source, "artifacts"))) await rename(join(source, "artifacts"), join(target, "artifacts"));
1450
+ if (inPlace) await replaceInPlace(root, oldRunId, target);
1451
+ else await rename(target, destination);
1452
+ }
1453
+ if (!inPlace) await rm(directory, {
1454
+ recursive: true,
1455
+ force: true
1456
+ });
1457
+ }
1458
+ async function copyShared(from, to, catalog, extras) {
1459
+ if (catalog) await writeJsonFile(join(to, "catalog.json"), upgradeCatalog(catalog));
1460
+ for (const file of extras) await cp(join(from, file), join(to, file));
1461
+ }
1462
+ async function trialIds(directory) {
1463
+ try {
1464
+ return (await readdir(join(directory, "trials"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1465
+ } catch {
1466
+ return [];
1467
+ }
1468
+ }
1469
+ async function presentFiles(directory, names) {
1470
+ const present = [];
1471
+ for (const name of names) if (await exists(join(directory, name))) present.push(name);
1472
+ return present;
1473
+ }
1474
+ async function exists(path) {
1475
+ try {
1476
+ await stat(path);
1477
+ return true;
1478
+ } catch {
1479
+ return false;
1480
+ }
1481
+ }
1482
+ async function readText(path) {
1483
+ try {
1484
+ return await readFile(path, "utf8");
1485
+ } catch {
1486
+ return;
1487
+ }
1488
+ }
1489
+ async function readJson(path) {
1490
+ const text = await readText(path);
1491
+ if (text === void 0) return void 0;
1492
+ let value;
1493
+ try {
1494
+ value = JSON.parse(text);
1495
+ } catch (error) {
1496
+ throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1497
+ }
1498
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1499
+ }
1500
+ async function writeJsonFile(path, value) {
1501
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
1502
+ }
1503
+ //#endregion
1068
1504
  //#region ../runner/src/index.ts
1069
- /** Effect-native aggregate eval execution entry point. */
1505
+ /**
1506
+ * Runs a scenario: once, or `repeat` times as one batch. Resolves to one
1507
+ * result per run started; runs never started because of cancellation are
1508
+ * left out.
1509
+ */
1070
1510
  function runEval(definition, options) {
1071
1511
  return Effect.gen(function* () {
1072
1512
  yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
@@ -1074,19 +1514,19 @@ function runEval(definition, options) {
1074
1514
  ...options.runId ? { runId: options.runId } : {},
1075
1515
  ...options.suiteId ? { suiteId: options.suiteId } : {}
1076
1516
  }));
1077
- const result = yield* Effect.tryPromise({
1078
- try: () => executeRun(definition, options),
1517
+ const runs = yield* Effect.tryPromise({
1518
+ try: () => executeBatch(definition, options),
1079
1519
  catch: (error) => error
1080
1520
  });
1081
1521
  yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
1082
1522
  evalId: authoringId(definition),
1083
- runId: result.runId,
1084
- status: result.status
1523
+ runs: runs.length,
1524
+ passed: runs.filter((run) => run.status === "passed").length
1085
1525
  }));
1086
- return result;
1526
+ return runs;
1087
1527
  });
1088
1528
  }
1089
- function createId(_kind) {
1529
+ function createId() {
1090
1530
  return crypto.randomUUID();
1091
1531
  }
1092
1532
  function canonicalId(kind, id) {
@@ -1095,9 +1535,9 @@ function canonicalId(kind, id) {
1095
1535
  function assertUuid(value, label) {
1096
1536
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) throw new Error(`${label} must be a UUID`);
1097
1537
  }
1098
- function runMetadata(definition, options, runId, startedAt) {
1538
+ function runMetadata(definition, options, runId, startedAt, extra) {
1099
1539
  return {
1100
- schemaVersion: 5,
1540
+ schemaVersion: 6,
1101
1541
  runId,
1102
1542
  runUri: canonicalId("run", runId),
1103
1543
  evalId: definition.id,
@@ -1110,7 +1550,9 @@ function runMetadata(definition, options, runId, startedAt) {
1110
1550
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1111
1551
  parameters: options.parameters,
1112
1552
  matrix: options.matrix,
1113
- startedAt
1553
+ startedAt,
1554
+ ...options.batch ? { batch: options.batch } : {},
1555
+ ...extra
1114
1556
  };
1115
1557
  }
1116
1558
  /** Recording a secondary error must never mask the original failure or block cleanup. */
@@ -1123,104 +1565,53 @@ async function tryEmitError(error, emitRunner, now) {
1123
1565
  });
1124
1566
  } catch {}
1125
1567
  }
1126
- /** Executes one aggregate eval run and its requested isolated trials. */
1127
- async function executeRun(definition, options) {
1568
+ /** Starts `repeat` runs of a scenario, as one batch when there are several. */
1569
+ async function executeBatch(definition, options) {
1128
1570
  let hasUser = false;
1129
1571
  for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
1130
1572
  else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
1131
- if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
1573
+ assertNoRemovedOptions(definition);
1574
+ assertJudgesIndependent(definition, options.judge);
1575
+ for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && !rule.agent && !options.judge) throw new Error(`Judge ${rule.name} in ${definition.id} has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts`);
1132
1576
  if (options.runId) assertUuid(options.runId, "runId");
1133
- if (options.trialId) assertUuid(options.trialId, "trialId");
1134
- const requestedTrials = options.trials ?? definition.policy?.trials ?? 1;
1135
- if (!Number.isInteger(requestedTrials) || requestedTrials < 1) throw new Error(`Trial count must be a positive integer; received ${requestedTrials}`);
1136
- if (requestedTrials === 1) return runTrial(definition, options);
1137
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
1138
- const aggregateStartedAt = now();
1139
- const runId = options.runId ?? createId("run");
1140
- const runWriter = await options.report.startRun(runMetadata(definition, options, runId, aggregateStartedAt.toISOString()), { catalog: options.catalog });
1577
+ const repeat = options.repeat ?? definition.repeat ?? 1;
1578
+ if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
1579
+ if (repeat === 1) return [await runOnce(definition, options)];
1580
+ if (options.runId) throw new Error("runId names one run; it cannot be used with repeat");
1581
+ const batchId = createId();
1141
1582
  const signal = options.signal;
1142
- const trialEffects = Array.from({ length: requestedTrials }, (_, trialIndex) => {
1143
- const trial = Effect.tryPromise({
1144
- try: async () => signal?.aborted ? void 0 : runTrial(definition, {
1583
+ const runs = Array.from({ length: repeat }, (_, index) => {
1584
+ const run = Effect.tryPromise({
1585
+ try: async () => signal?.aborted ? void 0 : runOnce(definition, {
1145
1586
  ...options,
1146
- runId,
1147
- trialId: createId("trial"),
1148
- trials: 1,
1149
- runWriter,
1150
- trialIndex
1587
+ batch: {
1588
+ id: batchId,
1589
+ index,
1590
+ size: repeat
1591
+ }
1151
1592
  }),
1152
1593
  catch: (error) => error
1153
1594
  });
1154
- return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
1155
- });
1156
- const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
1157
- const counts = trialCounts(trials);
1158
- const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
1159
- const endedAt = now();
1160
- await runWriter.finalize({
1161
- status,
1162
- endedAt: endedAt.toISOString(),
1163
- durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
1164
- trialCount: trials.length,
1165
- passed: counts.passed,
1166
- failed: counts.failed,
1167
- ...counts.cancelled ? { cancelled: counts.cancelled } : {}
1595
+ return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
1168
1596
  });
1169
- if (!trials.length) return {
1170
- runId,
1171
- runUri: canonicalId("run", runId),
1172
- trialId: "",
1173
- trialUri: canonicalId("trial", runId),
1174
- trialIndex: 0,
1175
- evalId: authoringId(definition),
1176
- status,
1177
- reportLocation: runWriter.location,
1178
- trialCount: 0,
1179
- passed: 0,
1180
- failed: 0,
1181
- aggregateScoring: {
1182
- passed: 0,
1183
- failed: 0,
1184
- passRate: 0
1185
- },
1186
- trials: []
1187
- };
1188
- const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
1189
- return {
1190
- ...trials[0],
1191
- runId,
1192
- evalId: authoringId(definition),
1193
- status,
1194
- trialCount: trials.length,
1195
- passed: counts.passed,
1196
- failed: counts.failed,
1197
- aggregateScoring: {
1198
- passed: counts.passed,
1199
- failed: counts.failed,
1200
- ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1201
- passRate: counts.passed / trials.length,
1202
- ...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
1203
- },
1204
- trials
1205
- };
1597
+ return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
1206
1598
  }
1207
- async function runTrial(definition, options) {
1599
+ /** One execution: a session in its own sandbox, scored and reported. */
1600
+ async function runOnce(definition, options) {
1208
1601
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
1209
- const runId = options.runId ?? createId("run");
1210
- const trialId = options.trialId ?? createId("trial");
1602
+ const runId = options.runId ?? createId();
1211
1603
  const startedAt = now().toISOString();
1212
1604
  const selectedRuntime = options.runtime;
1213
1605
  const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
1214
1606
  if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
1215
1607
  const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
1216
- const trialIndex = options.trialIndex ?? 0;
1608
+ const batch = options.batch;
1217
1609
  const signal = options.signal ?? new AbortController().signal;
1218
1610
  const root = options.root ?? process.cwd();
1219
1611
  const baseContext = {
1220
1612
  runId,
1221
1613
  evalId: authoringId(definition),
1222
- trialId,
1223
- trialIndex,
1614
+ ...batch ? { batch } : {},
1224
1615
  metadata: {
1225
1616
  ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
1226
1617
  randomSeed
@@ -1234,8 +1625,7 @@ async function runTrial(definition, options) {
1234
1625
  prepared = await prepareDatasets(definition.datasets ?? [], root, {
1235
1626
  evalId: baseContext.evalId,
1236
1627
  runId,
1237
- trialId,
1238
- trialIndex,
1628
+ ...batch ? { batch } : {},
1239
1629
  seed: randomSeed,
1240
1630
  parameters: options.parameters ?? {},
1241
1631
  signal
@@ -1247,40 +1637,28 @@ async function runTrial(definition, options) {
1247
1637
  const scoringData = datasetValues(prepared, "all");
1248
1638
  const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
1249
1639
  const events = [];
1250
- const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt), { catalog: options.catalog });
1251
- const trialWriter = await runWriter.startTrial({
1252
- schemaVersion: 5,
1253
- runId,
1254
- runUri: canonicalId("run", runId),
1255
- trialId,
1256
- trialUri: canonicalId("trial", trialId),
1257
- trialIndex: options.trialIndex ?? 0,
1258
- evalId: definition.id,
1259
- ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1260
- parameters: options.parameters,
1261
- matrix: options.matrix,
1262
- startedAt,
1640
+ const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
1263
1641
  workspaceIgnore,
1264
- ...prepared.length ? { datasets: trialDatasets(prepared) } : {}
1265
- });
1642
+ ...prepared.length ? { datasets: runDatasets(prepared) } : {}
1643
+ }), { catalog: options.catalog });
1644
+ const attachments = attachmentStore(runWriter, options.attachmentLimits);
1266
1645
  let writeChain = Promise.resolve();
1267
1646
  let reportingFailed = false;
1268
- const trialRef = {
1647
+ const runRef = {
1269
1648
  runId,
1270
- trialId,
1271
- trialIndex,
1649
+ ...batch ? { batch } : {},
1272
1650
  evalId: definition.id,
1273
1651
  ...options.suiteId ? { suiteId: options.suiteId } : {},
1274
1652
  ...options.parameters ? { parameters: options.parameters } : {}
1275
1653
  };
1276
1654
  const observe = (event) => {
1277
1655
  try {
1278
- options.onEvent?.(trialRef, event);
1656
+ options.onEvent?.(runRef, event);
1279
1657
  } catch {}
1280
1658
  };
1281
1659
  const append = async (event) => {
1282
1660
  events.push(event);
1283
- writeChain = writeChain.then(() => trialWriter.appendEvent(event));
1661
+ writeChain = writeChain.then(() => runWriter.appendEvent(event));
1284
1662
  try {
1285
1663
  await writeChain;
1286
1664
  observe(event);
@@ -1316,7 +1694,7 @@ async function runTrial(definition, options) {
1316
1694
  const workspaceScope = await Effect.runPromise(Scope.make());
1317
1695
  try {
1318
1696
  await emitRunner({
1319
- kind: "trial-started",
1697
+ kind: "run-started",
1320
1698
  timestamp: now().toISOString()
1321
1699
  });
1322
1700
  if (signal.aborted) throw cancelled();
@@ -1325,7 +1703,7 @@ async function runTrial(definition, options) {
1325
1703
  message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1326
1704
  });
1327
1705
  workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
1328
- try: () => createTrialWorkspace(trialId, prepared, options.workspaceRoot),
1706
+ try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
1329
1707
  catch: (cause) => new DatasetError({
1330
1708
  cause,
1331
1709
  message: cause instanceof Error ? cause.message : String(cause)
@@ -1338,20 +1716,21 @@ async function runTrial(definition, options) {
1338
1716
  })
1339
1717
  }).pipe(Effect.catchAll((error) => Effect.sync(() => {
1340
1718
  cleanupError = error;
1341
- }).pipe(Effect.tap(() => Effect.logError("trial workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
1342
- const trialWorkspace = workspace;
1343
- const trialContext = {
1719
+ }).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
1720
+ const runWorkspace = workspace;
1721
+ const runContext = {
1344
1722
  ...baseContext,
1345
1723
  data: candidateData,
1346
- workspace: trialWorkspace.artifacts.candidate,
1347
- evaluatorWorkspace: trialWorkspace.artifacts.evaluator
1724
+ workspace: runWorkspace.artifacts.candidate,
1725
+ evaluatorWorkspace: runWorkspace.artifacts.evaluator
1348
1726
  };
1349
- context = trialContext;
1727
+ context = runContext;
1350
1728
  await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
1351
1729
  try: () => definition.agent.start({
1352
- context: trialContext,
1730
+ context: runContext,
1353
1731
  ...runtime ? { runtime } : {},
1354
- onEvent: onAutEvent
1732
+ onEvent: onAutEvent,
1733
+ attach: attachments.attach
1355
1734
  }),
1356
1735
  catch: (cause) => new AutExecutionError({
1357
1736
  cause,
@@ -1359,8 +1738,8 @@ async function runTrial(definition, options) {
1359
1738
  })
1360
1739
  }), (session) => Effect.tryPromise({
1361
1740
  try: () => executeTranscript(definition, session, {
1362
- context: trialContext,
1363
- artifacts: trialWorkspace.artifacts,
1741
+ context: runContext,
1742
+ artifacts: runWorkspace.artifacts,
1364
1743
  events,
1365
1744
  now,
1366
1745
  randomSeed,
@@ -1368,7 +1747,9 @@ async function runTrial(definition, options) {
1368
1747
  emitAut,
1369
1748
  data: scoringData,
1370
1749
  signal,
1371
- progress
1750
+ progress,
1751
+ attach: attachments.attach,
1752
+ defaultJudge: options.judge
1372
1753
  }),
1373
1754
  catch: (cause) => cause
1374
1755
  }), (opened) => Effect.tryPromise({
@@ -1381,7 +1762,6 @@ async function runTrial(definition, options) {
1381
1762
  closeError = error;
1382
1763
  }).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
1383
1764
  runId,
1384
- trialId,
1385
1765
  evalId: definition.id
1386
1766
  })));
1387
1767
  } catch (error) {
@@ -1411,22 +1791,26 @@ async function runTrial(definition, options) {
1411
1791
  skippedScorers,
1412
1792
  data: scoringData,
1413
1793
  checkpoints,
1794
+ attach: attachments.attach,
1795
+ defaultJudge: options.judge,
1414
1796
  now,
1415
1797
  emitRunner
1416
1798
  });
1417
- metrics.push(...builtInMetrics(events));
1799
+ metrics.push(...builtInMetrics(events, [...scoreResults, ...checkpoints]));
1418
1800
  if (context && workspace && !wasCancelled) metrics.push(...await evaluateMetrics(definition, scoringContext({
1419
1801
  context,
1420
1802
  workspace,
1421
1803
  events,
1422
1804
  lastTurn,
1423
1805
  data: scoringData,
1424
- checkpoints
1806
+ checkpoints,
1807
+ transcript: definition.transcript,
1808
+ attach: attachments.attach
1425
1809
  })));
1426
1810
  scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1427
- await trialWriter.writeScores(scoring);
1811
+ await runWriter.writeScores(scoring);
1428
1812
  if (workspace) try {
1429
- artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter, workspaceIgnore);
1813
+ artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
1430
1814
  } catch (captureError) {
1431
1815
  if (!primaryError) primaryError = captureError;
1432
1816
  await tryEmitError(captureError, emitRunner, now);
@@ -1444,49 +1828,34 @@ async function runTrial(definition, options) {
1444
1828
  scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1445
1829
  if (!primaryError) try {
1446
1830
  await emitRunner({
1447
- kind: "trial-completed",
1831
+ kind: "run-completed",
1448
1832
  timestamp: now().toISOString()
1449
1833
  });
1450
1834
  } catch (error) {
1451
1835
  primaryError = error;
1452
1836
  }
1453
- status = wasCancelled ? "cancelled" : primaryError ? "failed" : "completed";
1837
+ status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
1454
1838
  const error = primaryError ? recordError(primaryError) : void 0;
1839
+ const attached = await attachments.close();
1455
1840
  const endedAt = now().toISOString();
1456
- await trialWriter.finalize({
1841
+ const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
1842
+ await runWriter.finalize({
1457
1843
  status,
1458
1844
  endedAt,
1459
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1845
+ durationMs,
1460
1846
  scoring,
1461
1847
  ...artifacts ? { artifacts } : {},
1848
+ ...attached.length ? { attachments: attached } : {},
1462
1849
  ...error ? { error } : {}
1463
1850
  });
1464
- if (!options.runWriter) {
1465
- const counts = trialCounts([{
1466
- status,
1467
- scoring
1468
- }]);
1469
- await runWriter.finalize({
1470
- status,
1471
- endedAt,
1472
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1473
- trialCount: 1,
1474
- passed: counts.passed,
1475
- failed: counts.failed,
1476
- ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1477
- ...error ? { error } : {}
1478
- });
1479
- }
1480
1851
  return {
1481
1852
  runId,
1482
1853
  runUri: canonicalId("run", runId),
1483
- trialId,
1484
- trialUri: canonicalId("trial", trialId),
1485
- trialIndex: options.trialIndex ?? 0,
1486
1854
  evalId: definition.id,
1487
1855
  status,
1856
+ ...batch ? { batch } : {},
1488
1857
  reportLocation: runWriter.location,
1489
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1858
+ durationMs,
1490
1859
  scoring,
1491
1860
  ...error ? { error } : {}
1492
1861
  };
@@ -1500,23 +1869,9 @@ function datasetValues(prepared, audience) {
1500
1869
  }
1501
1870
  return values;
1502
1871
  }
1503
- /** A cancelled trial is neither passed nor failed. */
1504
- function trialCounts(trials) {
1505
- let passed = 0;
1506
- let failed = 0;
1507
- let cancelledTrials = 0;
1508
- for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
1509
- else if (trial.status === "completed" && trial.scoring?.passed) passed++;
1510
- else failed++;
1511
- return {
1512
- passed,
1513
- failed,
1514
- cancelled: cancelledTrials
1515
- };
1516
- }
1517
- /** Persistent workspaces of trials that did not pass are kept for debugging by default. */
1872
+ /** Persistent workspaces of runs that did not pass are kept for debugging by default. */
1518
1873
  function keepWorkspace(policy = "failed", passed) {
1519
1874
  return policy === "all" || policy === "failed" && !passed;
1520
1875
  }
1521
1876
  //#endregion
1522
- export { CheckpointExecutionError as _, readRunSummary as a, ScoringError as b, readTrialScoring as c, trialWorkspacePath as d, staticDatasetFile as f, CancelledError as g, AutExecutionError as h, readRunManifest as i, readTrialSummary as l, globMatcher as m, runMatrix as n, readTrialEvents as o, staticDatasetFiles as p, ownerIsGone as r, readTrialManifest as s, runEval as t, localReportStore as u, DatasetError as v, ReportError as y };
1877
+ export { DatasetError as _, readRunEvents as a, readRunSummary as c, staticDatasetFile as d, staticDatasetFiles as f, CheckpointExecutionError as g, CancelledError as h, ownerIsGone as i, localReportStore as l, AutExecutionError as m, upgradeReports as n, readRunManifest as o, globMatcher as p, runMatrix as r, readRunScoring as s, runEval as t, runWorkspacePath as u, ReportError as v, ScoringError as y };