@leostera/evalkit 0.7.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 +56 -1
  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 +239 -178
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -2
  12. package/dist/runner.d.mts +246 -169
  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-b2Wnmzbb.mjs → src-BrfIXfO2.mjs} +75 -54
  30. package/dist/{src-DhromKNu.mjs → src-CcdLZqFk.mjs} +487 -250
  31. package/dist/upgrade-60SIRius.mjs +110 -0
  32. package/package.json +1 -1
  33. package/dist/dashboard/assets/index-BEBo_81Y.js +0 -34
  34. package/dist/dashboard/assets/index-C24IquYZ.css +0 -2
@@ -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-b2Wnmzbb.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, `**`
@@ -63,41 +137,82 @@ function globMatcher(pattern) {
63
137
  }
64
138
  //#endregion
65
139
  //#region ../runner/src/snapshot.ts
66
- /** A pattern without `/` matches a name at any depth, like `.gitignore`. */
67
- function excludeMatcher(patterns) {
68
- const matchers = patterns.map((pattern) => {
69
- const trimmed = pattern.replace(/^\.\//, "").replace(/\/$/, "");
70
- const match = globMatcher(trimmed);
71
- return trimmed.includes("/") ? match : (relativePath) => match(path.posix.basename(relativePath));
72
- });
73
- return (relativePath) => matchers.some((match) => match(relativePath));
140
+ const GLOB_CHARS = /[*?{]/;
141
+ function ignoreRule(pattern) {
142
+ const negated = pattern.startsWith("!");
143
+ const trimmed = (negated ? pattern.slice(1) : pattern).replace(/^\.\//, "").replace(/\/$/, "");
144
+ const match = globMatcher(trimmed);
145
+ if (!trimmed.includes("/")) return {
146
+ negated,
147
+ match: (relativePath) => match(path.posix.basename(relativePath)),
148
+ mayMatchBelow: () => true
149
+ };
150
+ const segments = trimmed.split("/");
151
+ const firstGlob = segments.findIndex((segment) => GLOB_CHARS.test(segment));
152
+ const prefix = segments.slice(0, firstGlob === -1 ? segments.length : firstGlob).join("/");
153
+ return {
154
+ negated,
155
+ match,
156
+ mayMatchBelow: (directory) => prefix === "" || prefix.startsWith(`${directory}/`) || prefix === directory || directory.startsWith(`${prefix}/`)
157
+ };
158
+ }
159
+ /**
160
+ * Ignore rules for candidate snapshots, read like `.gitignore`: globs relative
161
+ * to the workspace root, where a pattern without `/` matches that name at any
162
+ * depth, and later patterns win. `!pattern` re-includes a path; unlike Git, it
163
+ * also works below an ignored directory (`_build` then `!_build/doc/**`).
164
+ */
165
+ function ignoreRules(patterns) {
166
+ const rules = patterns.map(ignoreRule);
167
+ return {
168
+ /** Whether a path is ignored, given whether its directory was. */
169
+ ignored(relativePath, parentIgnored = false) {
170
+ let ignored = parentIgnored;
171
+ for (const rule of rules) if (rule.match(relativePath)) ignored = !rule.negated;
172
+ return ignored;
173
+ },
174
+ /** Whether an ignored directory must still be walked for re-included paths. */
175
+ mayReinclude(directory) {
176
+ return rules.some((rule) => rule.negated && rule.mayMatchBelow(directory));
177
+ }
178
+ };
74
179
  }
75
180
  /**
76
- * Copies the candidate workspace's regular files into the trial report,
77
- * leaving out excluded paths. Symbolic links and special files are skipped,
78
- * never followed, so a snapshot cannot read outside the trial.
181
+ * Copies the candidate workspace's regular files into the run report,
182
+ * leaving out ignored paths. Symbolic links and special files are skipped,
183
+ * never followed, so a snapshot cannot read outside the run. An ignored
184
+ * directory is walked only when a `!` pattern could re-include something in
185
+ * it, and is listed only if it ends up containing a snapshotted file.
79
186
  */
80
- async function snapshotCandidateWorkspace(candidateRoot, writer, exclude = DEFAULT_ARTIFACT_EXCLUDE) {
187
+ async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAULT_ARTIFACT_EXCLUDE) {
81
188
  const artifacts = [];
82
- const excluded = excludeMatcher(exclude);
83
- async function visit(directory, relativeDirectory) {
189
+ const rules = ignoreRules(ignore);
190
+ /** Returns whether anything below `directory` was snapshotted. */
191
+ async function visit(directory, relativeDirectory, directoryIgnored) {
84
192
  const entries = await readdir(directory, { withFileTypes: true });
85
193
  entries.sort((left, right) => left.name.localeCompare(right.name));
194
+ let kept = false;
86
195
  for (const entry of entries) {
87
196
  const absolutePath = path.join(directory, entry.name);
88
197
  const relativePath = path.join(relativeDirectory, entry.name);
89
- if (excluded(relativePath.split(path.sep).join("/"))) continue;
198
+ const posixPath = relativePath.split(path.sep).join("/");
199
+ const ignored = rules.ignored(posixPath, directoryIgnored);
90
200
  const stat = await lstat(absolutePath);
91
201
  if (stat.isSymbolicLink()) continue;
92
202
  if (stat.isDirectory()) {
93
- artifacts.push({
94
- path: path.join("candidate", relativePath),
95
- kind: "directory"
96
- });
97
- await visit(absolutePath, relativePath);
203
+ if (ignored && !rules.mayReinclude(posixPath)) continue;
204
+ const entryIndex = artifacts.length;
205
+ const keptBelow = await visit(absolutePath, relativePath, ignored);
206
+ if (!ignored || keptBelow) {
207
+ artifacts.splice(entryIndex, 0, {
208
+ path: path.join("candidate", relativePath),
209
+ kind: "directory"
210
+ });
211
+ kept = true;
212
+ }
98
213
  continue;
99
214
  }
100
- if (!stat.isFile()) continue;
215
+ if (ignored || !stat.isFile()) continue;
101
216
  const data = await readFile(absolutePath);
102
217
  const artifactPath = path.join("candidate", relativePath);
103
218
  await writer.writeArtifact(artifactPath, data);
@@ -106,9 +221,11 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, exclude = DEFAU
106
221
  kind: "file",
107
222
  size: data.byteLength
108
223
  });
224
+ kept = true;
109
225
  }
226
+ return kept;
110
227
  }
111
- await visit(candidateRoot, "");
228
+ await visit(candidateRoot, "", false);
112
229
  return artifacts;
113
230
  }
114
231
  //#endregion
@@ -116,7 +233,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, exclude = DEFAU
116
233
  const loads = /* @__PURE__ */ new WeakMap();
117
234
  /**
118
235
  * Loads a dataset at most once per process and project root. A failed load is
119
- * forgotten, so the next trial that needs the dataset tries again.
236
+ * forgotten, so the next run that needs the dataset tries again.
120
237
  */
121
238
  function loadDataset(dataset, root, signal) {
122
239
  let byRoot = loads.get(dataset);
@@ -167,12 +284,12 @@ async function readValueFile(id, file) {
167
284
  }
168
285
  throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
169
286
  }
170
- /** Loads (cached), derives this trial's content with `onRun`, and resolves it. */
171
- 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) {
172
289
  const prepared = [];
173
290
  for (const dataset of datasets) {
174
- const loaded = await loadDataset(dataset, root, trial.signal);
175
- 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;
176
293
  if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
177
294
  prepared.push(await resolveContent(dataset, content, root));
178
295
  }
@@ -240,7 +357,7 @@ async function digest(files, value) {
240
357
  if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
241
358
  return `sha256:${hash.digest("hex")}`;
242
359
  }
243
- function trialDatasets(prepared) {
360
+ function runDatasets(prepared) {
244
361
  return prepared.map(({ dataset, digest }) => ({
245
362
  id: dataset.id,
246
363
  visibility: dataset.visibility,
@@ -289,17 +406,17 @@ async function staticDatasetFile(dataset, root, file) {
289
406
  }
290
407
  //#endregion
291
408
  //#region ../runner/src/workspace.ts
292
- /** Where a trial's persistent workspace lives below `root`. */
293
- function trialWorkspacePath(root, trialId) {
294
- 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, "_"));
295
412
  }
296
413
  /**
297
414
  * Creates isolated candidate and evaluator directories and writes each
298
415
  * dataset's files below its mount, on the side its visibility allows.
299
416
  * Two datasets writing the same file is an error, never an overwrite.
300
417
  */
301
- async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
302
- 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-"));
303
420
  const persistent = persistentRoot !== void 0;
304
421
  const artifacts = {
305
422
  candidate: { root: path.join(root, "candidate") },
@@ -351,6 +468,20 @@ function collision(datasetId, file, cause) {
351
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));
352
469
  }
353
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
354
485
  //#region ../runner/src/score.ts
355
486
  /**
356
487
  * Shared normalization for inline and final predicate and judge rules.
@@ -404,6 +535,7 @@ function runJudgeAgent(rule, agent, input, placement) {
404
535
  yield* Effect.acquireUseRelease(Effect.tryPromise({
405
536
  try: () => agent.start({
406
537
  context,
538
+ attach: input.attach,
407
539
  onEvent: (event) => {
408
540
  const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
409
541
  ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
@@ -540,7 +672,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
540
672
  const annotations = {
541
673
  evalId: input.context.evalId,
542
674
  runId: input.context.runId,
543
- trialId: input.context.trialId,
544
675
  step: index,
545
676
  checkpoint: step.name
546
677
  };
@@ -639,7 +770,7 @@ function userMessage(step, index, { context, randomSeed }) {
639
770
  parameters: context.parameters ?? {},
640
771
  data: context.data,
641
772
  seed: randomSeed,
642
- trialIndex: context.trialIndex
773
+ ...context.batch ? { batch: context.batch } : {}
643
774
  });
644
775
  if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
645
776
  return message;
@@ -670,7 +801,9 @@ async function runCheckpoint(definition, step, index, input) {
670
801
  turn: progress.lastTurn,
671
802
  ...turnOutput(progress.lastTurn),
672
803
  data: input.data,
673
- checkpoints: [...progress.checkpoints]
804
+ checkpoints: [...progress.checkpoints],
805
+ checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
806
+ attach: input.attach
674
807
  }, now, definition.judge)).catch(async (error) => {
675
808
  const recorded = recordError(error);
676
809
  progress.checkpoints.push({
@@ -736,7 +869,9 @@ function scoringContext(input) {
736
869
  ...input.lastTurn ? { turn: input.lastTurn } : {},
737
870
  ...turnOutput(input.lastTurn),
738
871
  data: input.data,
739
- checkpoints: input.checkpoints
872
+ checkpoints: input.checkpoints,
873
+ checkpoint: checkpointLookup(input.transcript, input.checkpoints),
874
+ attach: input.attach
740
875
  };
741
876
  }
742
877
  /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
@@ -758,7 +893,8 @@ async function evaluateFinalScorers(definition, input) {
758
893
  const score = await runBoundary(evaluateRule(scorer, scoringContext({
759
894
  ...input,
760
895
  context,
761
- workspace
896
+ workspace,
897
+ transcript: definition.transcript
762
898
  }), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
763
899
  cause,
764
900
  message: cause.message
@@ -798,7 +934,7 @@ async function evaluateFinalScorers(definition, input) {
798
934
  //#endregion
799
935
  //#region ../runner/src/metrics.ts
800
936
  /**
801
- * 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
802
938
  * (first event to the last one before final scoring), tokens when the agent
803
939
  * reported usage, and tool calls.
804
940
  */
@@ -814,8 +950,7 @@ function builtInMetrics(events) {
814
950
  });
815
951
  let tokens;
816
952
  for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
817
- const { totalTokens, inputTokens, outputTokens } = event.usage;
818
- const turn = totalTokens ?? (inputTokens === void 0 && outputTokens === void 0 ? void 0 : (inputTokens ?? 0) + (outputTokens ?? 0));
953
+ const turn = turnTokens(event.usage);
819
954
  if (turn !== void 0) tokens = (tokens ?? 0) + turn;
820
955
  }
821
956
  if (tokens !== void 0) metrics.push({
@@ -860,7 +995,7 @@ async function evaluateMetrics(definition, context) {
860
995
  //#endregion
861
996
  //#region ../runner/src/local-report-store.ts
862
997
  const MANIFEST_FILE = "manifest.json";
863
- /** The project's evals and suites when the run started (schema version 4+). */
998
+ /** The project's evals and suites when the run started. */
864
999
  const CATALOG_FILE = "catalog.json";
865
1000
  async function writeJson(filePath, value) {
866
1001
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
@@ -879,10 +1014,12 @@ function artifactPath(root, value) {
879
1014
  if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
880
1015
  return path.join(root, normalized);
881
1016
  }
882
- var LocalTrialWriter = class {
1017
+ var LocalRunWriter = class {
1018
+ location;
883
1019
  directory;
884
1020
  metadata;
885
- constructor(directory, metadata) {
1021
+ constructor(location, directory, metadata) {
1022
+ this.location = location;
886
1023
  this.directory = directory;
887
1024
  this.metadata = metadata;
888
1025
  }
@@ -890,40 +1027,13 @@ var LocalTrialWriter = class {
890
1027
  return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
891
1028
  }
892
1029
  writeScores(scoring) {
893
- 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));
894
1031
  }
895
1032
  async writeArtifact(relativePath, data) {
896
1033
  const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
897
1034
  await mkdir(path.dirname(target), { recursive: true });
898
1035
  await writeFile(target, data);
899
1036
  }
900
- async finalize(summary) {
901
- await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(TrialSummarySchema)(summary));
902
- await replaceJson(path.join(this.directory, MANIFEST_FILE), {
903
- ...Schema.encodeSync(TrialMetadataSchema)(this.metadata),
904
- status: summary.status
905
- });
906
- }
907
- };
908
- var LocalRunWriter = class {
909
- location;
910
- directory;
911
- metadata;
912
- constructor(location, directory, metadata) {
913
- this.location = location;
914
- this.directory = directory;
915
- this.metadata = metadata;
916
- }
917
- async startTrial(metadata) {
918
- assertPathSegment(metadata.trialId, "trialId");
919
- const directory = path.join(this.directory, "trials", metadata.trialId);
920
- await mkdir(directory, { recursive: true });
921
- await writeJson(path.join(directory, MANIFEST_FILE), {
922
- ...Schema.encodeSync(TrialMetadataSchema)(metadata),
923
- status: "running"
924
- });
925
- return new LocalTrialWriter(directory, metadata);
926
- }
927
1037
  async finalize(summary) {
928
1038
  await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
929
1039
  await replaceJson(path.join(this.directory, MANIFEST_FILE), {
@@ -948,17 +1058,15 @@ function localReportStore(rootDirectory) {
948
1058
  }
949
1059
  //#endregion
950
1060
  //#region ../runner/src/local-report-reader.ts
951
- /** 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). */
952
1062
  async function decode(file, schema) {
953
1063
  return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
954
1064
  }
955
1065
  const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
956
1066
  const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
957
- const readTrialManifest = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "manifest.json"), TrialManifestSchema);
958
- const readTrialSummary = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "summary.json"), TrialSummarySchema);
959
- const readTrialScoring = (root, runId, trialId) => decode(join(root, runId, "trials", trialId, "scoring.json"), TrialScoringSchema);
960
- async function readTrialEvents(root, runId, trialId) {
961
- 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"));
962
1070
  }
963
1071
  /**
964
1072
  * True when a run's owning process is known to be gone: it ran on this host
@@ -985,6 +1093,7 @@ function runMatrix(matrix, options) {
985
1093
  const iterator = matrix.cells(options.selection);
986
1094
  const summary = {
987
1095
  cells: 0,
1096
+ runs: 0,
988
1097
  passed: 0,
989
1098
  failed: 0,
990
1099
  cancelled: 0
@@ -1000,7 +1109,7 @@ function runMatrix(matrix, options) {
1000
1109
  "sandbox"
1001
1110
  ].find((name) => agentRuntimes(cell.eval.agent)[name]);
1002
1111
  const suiteId = options.suiteId ?? options.suiteFor?.(cell);
1003
- const result = yield* runEval(cell.eval, {
1112
+ const runs = yield* runEval(cell.eval, {
1004
1113
  ...options,
1005
1114
  ...suiteId ? { suiteId } : {},
1006
1115
  concurrency: 1,
@@ -1012,18 +1121,232 @@ function runMatrix(matrix, options) {
1012
1121
  }
1013
1122
  });
1014
1123
  summary.cells++;
1015
- if (result.status === "cancelled") summary.cancelled++;
1016
- else if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
1017
- else summary.failed++;
1018
- 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));
1019
1131
  }
1020
1132
  })), { concurrency });
1021
1133
  return summary;
1022
1134
  });
1023
1135
  }
1024
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
1025
1344
  //#region ../runner/src/index.ts
1026
- /** 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
+ */
1027
1350
  function runEval(definition, options) {
1028
1351
  return Effect.gen(function* () {
1029
1352
  yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
@@ -1031,19 +1354,19 @@ function runEval(definition, options) {
1031
1354
  ...options.runId ? { runId: options.runId } : {},
1032
1355
  ...options.suiteId ? { suiteId: options.suiteId } : {}
1033
1356
  }));
1034
- const result = yield* Effect.tryPromise({
1035
- try: () => executeRun(definition, options),
1357
+ const runs = yield* Effect.tryPromise({
1358
+ try: () => executeBatch(definition, options),
1036
1359
  catch: (error) => error
1037
1360
  });
1038
1361
  yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
1039
1362
  evalId: authoringId(definition),
1040
- runId: result.runId,
1041
- status: result.status
1363
+ runs: runs.length,
1364
+ passed: runs.filter((run) => run.status === "passed").length
1042
1365
  }));
1043
- return result;
1366
+ return runs;
1044
1367
  });
1045
1368
  }
1046
- function createId(_kind) {
1369
+ function createId() {
1047
1370
  return crypto.randomUUID();
1048
1371
  }
1049
1372
  function canonicalId(kind, id) {
@@ -1052,9 +1375,9 @@ function canonicalId(kind, id) {
1052
1375
  function assertUuid(value, label) {
1053
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`);
1054
1377
  }
1055
- function runMetadata(definition, options, runId, startedAt) {
1378
+ function runMetadata(definition, options, runId, startedAt, extra) {
1056
1379
  return {
1057
- schemaVersion: 5,
1380
+ schemaVersion: 6,
1058
1381
  runId,
1059
1382
  runUri: canonicalId("run", runId),
1060
1383
  evalId: definition.id,
@@ -1067,7 +1390,9 @@ function runMetadata(definition, options, runId, startedAt) {
1067
1390
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1068
1391
  parameters: options.parameters,
1069
1392
  matrix: options.matrix,
1070
- startedAt
1393
+ startedAt,
1394
+ ...options.batch ? { batch: options.batch } : {},
1395
+ ...extra
1071
1396
  };
1072
1397
  }
1073
1398
  /** Recording a secondary error must never mask the original failure or block cleanup. */
@@ -1080,104 +1405,52 @@ async function tryEmitError(error, emitRunner, now) {
1080
1405
  });
1081
1406
  } catch {}
1082
1407
  }
1083
- /** Executes one aggregate eval run and its requested isolated trials. */
1084
- 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) {
1085
1410
  let hasUser = false;
1086
1411
  for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
1087
1412
  else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
1088
1413
  if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
1414
+ assertNoRemovedOptions(definition);
1089
1415
  if (options.runId) assertUuid(options.runId, "runId");
1090
- if (options.trialId) assertUuid(options.trialId, "trialId");
1091
- const requestedTrials = options.trials ?? definition.policy?.trials ?? 1;
1092
- if (!Number.isInteger(requestedTrials) || requestedTrials < 1) throw new Error(`Trial count must be a positive integer; received ${requestedTrials}`);
1093
- if (requestedTrials === 1) return runTrial(definition, options);
1094
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
1095
- const aggregateStartedAt = now();
1096
- const runId = options.runId ?? createId("run");
1097
- 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();
1098
1421
  const signal = options.signal;
1099
- const trialEffects = Array.from({ length: requestedTrials }, (_, trialIndex) => {
1100
- const trial = Effect.tryPromise({
1101
- 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, {
1102
1425
  ...options,
1103
- runId,
1104
- trialId: createId("trial"),
1105
- trials: 1,
1106
- runWriter,
1107
- trialIndex
1426
+ batch: {
1427
+ id: batchId,
1428
+ index,
1429
+ size: repeat
1430
+ }
1108
1431
  }),
1109
1432
  catch: (error) => error
1110
1433
  });
1111
- return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
1112
- });
1113
- const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
1114
- const counts = trialCounts(trials);
1115
- const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
1116
- const endedAt = now();
1117
- await runWriter.finalize({
1118
- status,
1119
- endedAt: endedAt.toISOString(),
1120
- durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
1121
- trialCount: trials.length,
1122
- passed: counts.passed,
1123
- failed: counts.failed,
1124
- ...counts.cancelled ? { cancelled: counts.cancelled } : {}
1434
+ return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
1125
1435
  });
1126
- if (!trials.length) return {
1127
- runId,
1128
- runUri: canonicalId("run", runId),
1129
- trialId: "",
1130
- trialUri: canonicalId("trial", runId),
1131
- trialIndex: 0,
1132
- evalId: authoringId(definition),
1133
- status,
1134
- reportLocation: runWriter.location,
1135
- trialCount: 0,
1136
- passed: 0,
1137
- failed: 0,
1138
- aggregateScoring: {
1139
- passed: 0,
1140
- failed: 0,
1141
- passRate: 0
1142
- },
1143
- trials: []
1144
- };
1145
- const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
1146
- return {
1147
- ...trials[0],
1148
- runId,
1149
- evalId: authoringId(definition),
1150
- status,
1151
- trialCount: trials.length,
1152
- passed: counts.passed,
1153
- failed: counts.failed,
1154
- aggregateScoring: {
1155
- passed: counts.passed,
1156
- failed: counts.failed,
1157
- ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1158
- passRate: counts.passed / trials.length,
1159
- ...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
1160
- },
1161
- trials
1162
- };
1436
+ return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
1163
1437
  }
1164
- async function runTrial(definition, options) {
1438
+ /** One execution: a session in its own sandbox, scored and reported. */
1439
+ async function runOnce(definition, options) {
1165
1440
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
1166
- const runId = options.runId ?? createId("run");
1167
- const trialId = options.trialId ?? createId("trial");
1441
+ const runId = options.runId ?? createId();
1168
1442
  const startedAt = now().toISOString();
1169
1443
  const selectedRuntime = options.runtime;
1170
1444
  const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
1171
1445
  if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
1172
1446
  const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
1173
- const trialIndex = options.trialIndex ?? 0;
1447
+ const batch = options.batch;
1174
1448
  const signal = options.signal ?? new AbortController().signal;
1175
1449
  const root = options.root ?? process.cwd();
1176
1450
  const baseContext = {
1177
1451
  runId,
1178
1452
  evalId: authoringId(definition),
1179
- trialId,
1180
- trialIndex,
1453
+ ...batch ? { batch } : {},
1181
1454
  metadata: {
1182
1455
  ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
1183
1456
  randomSeed
@@ -1191,8 +1464,7 @@ async function runTrial(definition, options) {
1191
1464
  prepared = await prepareDatasets(definition.datasets ?? [], root, {
1192
1465
  evalId: baseContext.evalId,
1193
1466
  runId,
1194
- trialId,
1195
- trialIndex,
1467
+ ...batch ? { batch } : {},
1196
1468
  seed: randomSeed,
1197
1469
  parameters: options.parameters ?? {},
1198
1470
  signal
@@ -1202,40 +1474,30 @@ async function runTrial(definition, options) {
1202
1474
  }
1203
1475
  const candidateData = datasetValues(prepared, "candidate");
1204
1476
  const scoringData = datasetValues(prepared, "all");
1477
+ const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
1205
1478
  const events = [];
1206
- const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt), { catalog: options.catalog });
1207
- const trialWriter = await runWriter.startTrial({
1208
- schemaVersion: 5,
1209
- runId,
1210
- runUri: canonicalId("run", runId),
1211
- trialId,
1212
- trialUri: canonicalId("trial", trialId),
1213
- trialIndex: options.trialIndex ?? 0,
1214
- evalId: definition.id,
1215
- ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1216
- parameters: options.parameters,
1217
- matrix: options.matrix,
1218
- startedAt,
1219
- ...prepared.length ? { datasets: trialDatasets(prepared) } : {}
1220
- });
1479
+ const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
1480
+ workspaceIgnore,
1481
+ ...prepared.length ? { datasets: runDatasets(prepared) } : {}
1482
+ }), { catalog: options.catalog });
1483
+ const attachments = attachmentStore(runWriter, options.attachmentLimits);
1221
1484
  let writeChain = Promise.resolve();
1222
1485
  let reportingFailed = false;
1223
- const trialRef = {
1486
+ const runRef = {
1224
1487
  runId,
1225
- trialId,
1226
- trialIndex,
1488
+ ...batch ? { batch } : {},
1227
1489
  evalId: definition.id,
1228
1490
  ...options.suiteId ? { suiteId: options.suiteId } : {},
1229
1491
  ...options.parameters ? { parameters: options.parameters } : {}
1230
1492
  };
1231
1493
  const observe = (event) => {
1232
1494
  try {
1233
- options.onEvent?.(trialRef, event);
1495
+ options.onEvent?.(runRef, event);
1234
1496
  } catch {}
1235
1497
  };
1236
1498
  const append = async (event) => {
1237
1499
  events.push(event);
1238
- writeChain = writeChain.then(() => trialWriter.appendEvent(event));
1500
+ writeChain = writeChain.then(() => runWriter.appendEvent(event));
1239
1501
  try {
1240
1502
  await writeChain;
1241
1503
  observe(event);
@@ -1271,7 +1533,7 @@ async function runTrial(definition, options) {
1271
1533
  const workspaceScope = await Effect.runPromise(Scope.make());
1272
1534
  try {
1273
1535
  await emitRunner({
1274
- kind: "trial-started",
1536
+ kind: "run-started",
1275
1537
  timestamp: now().toISOString()
1276
1538
  });
1277
1539
  if (signal.aborted) throw cancelled();
@@ -1280,7 +1542,7 @@ async function runTrial(definition, options) {
1280
1542
  message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1281
1543
  });
1282
1544
  workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
1283
- try: () => createTrialWorkspace(trialId, prepared, options.workspaceRoot),
1545
+ try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
1284
1546
  catch: (cause) => new DatasetError({
1285
1547
  cause,
1286
1548
  message: cause instanceof Error ? cause.message : String(cause)
@@ -1293,20 +1555,21 @@ async function runTrial(definition, options) {
1293
1555
  })
1294
1556
  }).pipe(Effect.catchAll((error) => Effect.sync(() => {
1295
1557
  cleanupError = error;
1296
- }).pipe(Effect.tap(() => Effect.logError("trial workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
1297
- const trialWorkspace = workspace;
1298
- 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 = {
1299
1561
  ...baseContext,
1300
1562
  data: candidateData,
1301
- workspace: trialWorkspace.artifacts.candidate,
1302
- evaluatorWorkspace: trialWorkspace.artifacts.evaluator
1563
+ workspace: runWorkspace.artifacts.candidate,
1564
+ evaluatorWorkspace: runWorkspace.artifacts.evaluator
1303
1565
  };
1304
- context = trialContext;
1566
+ context = runContext;
1305
1567
  await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
1306
1568
  try: () => definition.agent.start({
1307
- context: trialContext,
1569
+ context: runContext,
1308
1570
  ...runtime ? { runtime } : {},
1309
- onEvent: onAutEvent
1571
+ onEvent: onAutEvent,
1572
+ attach: attachments.attach
1310
1573
  }),
1311
1574
  catch: (cause) => new AutExecutionError({
1312
1575
  cause,
@@ -1314,8 +1577,8 @@ async function runTrial(definition, options) {
1314
1577
  })
1315
1578
  }), (session) => Effect.tryPromise({
1316
1579
  try: () => executeTranscript(definition, session, {
1317
- context: trialContext,
1318
- artifacts: trialWorkspace.artifacts,
1580
+ context: runContext,
1581
+ artifacts: runWorkspace.artifacts,
1319
1582
  events,
1320
1583
  now,
1321
1584
  randomSeed,
@@ -1323,7 +1586,8 @@ async function runTrial(definition, options) {
1323
1586
  emitAut,
1324
1587
  data: scoringData,
1325
1588
  signal,
1326
- progress
1589
+ progress,
1590
+ attach: attachments.attach
1327
1591
  }),
1328
1592
  catch: (cause) => cause
1329
1593
  }), (opened) => Effect.tryPromise({
@@ -1336,7 +1600,6 @@ async function runTrial(definition, options) {
1336
1600
  closeError = error;
1337
1601
  }).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
1338
1602
  runId,
1339
- trialId,
1340
1603
  evalId: definition.id
1341
1604
  })));
1342
1605
  } catch (error) {
@@ -1366,6 +1629,7 @@ async function runTrial(definition, options) {
1366
1629
  skippedScorers,
1367
1630
  data: scoringData,
1368
1631
  checkpoints,
1632
+ attach: attachments.attach,
1369
1633
  now,
1370
1634
  emitRunner
1371
1635
  });
@@ -1376,12 +1640,14 @@ async function runTrial(definition, options) {
1376
1640
  events,
1377
1641
  lastTurn,
1378
1642
  data: scoringData,
1379
- checkpoints
1643
+ checkpoints,
1644
+ transcript: definition.transcript,
1645
+ attach: attachments.attach
1380
1646
  })));
1381
1647
  scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1382
- await trialWriter.writeScores(scoring);
1648
+ await runWriter.writeScores(scoring);
1383
1649
  if (workspace) try {
1384
- artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter, options.artifactExclude);
1650
+ artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
1385
1651
  } catch (captureError) {
1386
1652
  if (!primaryError) primaryError = captureError;
1387
1653
  await tryEmitError(captureError, emitRunner, now);
@@ -1399,49 +1665,34 @@ async function runTrial(definition, options) {
1399
1665
  scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1400
1666
  if (!primaryError) try {
1401
1667
  await emitRunner({
1402
- kind: "trial-completed",
1668
+ kind: "run-completed",
1403
1669
  timestamp: now().toISOString()
1404
1670
  });
1405
1671
  } catch (error) {
1406
1672
  primaryError = error;
1407
1673
  }
1408
- status = wasCancelled ? "cancelled" : primaryError ? "failed" : "completed";
1674
+ status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
1409
1675
  const error = primaryError ? recordError(primaryError) : void 0;
1676
+ const attached = await attachments.close();
1410
1677
  const endedAt = now().toISOString();
1411
- await trialWriter.finalize({
1678
+ const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
1679
+ await runWriter.finalize({
1412
1680
  status,
1413
1681
  endedAt,
1414
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1682
+ durationMs,
1415
1683
  scoring,
1416
1684
  ...artifacts ? { artifacts } : {},
1685
+ ...attached.length ? { attachments: attached } : {},
1417
1686
  ...error ? { error } : {}
1418
1687
  });
1419
- if (!options.runWriter) {
1420
- const counts = trialCounts([{
1421
- status,
1422
- scoring
1423
- }]);
1424
- await runWriter.finalize({
1425
- status,
1426
- endedAt,
1427
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1428
- trialCount: 1,
1429
- passed: counts.passed,
1430
- failed: counts.failed,
1431
- ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1432
- ...error ? { error } : {}
1433
- });
1434
- }
1435
1688
  return {
1436
1689
  runId,
1437
1690
  runUri: canonicalId("run", runId),
1438
- trialId,
1439
- trialUri: canonicalId("trial", trialId),
1440
- trialIndex: options.trialIndex ?? 0,
1441
1691
  evalId: definition.id,
1442
1692
  status,
1693
+ ...batch ? { batch } : {},
1443
1694
  reportLocation: runWriter.location,
1444
- durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1695
+ durationMs,
1445
1696
  scoring,
1446
1697
  ...error ? { error } : {}
1447
1698
  };
@@ -1455,23 +1706,9 @@ function datasetValues(prepared, audience) {
1455
1706
  }
1456
1707
  return values;
1457
1708
  }
1458
- /** A cancelled trial is neither passed nor failed. */
1459
- function trialCounts(trials) {
1460
- let passed = 0;
1461
- let failed = 0;
1462
- let cancelledTrials = 0;
1463
- for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
1464
- else if (trial.status === "completed" && trial.scoring?.passed) passed++;
1465
- else failed++;
1466
- return {
1467
- passed,
1468
- failed,
1469
- cancelled: cancelledTrials
1470
- };
1471
- }
1472
- /** 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. */
1473
1710
  function keepWorkspace(policy = "failed", passed) {
1474
1711
  return policy === "all" || policy === "failed" && !passed;
1475
1712
  }
1476
1713
  //#endregion
1477
- 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 };