@leostera/evalkit 0.8.0 → 0.9.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 +46 -2
  2. package/README.md +2 -2
  3. package/dist/cli.mjs +386 -365
  4. package/dist/dashboard/assets/index-By1SJ1on.js +66 -0
  5. package/dist/dashboard/assets/index-geXMXCx9.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-BbVblaHz.mjs} +333 -51
  9. package/dist/index.d.mts +220 -178
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -2
  12. package/dist/runner.d.mts +226 -168
  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 +3 -3
  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 +5 -3
  26. package/dist/skills/evalkit-matrices/SKILL.md +2 -2
  27. package/dist/skills/evalkit-results/SKILL.md +6 -6
  28. package/dist/{src-CZnDXP1K.mjs → src-Bn6fnZnF.mjs} +8 -3
  29. package/dist/{src-LeGSyDD2.mjs → src-BrfIXfO2.mjs} +72 -54
  30. package/dist/{src-mm8L_IxM.mjs → src-CcdLZqFk.mjs} +422 -230
  31. package/dist/upgrade-60SIRius.mjs +110 -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 relativeDatasetPath, A as RunManifestSchema, B as turnResultEvents, C as CheckpointResultSchema, F as RunSummarySchema, G as DEFAULT_ATTACHMENT_LIMITS, H as canonicalParameters, I as ScoreResultSchema, L as TrajectoryEventSchema, M as RunScoringSchema, U as turnTokens, W as DEFAULT_ARTIFACT_EXCLUDE, X as toJsonObject, Y as toJson, _ as parseTrajectoryJsonl, a as authoringId, i as agentRuntimes, j as RunMetadataSchema, ot as resourceUri, p as recordError, w as JsonValueSchema, z as normalizeAutEvent } from "./src-BrfIXfO2.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-60SIRius.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,79 @@ 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
+ //#endregion
35
109
  //#region ../core/src/glob.ts
36
110
  /**
37
111
  * A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
@@ -104,9 +178,9 @@ function ignoreRules(patterns) {
104
178
  };
105
179
  }
106
180
  /**
107
- * Copies the candidate workspace's regular files into the trial report,
181
+ * Copies the candidate workspace's regular files into the run report,
108
182
  * leaving out ignored paths. Symbolic links and special files are skipped,
109
- * never followed, so a snapshot cannot read outside the trial. An ignored
183
+ * never followed, so a snapshot cannot read outside the run. An ignored
110
184
  * directory is walked only when a `!` pattern could re-include something in
111
185
  * it, and is listed only if it ends up containing a snapshotted file.
112
186
  */
@@ -159,7 +233,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAUL
159
233
  const loads = /* @__PURE__ */ new WeakMap();
160
234
  /**
161
235
  * 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.
236
+ * forgotten, so the next run that needs the dataset tries again.
163
237
  */
164
238
  function loadDataset(dataset, root, signal) {
165
239
  let byRoot = loads.get(dataset);
@@ -210,12 +284,12 @@ async function readValueFile(id, file) {
210
284
  }
211
285
  throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
212
286
  }
213
- /** Loads (cached), derives this trial's content with `onRun`, and resolves it. */
214
- async function prepareDatasets(datasets, root, trial) {
287
+ /** Loads (cached), derives this run's content with `onRun`, and resolves it. */
288
+ async function prepareDatasets(datasets, root, run) {
215
289
  const prepared = [];
216
290
  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;
291
+ const loaded = await loadDataset(dataset, root, run.signal);
292
+ const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, run) : loaded;
219
293
  if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
220
294
  prepared.push(await resolveContent(dataset, content, root));
221
295
  }
@@ -283,7 +357,7 @@ async function digest(files, value) {
283
357
  if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
284
358
  return `sha256:${hash.digest("hex")}`;
285
359
  }
286
- function trialDatasets(prepared) {
360
+ function runDatasets(prepared) {
287
361
  return prepared.map(({ dataset, digest }) => ({
288
362
  id: dataset.id,
289
363
  visibility: dataset.visibility,
@@ -332,17 +406,17 @@ async function staticDatasetFile(dataset, root, file) {
332
406
  }
333
407
  //#endregion
334
408
  //#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, "_"));
409
+ /** Where a run's persistent workspace lives below `root`. */
410
+ function runWorkspacePath(root, runId) {
411
+ return path.join(path.resolve(root), runId.replace(/[^a-zA-Z0-9._-]/g, "_"));
338
412
  }
339
413
  /**
340
414
  * Creates isolated candidate and evaluator directories and writes each
341
415
  * dataset's files below its mount, on the side its visibility allows.
342
416
  * Two datasets writing the same file is an error, never an overwrite.
343
417
  */
344
- async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
345
- const root = persistentRoot ? trialWorkspacePath(persistentRoot, trialId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
418
+ async function createRunWorkspace(runId, datasets = [], persistentRoot) {
419
+ const root = persistentRoot ? runWorkspacePath(persistentRoot, runId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-run-"));
346
420
  const persistent = persistentRoot !== void 0;
347
421
  const artifacts = {
348
422
  candidate: { root: path.join(root, "candidate") },
@@ -394,6 +468,20 @@ function collision(datasetId, file, cause) {
394
468
  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
469
  }
396
470
  //#endregion
471
+ //#region ../runner/src/checkpoint-lookup.ts
472
+ /**
473
+ * `checkpoint(rule)` for a scoring context: the latest result of the steps
474
+ * that are this very rule. A rule outside the transcript is a mistake in the
475
+ * scenario, so it throws rather than reading as "not run".
476
+ */
477
+ function checkpointLookup(transcript, results) {
478
+ return ((rule) => {
479
+ const steps = new Set(transcript.flatMap((step, index) => step === rule ? [index] : []));
480
+ 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`);
481
+ for (let index = results.length - 1; index >= 0; index--) if (steps.has(results[index].step)) return results[index];
482
+ });
483
+ }
484
+ //#endregion
397
485
  //#region ../runner/src/score.ts
398
486
  /**
399
487
  * Shared normalization for inline and final predicate and judge rules.
@@ -447,6 +535,7 @@ function runJudgeAgent(rule, agent, input, placement) {
447
535
  yield* Effect.acquireUseRelease(Effect.tryPromise({
448
536
  try: () => agent.start({
449
537
  context,
538
+ attach: input.attach,
450
539
  onEvent: (event) => {
451
540
  const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
452
541
  ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
@@ -583,7 +672,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
583
672
  const annotations = {
584
673
  evalId: input.context.evalId,
585
674
  runId: input.context.runId,
586
- trialId: input.context.trialId,
587
675
  step: index,
588
676
  checkpoint: step.name
589
677
  };
@@ -682,7 +770,7 @@ function userMessage(step, index, { context, randomSeed }) {
682
770
  parameters: context.parameters ?? {},
683
771
  data: context.data,
684
772
  seed: randomSeed,
685
- trialIndex: context.trialIndex
773
+ ...context.batch ? { batch: context.batch } : {}
686
774
  });
687
775
  if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
688
776
  return message;
@@ -713,7 +801,9 @@ async function runCheckpoint(definition, step, index, input) {
713
801
  turn: progress.lastTurn,
714
802
  ...turnOutput(progress.lastTurn),
715
803
  data: input.data,
716
- checkpoints: [...progress.checkpoints]
804
+ checkpoints: [...progress.checkpoints],
805
+ checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
806
+ attach: input.attach
717
807
  }, now, definition.judge)).catch(async (error) => {
718
808
  const recorded = recordError(error);
719
809
  progress.checkpoints.push({
@@ -779,7 +869,9 @@ function scoringContext(input) {
779
869
  ...input.lastTurn ? { turn: input.lastTurn } : {},
780
870
  ...turnOutput(input.lastTurn),
781
871
  data: input.data,
782
- checkpoints: input.checkpoints
872
+ checkpoints: input.checkpoints,
873
+ checkpoint: checkpointLookup(input.transcript, input.checkpoints),
874
+ attach: input.attach
783
875
  };
784
876
  }
785
877
  /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
@@ -801,7 +893,8 @@ async function evaluateFinalScorers(definition, input) {
801
893
  const score = await runBoundary(evaluateRule(scorer, scoringContext({
802
894
  ...input,
803
895
  context,
804
- workspace
896
+ workspace,
897
+ transcript: definition.transcript
805
898
  }), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
806
899
  cause,
807
900
  message: cause.message
@@ -841,7 +934,7 @@ async function evaluateFinalScorers(definition, input) {
841
934
  //#endregion
842
935
  //#region ../runner/src/metrics.ts
843
936
  /**
844
- * What EvalKit measures for every trial from its trajectory: the agent's time
937
+ * What EvalKit measures for every run from its trajectory: the agent's time
845
938
  * (first event to the last one before final scoring), tokens when the agent
846
939
  * reported usage, and tool calls.
847
940
  */
@@ -857,8 +950,7 @@ function builtInMetrics(events) {
857
950
  });
858
951
  let tokens;
859
952
  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));
953
+ const turn = turnTokens(event.usage);
862
954
  if (turn !== void 0) tokens = (tokens ?? 0) + turn;
863
955
  }
864
956
  if (tokens !== void 0) metrics.push({
@@ -903,7 +995,7 @@ async function evaluateMetrics(definition, context) {
903
995
  //#endregion
904
996
  //#region ../runner/src/local-report-store.ts
905
997
  const MANIFEST_FILE = "manifest.json";
906
- /** The project's evals and suites when the run started (schema version 4+). */
998
+ /** The project's evals and suites when the run started. */
907
999
  const CATALOG_FILE = "catalog.json";
908
1000
  async function writeJson(filePath, value) {
909
1001
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
@@ -922,10 +1014,12 @@ function artifactPath(root, value) {
922
1014
  if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
923
1015
  return path.join(root, normalized);
924
1016
  }
925
- var LocalTrialWriter = class {
1017
+ var LocalRunWriter = class {
1018
+ location;
926
1019
  directory;
927
1020
  metadata;
928
- constructor(directory, metadata) {
1021
+ constructor(location, directory, metadata) {
1022
+ this.location = location;
929
1023
  this.directory = directory;
930
1024
  this.metadata = metadata;
931
1025
  }
@@ -933,40 +1027,13 @@ var LocalTrialWriter = class {
933
1027
  return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
934
1028
  }
935
1029
  writeScores(scoring) {
936
- return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(TrialScoringSchema)(scoring));
1030
+ return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
937
1031
  }
938
1032
  async writeArtifact(relativePath, data) {
939
1033
  const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
940
1034
  await mkdir(path.dirname(target), { recursive: true });
941
1035
  await writeFile(target, data);
942
1036
  }
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
1037
  async finalize(summary) {
971
1038
  await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
972
1039
  await replaceJson(path.join(this.directory, MANIFEST_FILE), {
@@ -991,17 +1058,15 @@ function localReportStore(rootDirectory) {
991
1058
  }
992
1059
  //#endregion
993
1060
  //#region ../runner/src/local-report-reader.ts
994
- /** Shared v2/v3 local report boundary for CLI, dashboard, and CI consumers. */
1061
+ /** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
995
1062
  async function decode(file, schema) {
996
1063
  return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
997
1064
  }
998
1065
  const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
999
1066
  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"));
1067
+ const readRunScoring = (root, runId) => decode(join(root, runId, "scoring.json"), RunScoringSchema);
1068
+ async function readRunEvents(root, runId) {
1069
+ return parseTrajectoryJsonl(await readFile(join(root, runId, "trajectory.jsonl"), "utf8"));
1005
1070
  }
1006
1071
  /**
1007
1072
  * True when a run's owning process is known to be gone: it ran on this host
@@ -1028,6 +1093,7 @@ function runMatrix(matrix, options) {
1028
1093
  const iterator = matrix.cells(options.selection);
1029
1094
  const summary = {
1030
1095
  cells: 0,
1096
+ runs: 0,
1031
1097
  passed: 0,
1032
1098
  failed: 0,
1033
1099
  cancelled: 0
@@ -1043,7 +1109,7 @@ function runMatrix(matrix, options) {
1043
1109
  "sandbox"
1044
1110
  ].find((name) => agentRuntimes(cell.eval.agent)[name]);
1045
1111
  const suiteId = options.suiteId ?? options.suiteFor?.(cell);
1046
- const result = yield* runEval(cell.eval, {
1112
+ const runs = yield* runEval(cell.eval, {
1047
1113
  ...options,
1048
1114
  ...suiteId ? { suiteId } : {},
1049
1115
  concurrency: 1,
@@ -1055,18 +1121,232 @@ function runMatrix(matrix, options) {
1055
1121
  }
1056
1122
  });
1057
1123
  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));
1124
+ for (const run of runs) {
1125
+ summary.runs++;
1126
+ if (run.status === "cancelled") summary.cancelled++;
1127
+ else if (run.status === "passed") summary.passed++;
1128
+ else summary.failed++;
1129
+ }
1130
+ if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, runs));
1062
1131
  }
1063
1132
  })), { concurrency });
1064
1133
  return summary;
1065
1134
  });
1066
1135
  }
1067
1136
  //#endregion
1137
+ //#region ../runner/src/upgrade.ts
1138
+ /** A new run being written. */
1139
+ const TEMPORARY = ".upgrade-";
1140
+ /** An old run set aside while its replacement, with the same ID, moves in. */
1141
+ const RETIRED = ".upgraded-";
1142
+ const PER_RUN_FILES = ["archived.json"];
1143
+ /**
1144
+ * Rewrites results from report schema versions 2 to 5 into version 6, in
1145
+ * place (RFD0008). An old run of one trial (most of them) becomes a run with
1146
+ * the same ID, so links to it keep working. An old run of several trials
1147
+ * becomes runs with the trials' IDs, in a batch with the old run's ID.
1148
+ *
1149
+ * Each new run is written under a temporary name and renamed into place;
1150
+ * artifacts are moved, not copied, as the last step before that rename. An
1151
+ * old run is removed only after its new runs exist, and one replaced under
1152
+ * the same ID is set aside first, so a crash at any point leaves the old run
1153
+ * or complete new ones, and re-running finishes the job. Version 6 runs are
1154
+ * left alone.
1155
+ */
1156
+ async function upgradeReports(root, options = {}) {
1157
+ const report = {
1158
+ upgraded: 0,
1159
+ runs: 0,
1160
+ batches: 0,
1161
+ running: []
1162
+ };
1163
+ let entries;
1164
+ try {
1165
+ entries = await readdir(root);
1166
+ } catch {
1167
+ return report;
1168
+ }
1169
+ if (!options.dryRun) await finishReplacements(root, entries);
1170
+ for (const entry of entries) {
1171
+ if (entry.startsWith(".")) continue;
1172
+ const directory = join(root, entry);
1173
+ const manifest = await readJson(join(directory, "manifest.json"));
1174
+ if (!manifest || !UPGRADABLE_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) continue;
1175
+ const summary = await readJson(join(directory, "summary.json"));
1176
+ if (!summary && !ownerIsGone(manifest.process)) {
1177
+ report.running.push(entry);
1178
+ continue;
1179
+ }
1180
+ const trials = await trialIds(directory);
1181
+ report.upgraded += 1;
1182
+ report.runs += Math.max(trials.length, 1);
1183
+ if (trials.length > 1) report.batches += 1;
1184
+ if (options.dryRun) continue;
1185
+ await upgradeRun(root, entry, manifest, summary, trials);
1186
+ }
1187
+ if (!options.dryRun) await removeLeftovers(root);
1188
+ return report;
1189
+ }
1190
+ /**
1191
+ * Finishes same-ID replacements a crash interrupted: the old run was set
1192
+ * aside, and its replacement may or may not have moved in.
1193
+ */
1194
+ async function finishReplacements(root, entries) {
1195
+ for (const entry of entries) {
1196
+ if (!entry.startsWith(RETIRED)) continue;
1197
+ const id = entry.slice(10);
1198
+ const retired = join(root, entry);
1199
+ const directory = join(root, id);
1200
+ const replacement = join(root, `${TEMPORARY}${id}`);
1201
+ if (!await exists(directory)) await rename(await exists(replacement) ? replacement : retired, directory);
1202
+ await rm(retired, {
1203
+ recursive: true,
1204
+ force: true
1205
+ });
1206
+ }
1207
+ }
1208
+ /**
1209
+ * Temporary folders nothing will finish. One holding artifacts is kept: it
1210
+ * could be the only copy, and an old run would have moved them back.
1211
+ */
1212
+ async function removeLeftovers(root) {
1213
+ for (const entry of await readdir(root)) if (entry.startsWith(TEMPORARY) && !await exists(join(root, entry, "artifacts"))) await rm(join(root, entry), {
1214
+ recursive: true,
1215
+ force: true
1216
+ });
1217
+ }
1218
+ /** Swaps a fully written replacement in for the old run of the same ID. */
1219
+ async function replaceInPlace(root, id, replacement) {
1220
+ const retired = join(root, `${RETIRED}${id}`);
1221
+ await rename(join(root, id), retired);
1222
+ await rename(replacement, join(root, id));
1223
+ await rm(retired, {
1224
+ recursive: true,
1225
+ force: true
1226
+ });
1227
+ }
1228
+ async function upgradeRun(root, oldRunId, runManifest, runSummary, trials) {
1229
+ const directory = join(root, oldRunId);
1230
+ const catalog = await readJson(join(directory, "catalog.json"));
1231
+ const extras = await presentFiles(directory, PER_RUN_FILES);
1232
+ if (!trials.length) {
1233
+ const target = join(root, `${TEMPORARY}${oldRunId}`);
1234
+ await mkdir(target, { recursive: true });
1235
+ const status = runSummary ? "cancelled" : "running";
1236
+ await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1237
+ run: runManifest,
1238
+ runId: oldRunId,
1239
+ status
1240
+ }));
1241
+ if (runSummary) await writeJsonFile(join(target, "summary.json"), {
1242
+ status,
1243
+ endedAt: runSummary.endedAt,
1244
+ ...runSummary.durationMs === void 0 ? {} : { durationMs: runSummary.durationMs },
1245
+ ...runSummary.error ? { error: runSummary.error } : {}
1246
+ });
1247
+ await copyShared(directory, target, catalog, extras);
1248
+ await replaceInPlace(root, oldRunId, target);
1249
+ return;
1250
+ }
1251
+ const inPlace = trials.length === 1;
1252
+ for (const trialId of trials) {
1253
+ const runId = inPlace ? oldRunId : trialId;
1254
+ const destination = join(root, runId);
1255
+ if (!inPlace && await exists(join(destination, "manifest.json"))) continue;
1256
+ const source = join(directory, "trials", trialId);
1257
+ const trialManifest = await readJson(join(source, "manifest.json")) ?? {};
1258
+ const trialSummary = await readJson(join(source, "summary.json"));
1259
+ const passed = (trialSummary?.scoring)?.passed;
1260
+ const status = upgradeStatus(trialSummary?.status ?? trialManifest.status, passed);
1261
+ const index = typeof trialManifest.trialIndex === "number" ? trialManifest.trialIndex : trials.indexOf(trialId);
1262
+ const target = join(root, `${TEMPORARY}${runId}`);
1263
+ if (!await exists(join(source, "artifacts")) && await exists(join(target, "artifacts"))) await rename(join(target, "artifacts"), join(source, "artifacts"));
1264
+ await rm(target, {
1265
+ recursive: true,
1266
+ force: true
1267
+ });
1268
+ await mkdir(target, { recursive: true });
1269
+ await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1270
+ run: runManifest,
1271
+ trial: trialManifest,
1272
+ runId,
1273
+ ...!inPlace ? { batch: {
1274
+ id: oldRunId,
1275
+ index,
1276
+ size: trials.length
1277
+ } } : {},
1278
+ status
1279
+ }));
1280
+ if (trialSummary) await writeJsonFile(join(target, "summary.json"), {
1281
+ ...trialSummary,
1282
+ status
1283
+ });
1284
+ const scoring = await readText(join(source, "scoring.json"));
1285
+ if (scoring !== void 0) await writeFile(join(target, "scoring.json"), scoring);
1286
+ const trajectory = await readText(join(source, "trajectory.jsonl"));
1287
+ if (trajectory !== void 0) await writeFile(join(target, "trajectory.jsonl"), upgradeTrajectory(trajectory));
1288
+ await copyShared(directory, target, catalog, extras);
1289
+ if (await exists(join(source, "artifacts"))) await rename(join(source, "artifacts"), join(target, "artifacts"));
1290
+ if (inPlace) await replaceInPlace(root, oldRunId, target);
1291
+ else await rename(target, destination);
1292
+ }
1293
+ if (!inPlace) await rm(directory, {
1294
+ recursive: true,
1295
+ force: true
1296
+ });
1297
+ }
1298
+ async function copyShared(from, to, catalog, extras) {
1299
+ if (catalog) await writeJsonFile(join(to, "catalog.json"), upgradeCatalog(catalog));
1300
+ for (const file of extras) await cp(join(from, file), join(to, file));
1301
+ }
1302
+ async function trialIds(directory) {
1303
+ try {
1304
+ return (await readdir(join(directory, "trials"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1305
+ } catch {
1306
+ return [];
1307
+ }
1308
+ }
1309
+ async function presentFiles(directory, names) {
1310
+ const present = [];
1311
+ for (const name of names) if (await exists(join(directory, name))) present.push(name);
1312
+ return present;
1313
+ }
1314
+ async function exists(path) {
1315
+ try {
1316
+ await stat(path);
1317
+ return true;
1318
+ } catch {
1319
+ return false;
1320
+ }
1321
+ }
1322
+ async function readText(path) {
1323
+ try {
1324
+ return await readFile(path, "utf8");
1325
+ } catch {
1326
+ return;
1327
+ }
1328
+ }
1329
+ async function readJson(path) {
1330
+ const text = await readText(path);
1331
+ if (text === void 0) return void 0;
1332
+ let value;
1333
+ try {
1334
+ value = JSON.parse(text);
1335
+ } catch (error) {
1336
+ throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1337
+ }
1338
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1339
+ }
1340
+ async function writeJsonFile(path, value) {
1341
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
1342
+ }
1343
+ //#endregion
1068
1344
  //#region ../runner/src/index.ts
1069
- /** Effect-native aggregate eval execution entry point. */
1345
+ /**
1346
+ * Runs a scenario: once, or `repeat` times as one batch. Resolves to one
1347
+ * result per run started; runs never started because of cancellation are
1348
+ * left out.
1349
+ */
1070
1350
  function runEval(definition, options) {
1071
1351
  return Effect.gen(function* () {
1072
1352
  yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
@@ -1074,19 +1354,19 @@ function runEval(definition, options) {
1074
1354
  ...options.runId ? { runId: options.runId } : {},
1075
1355
  ...options.suiteId ? { suiteId: options.suiteId } : {}
1076
1356
  }));
1077
- const result = yield* Effect.tryPromise({
1078
- try: () => executeRun(definition, options),
1357
+ const runs = yield* Effect.tryPromise({
1358
+ try: () => executeBatch(definition, options),
1079
1359
  catch: (error) => error
1080
1360
  });
1081
1361
  yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
1082
1362
  evalId: authoringId(definition),
1083
- runId: result.runId,
1084
- status: result.status
1363
+ runs: runs.length,
1364
+ passed: runs.filter((run) => run.status === "passed").length
1085
1365
  }));
1086
- return result;
1366
+ return runs;
1087
1367
  });
1088
1368
  }
1089
- function createId(_kind) {
1369
+ function createId() {
1090
1370
  return crypto.randomUUID();
1091
1371
  }
1092
1372
  function canonicalId(kind, id) {
@@ -1095,9 +1375,9 @@ function canonicalId(kind, id) {
1095
1375
  function assertUuid(value, label) {
1096
1376
  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
1377
  }
1098
- function runMetadata(definition, options, runId, startedAt) {
1378
+ function runMetadata(definition, options, runId, startedAt, extra) {
1099
1379
  return {
1100
- schemaVersion: 5,
1380
+ schemaVersion: 6,
1101
1381
  runId,
1102
1382
  runUri: canonicalId("run", runId),
1103
1383
  evalId: definition.id,
@@ -1110,7 +1390,9 @@ function runMetadata(definition, options, runId, startedAt) {
1110
1390
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1111
1391
  parameters: options.parameters,
1112
1392
  matrix: options.matrix,
1113
- startedAt
1393
+ startedAt,
1394
+ ...options.batch ? { batch: options.batch } : {},
1395
+ ...extra
1114
1396
  };
1115
1397
  }
1116
1398
  /** Recording a secondary error must never mask the original failure or block cleanup. */
@@ -1123,104 +1405,52 @@ async function tryEmitError(error, emitRunner, now) {
1123
1405
  });
1124
1406
  } catch {}
1125
1407
  }
1126
- /** Executes one aggregate eval run and its requested isolated trials. */
1127
- async function executeRun(definition, options) {
1408
+ /** Starts `repeat` runs of a scenario, as one batch when there are several. */
1409
+ async function executeBatch(definition, options) {
1128
1410
  let hasUser = false;
1129
1411
  for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
1130
1412
  else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
1131
1413
  if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
1414
+ assertNoRemovedOptions(definition);
1132
1415
  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 });
1416
+ const repeat = options.repeat ?? definition.repeat ?? 1;
1417
+ if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
1418
+ if (repeat === 1) return [await runOnce(definition, options)];
1419
+ if (options.runId) throw new Error("runId names one run; it cannot be used with repeat");
1420
+ const batchId = createId();
1141
1421
  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, {
1422
+ const runs = Array.from({ length: repeat }, (_, index) => {
1423
+ const run = Effect.tryPromise({
1424
+ try: async () => signal?.aborted ? void 0 : runOnce(definition, {
1145
1425
  ...options,
1146
- runId,
1147
- trialId: createId("trial"),
1148
- trials: 1,
1149
- runWriter,
1150
- trialIndex
1426
+ batch: {
1427
+ id: batchId,
1428
+ index,
1429
+ size: repeat
1430
+ }
1151
1431
  }),
1152
1432
  catch: (error) => error
1153
1433
  });
1154
- return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
1434
+ return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
1155
1435
  });
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 } : {}
1168
- });
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
- };
1436
+ return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
1206
1437
  }
1207
- async function runTrial(definition, options) {
1438
+ /** One execution: a session in its own sandbox, scored and reported. */
1439
+ async function runOnce(definition, options) {
1208
1440
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
1209
- const runId = options.runId ?? createId("run");
1210
- const trialId = options.trialId ?? createId("trial");
1441
+ const runId = options.runId ?? createId();
1211
1442
  const startedAt = now().toISOString();
1212
1443
  const selectedRuntime = options.runtime;
1213
1444
  const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
1214
1445
  if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
1215
1446
  const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
1216
- const trialIndex = options.trialIndex ?? 0;
1447
+ const batch = options.batch;
1217
1448
  const signal = options.signal ?? new AbortController().signal;
1218
1449
  const root = options.root ?? process.cwd();
1219
1450
  const baseContext = {
1220
1451
  runId,
1221
1452
  evalId: authoringId(definition),
1222
- trialId,
1223
- trialIndex,
1453
+ ...batch ? { batch } : {},
1224
1454
  metadata: {
1225
1455
  ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
1226
1456
  randomSeed
@@ -1234,8 +1464,7 @@ async function runTrial(definition, options) {
1234
1464
  prepared = await prepareDatasets(definition.datasets ?? [], root, {
1235
1465
  evalId: baseContext.evalId,
1236
1466
  runId,
1237
- trialId,
1238
- trialIndex,
1467
+ ...batch ? { batch } : {},
1239
1468
  seed: randomSeed,
1240
1469
  parameters: options.parameters ?? {},
1241
1470
  signal
@@ -1247,40 +1476,28 @@ async function runTrial(definition, options) {
1247
1476
  const scoringData = datasetValues(prepared, "all");
1248
1477
  const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
1249
1478
  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,
1479
+ const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
1263
1480
  workspaceIgnore,
1264
- ...prepared.length ? { datasets: trialDatasets(prepared) } : {}
1265
- });
1481
+ ...prepared.length ? { datasets: runDatasets(prepared) } : {}
1482
+ }), { catalog: options.catalog });
1483
+ const attachments = attachmentStore(runWriter, options.attachmentLimits);
1266
1484
  let writeChain = Promise.resolve();
1267
1485
  let reportingFailed = false;
1268
- const trialRef = {
1486
+ const runRef = {
1269
1487
  runId,
1270
- trialId,
1271
- trialIndex,
1488
+ ...batch ? { batch } : {},
1272
1489
  evalId: definition.id,
1273
1490
  ...options.suiteId ? { suiteId: options.suiteId } : {},
1274
1491
  ...options.parameters ? { parameters: options.parameters } : {}
1275
1492
  };
1276
1493
  const observe = (event) => {
1277
1494
  try {
1278
- options.onEvent?.(trialRef, event);
1495
+ options.onEvent?.(runRef, event);
1279
1496
  } catch {}
1280
1497
  };
1281
1498
  const append = async (event) => {
1282
1499
  events.push(event);
1283
- writeChain = writeChain.then(() => trialWriter.appendEvent(event));
1500
+ writeChain = writeChain.then(() => runWriter.appendEvent(event));
1284
1501
  try {
1285
1502
  await writeChain;
1286
1503
  observe(event);
@@ -1316,7 +1533,7 @@ async function runTrial(definition, options) {
1316
1533
  const workspaceScope = await Effect.runPromise(Scope.make());
1317
1534
  try {
1318
1535
  await emitRunner({
1319
- kind: "trial-started",
1536
+ kind: "run-started",
1320
1537
  timestamp: now().toISOString()
1321
1538
  });
1322
1539
  if (signal.aborted) throw cancelled();
@@ -1325,7 +1542,7 @@ async function runTrial(definition, options) {
1325
1542
  message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1326
1543
  });
1327
1544
  workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
1328
- try: () => createTrialWorkspace(trialId, prepared, options.workspaceRoot),
1545
+ try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
1329
1546
  catch: (cause) => new DatasetError({
1330
1547
  cause,
1331
1548
  message: cause instanceof Error ? cause.message : String(cause)
@@ -1338,20 +1555,21 @@ async function runTrial(definition, options) {
1338
1555
  })
1339
1556
  }).pipe(Effect.catchAll((error) => Effect.sync(() => {
1340
1557
  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 = {
1558
+ }).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
1559
+ const runWorkspace = workspace;
1560
+ const runContext = {
1344
1561
  ...baseContext,
1345
1562
  data: candidateData,
1346
- workspace: trialWorkspace.artifacts.candidate,
1347
- evaluatorWorkspace: trialWorkspace.artifacts.evaluator
1563
+ workspace: runWorkspace.artifacts.candidate,
1564
+ evaluatorWorkspace: runWorkspace.artifacts.evaluator
1348
1565
  };
1349
- context = trialContext;
1566
+ context = runContext;
1350
1567
  await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
1351
1568
  try: () => definition.agent.start({
1352
- context: trialContext,
1569
+ context: runContext,
1353
1570
  ...runtime ? { runtime } : {},
1354
- onEvent: onAutEvent
1571
+ onEvent: onAutEvent,
1572
+ attach: attachments.attach
1355
1573
  }),
1356
1574
  catch: (cause) => new AutExecutionError({
1357
1575
  cause,
@@ -1359,8 +1577,8 @@ async function runTrial(definition, options) {
1359
1577
  })
1360
1578
  }), (session) => Effect.tryPromise({
1361
1579
  try: () => executeTranscript(definition, session, {
1362
- context: trialContext,
1363
- artifacts: trialWorkspace.artifacts,
1580
+ context: runContext,
1581
+ artifacts: runWorkspace.artifacts,
1364
1582
  events,
1365
1583
  now,
1366
1584
  randomSeed,
@@ -1368,7 +1586,8 @@ async function runTrial(definition, options) {
1368
1586
  emitAut,
1369
1587
  data: scoringData,
1370
1588
  signal,
1371
- progress
1589
+ progress,
1590
+ attach: attachments.attach
1372
1591
  }),
1373
1592
  catch: (cause) => cause
1374
1593
  }), (opened) => Effect.tryPromise({
@@ -1381,7 +1600,6 @@ async function runTrial(definition, options) {
1381
1600
  closeError = error;
1382
1601
  }).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
1383
1602
  runId,
1384
- trialId,
1385
1603
  evalId: definition.id
1386
1604
  })));
1387
1605
  } catch (error) {
@@ -1411,6 +1629,7 @@ async function runTrial(definition, options) {
1411
1629
  skippedScorers,
1412
1630
  data: scoringData,
1413
1631
  checkpoints,
1632
+ attach: attachments.attach,
1414
1633
  now,
1415
1634
  emitRunner
1416
1635
  });
@@ -1421,12 +1640,14 @@ async function runTrial(definition, options) {
1421
1640
  events,
1422
1641
  lastTurn,
1423
1642
  data: scoringData,
1424
- checkpoints
1643
+ checkpoints,
1644
+ transcript: definition.transcript,
1645
+ attach: attachments.attach
1425
1646
  })));
1426
1647
  scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1427
- await trialWriter.writeScores(scoring);
1648
+ await runWriter.writeScores(scoring);
1428
1649
  if (workspace) try {
1429
- artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter, workspaceIgnore);
1650
+ artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
1430
1651
  } catch (captureError) {
1431
1652
  if (!primaryError) primaryError = captureError;
1432
1653
  await tryEmitError(captureError, emitRunner, now);
@@ -1444,49 +1665,34 @@ async function runTrial(definition, options) {
1444
1665
  scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1445
1666
  if (!primaryError) try {
1446
1667
  await emitRunner({
1447
- kind: "trial-completed",
1668
+ kind: "run-completed",
1448
1669
  timestamp: now().toISOString()
1449
1670
  });
1450
1671
  } catch (error) {
1451
1672
  primaryError = error;
1452
1673
  }
1453
- status = wasCancelled ? "cancelled" : primaryError ? "failed" : "completed";
1674
+ status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
1454
1675
  const error = primaryError ? recordError(primaryError) : void 0;
1676
+ const attached = await attachments.close();
1455
1677
  const endedAt = now().toISOString();
1456
- await trialWriter.finalize({
1678
+ const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
1679
+ await runWriter.finalize({
1457
1680
  status,
1458
1681
  endedAt,
1459
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1682
+ durationMs,
1460
1683
  scoring,
1461
1684
  ...artifacts ? { artifacts } : {},
1685
+ ...attached.length ? { attachments: attached } : {},
1462
1686
  ...error ? { error } : {}
1463
1687
  });
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
1688
  return {
1481
1689
  runId,
1482
1690
  runUri: canonicalId("run", runId),
1483
- trialId,
1484
- trialUri: canonicalId("trial", trialId),
1485
- trialIndex: options.trialIndex ?? 0,
1486
1691
  evalId: definition.id,
1487
1692
  status,
1693
+ ...batch ? { batch } : {},
1488
1694
  reportLocation: runWriter.location,
1489
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1695
+ durationMs,
1490
1696
  scoring,
1491
1697
  ...error ? { error } : {}
1492
1698
  };
@@ -1500,23 +1706,9 @@ function datasetValues(prepared, audience) {
1500
1706
  }
1501
1707
  return values;
1502
1708
  }
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. */
1709
+ /** Persistent workspaces of runs that did not pass are kept for debugging by default. */
1518
1710
  function keepWorkspace(policy = "failed", passed) {
1519
1711
  return policy === "all" || policy === "failed" && !passed;
1520
1712
  }
1521
1713
  //#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 };
1714
+ 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 };