@leostera/evalkit 0.5.0 → 0.6.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 (45) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +10 -8
  3. package/dist/cli.mjs +1227 -258
  4. package/dist/dashboard/assets/index-BK9x-zoi.css +2 -0
  5. package/dist/dashboard/assets/index-DzsDLGHx.js +32 -0
  6. package/dist/dashboard/index.html +2 -2
  7. package/dist/files-C-U0-PWW.mjs +0 -2
  8. package/dist/index-db-vbqEGFaU.mjs +0 -2
  9. package/dist/index.d.mts +1344 -2
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -4
  12. package/dist/runner.d.mts +807 -11
  13. package/dist/runner.d.mts.map +1 -1
  14. package/dist/runner.mjs +2 -2
  15. package/dist/share-worker.d.mts +76 -1
  16. package/dist/share-worker.d.mts.map +1 -1
  17. package/dist/share-worker.mjs +0 -2
  18. package/dist/share.d.mts +149 -6
  19. package/dist/share.d.mts.map +1 -1
  20. package/dist/share.mjs +1 -1
  21. package/dist/skills/evalkit/SKILL.md +32 -0
  22. package/dist/skills/evalkit-agents/SKILL.md +16 -0
  23. package/dist/skills/evalkit-datasets/SKILL.md +17 -0
  24. package/dist/skills/evalkit-evals/SKILL.md +19 -0
  25. package/dist/skills/evalkit-judges/SKILL.md +17 -0
  26. package/dist/skills/evalkit-matrices/SKILL.md +14 -0
  27. package/dist/skills/evalkit-results/SKILL.md +14 -0
  28. package/dist/{src-DoPs9Yug.mjs → src-CVSCn3bZ.mjs} +445 -115
  29. package/dist/{src-DzZueFn3.mjs → src-CZnDXP1K.mjs} +46 -10
  30. package/dist/{src-ButXPY8P.mjs → src-D4tCwxNb.mjs} +418 -94
  31. package/package.json +3 -2
  32. package/dist/cli.mjs.map +0 -1
  33. package/dist/dashboard/assets/index-D9NtA46z.css +0 -2
  34. package/dist/dashboard/assets/index-DrBwLKJD.js +0 -30
  35. package/dist/files-C-U0-PWW.mjs.map +0 -1
  36. package/dist/index-db-HdZXelOS.d.mts +0 -142
  37. package/dist/index-db-HdZXelOS.d.mts.map +0 -1
  38. package/dist/index-db-vbqEGFaU.mjs.map +0 -1
  39. package/dist/index-lOw0fmak.d.mts +0 -1028
  40. package/dist/index-lOw0fmak.d.mts.map +0 -1
  41. package/dist/index.mjs.map +0 -1
  42. package/dist/share-worker.mjs.map +0 -1
  43. package/dist/src-ButXPY8P.mjs.map +0 -1
  44. package/dist/src-DoPs9Yug.mjs.map +0 -1
  45. package/dist/src-DzZueFn3.mjs.map +0 -1
@@ -1,17 +1,36 @@
1
- import { A as TrajectoryEventSchema, L as canonicalParameters, M as TrialMetadataSchema, N as TrialScoringSchema, O as RunSummarySchema, P as TrialSummarySchema, T as RunMetadataSchema, U as resourceUri, b as CheckpointResultSchema, g as parseTrajectoryJsonl, j as TrialManifestSchema, k as ScoreResultSchema, n as authoringId, p as recordError, w as RunManifestSchema, x as JsonValueSchema } from "./src-ButXPY8P.mjs";
1
+ import { A as TrialManifestSchema, C as RunManifestSchema, D as RunSummarySchema, F as normalizeAutEvent, G as relativeDatasetPath, H as toJsonObject, I as turnResultEvents, M as TrialScoringSchema, N as TrialSummarySchema, O as ScoreResultSchema, Q as resourceUri, R as canonicalParameters, V as toJson, b as JsonValueSchema, d as recordError, h as parseTrajectoryJsonl, i as authoringId, j as TrialMetadataSchema, k as TrajectoryEventSchema, r as agentRuntimes, w as RunMetadataSchema, y as CheckpointResultSchema } from "./src-D4tCwxNb.mjs";
2
2
  import * as Schema from "effect/Schema";
3
3
  import { Data, Effect, Either, Exit, Scope } from "effect";
4
- import { appendFile, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
5
- import path, { join } from "node:path";
6
4
  import os from "node:os";
7
- import { randomUUID } from "node:crypto";
5
+ import { appendFile, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import path, { join } from "node:path";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  //#region ../runner/src/errors.ts
9
9
  var AutExecutionError = class extends Data.TaggedError("AutExecutionError") {};
10
- var FixtureError = class extends Data.TaggedError("FixtureError") {};
10
+ var DatasetError = class extends Data.TaggedError("DatasetError") {};
11
11
  var ReportError = class extends Data.TaggedError("ReportError") {};
12
12
  var ScoringError = class extends Data.TaggedError("ScoringError") {};
13
13
  var RuleExecutionError = class extends Data.TaggedError("RuleExecutionError") {};
14
14
  var CheckpointExecutionError = class extends Data.TaggedError("CheckpointExecutionError") {};
15
+ /** The run was cancelled (Ctrl+C); the trial stops and is finalized as cancelled. */
16
+ var CancelledError = class extends Data.TaggedError("CancelledError") {};
17
+ function cancelled() {
18
+ return new CancelledError({ message: "Cancelled" });
19
+ }
20
+ /** Settles like `work`, or rejects with `CancelledError` as soon as `signal` aborts. */
21
+ async function unlessAborted(work, signal) {
22
+ if (signal.aborted) throw cancelled();
23
+ let onAbort;
24
+ const aborted = new Promise((_, reject) => {
25
+ onAbort = () => reject(cancelled());
26
+ signal.addEventListener("abort", onAbort, { once: true });
27
+ });
28
+ try {
29
+ return await Promise.race([work(), aborted]);
30
+ } finally {
31
+ signal.removeEventListener("abort", onAbort);
32
+ }
33
+ }
15
34
  //#endregion
16
35
  //#region ../runner/src/snapshot.ts
17
36
  /**
@@ -51,46 +70,193 @@ async function snapshotCandidateWorkspace(candidateRoot, writer) {
51
70
  return artifacts;
52
71
  }
53
72
  //#endregion
54
- //#region ../runner/src/workspace.ts
55
- const MAX_DYNAMIC_FIXTURE_DEPTH = 16;
56
- function workspaceRoot(visibility, artifacts) {
57
- return visibility === "candidate" ? artifacts.candidate.root : artifacts.evaluator.root;
58
- }
59
- function destinationPath(root, destination) {
60
- if (!destination || path.isAbsolute(destination)) throw new Error(`Fixture destination must be a non-empty relative path: ${destination}`);
61
- const normalized = path.normalize(destination);
62
- if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Fixture destination escapes its workspace: ${destination}`);
63
- return path.join(root, normalized);
73
+ //#region ../runner/src/datasets.ts
74
+ const loads = /* @__PURE__ */ new WeakMap();
75
+ /**
76
+ * Loads a dataset at most once per process and project root. A failed load is
77
+ * forgotten, so the next trial that needs the dataset tries again.
78
+ */
79
+ function loadDataset(dataset, root, signal) {
80
+ let byRoot = loads.get(dataset);
81
+ if (!byRoot) loads.set(dataset, byRoot = /* @__PURE__ */ new Map());
82
+ const cached = byRoot.get(root);
83
+ if (cached) return cached;
84
+ const loading = loadUncached(dataset, root, signal);
85
+ byRoot.set(root, loading);
86
+ loading.catch(() => {
87
+ if (byRoot.get(root) === loading) byRoot.delete(root);
88
+ });
89
+ return loading;
64
90
  }
65
- async function resolveFixtures(fixtures, context, depth = 0) {
66
- if (depth > MAX_DYNAMIC_FIXTURE_DEPTH) throw new Error("Dynamic fixture nesting exceeds the supported depth");
67
- const resolved = [];
68
- for (const fixture of fixtures) {
69
- if (fixture.kind !== "dynamic") {
70
- resolved.push(fixture);
71
- continue;
91
+ async function loadUncached(dataset, root, signal) {
92
+ const { source } = dataset;
93
+ if (source.kind === "computed") {
94
+ const loaded = await source.onLoad({
95
+ root,
96
+ signal
97
+ });
98
+ if (typeof loaded !== "object" || loaded === null) throw new Error(`Dataset ${dataset.id} onLoad must return { files?, value? }`);
99
+ return loaded;
100
+ }
101
+ return {
102
+ ...source.files !== void 0 ? { files: source.files } : {},
103
+ ...source.valueFile !== void 0 ? { value: await readValueFile(dataset.id, path.resolve(root, source.valueFile)) } : source.value !== void 0 ? { value: source.value } : {}
104
+ };
105
+ }
106
+ /** `.json` is one value; `.jsonl` is an array of one JSON object per non-blank line. */
107
+ async function readValueFile(id, file) {
108
+ const text = await readFile(file, "utf8").catch((cause) => {
109
+ throw new Error(`Dataset ${id} cannot read ${file}: ${String(cause)}`);
110
+ });
111
+ if (file.endsWith(".jsonl")) return text.split("\n").flatMap((line, index) => {
112
+ if (!line.trim()) return [];
113
+ try {
114
+ const row = JSON.parse(line);
115
+ if (typeof row !== "object" || row === null || Array.isArray(row)) throw new Error("expected a JSON object");
116
+ return [row];
117
+ } catch (cause) {
118
+ throw new Error(`Dataset ${id} ${file}:${index + 1}: ${cause instanceof Error ? cause.message : String(cause)}`);
72
119
  }
73
- const created = await fixture.create(context);
74
- const nested = Array.isArray(created) ? created : [created];
75
- resolved.push(...await resolveFixtures(nested, context, depth + 1));
120
+ });
121
+ if (file.endsWith(".json")) try {
122
+ return JSON.parse(text);
123
+ } catch (cause) {
124
+ throw new Error(`Dataset ${id} ${file}: ${cause instanceof Error ? cause.message : String(cause)}`);
125
+ }
126
+ throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
127
+ }
128
+ /** Loads (cached), derives this trial's content with `onRun`, and resolves it. */
129
+ async function prepareDatasets(datasets, root, trial) {
130
+ const prepared = [];
131
+ for (const dataset of datasets) {
132
+ const loaded = await loadDataset(dataset, root, trial.signal);
133
+ const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, trial) : loaded;
134
+ if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
135
+ prepared.push(await resolveContent(dataset, content, root));
136
+ }
137
+ return prepared;
138
+ }
139
+ async function resolveContent(dataset, content, root) {
140
+ const label = `Dataset ${dataset.id}`;
141
+ const value = content.value === void 0 ? void 0 : toJson(content.value, `datasets.${dataset.id}.value`);
142
+ let files = { kind: "none" };
143
+ if (typeof content.files === "string") {
144
+ const source = path.resolve(root, content.files);
145
+ if (!(await stat(source).catch(() => void 0))?.isDirectory()) throw new Error(`${label} files directory does not exist: ${content.files}`);
146
+ files = {
147
+ kind: "directory",
148
+ source
149
+ };
150
+ } else if (content.files !== void 0) files = {
151
+ kind: "entries",
152
+ entries: Object.entries(content.files).map(([file, source]) => preparedFile(relativeDatasetPath(file, `${label} file`), source, root, label))
153
+ };
154
+ return {
155
+ dataset,
156
+ files,
157
+ ...value === void 0 ? {} : { value },
158
+ digest: await digest(files, value)
159
+ };
160
+ }
161
+ function preparedFile(file, source, root, label) {
162
+ if (typeof source === "string") return {
163
+ path: file,
164
+ kind: "bytes",
165
+ data: new TextEncoder().encode(source)
166
+ };
167
+ if (source instanceof Uint8Array) return {
168
+ path: file,
169
+ kind: "bytes",
170
+ data: source
171
+ };
172
+ if (typeof source === "object" && source !== null && typeof source.file === "string") return {
173
+ path: file,
174
+ kind: "file",
175
+ source: path.resolve(root, source.file)
176
+ };
177
+ throw new Error(`${label} file ${file} must be a string, bytes, or { file: 'path' }`);
178
+ }
179
+ /** Lists a directory's files as sorted relative POSIX paths. */
180
+ async function listFiles(directory, prefix = "") {
181
+ const entries = await readdir(path.join(directory, prefix), { withFileTypes: true });
182
+ const files = [];
183
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
184
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
185
+ if (entry.isDirectory()) files.push(...await listFiles(directory, relative));
186
+ else if (entry.isFile()) files.push(relative);
76
187
  }
77
- return resolved;
78
- }
79
- function assertUniqueDestinations(fixtures) {
80
- const destinations = /* @__PURE__ */ new Set();
81
- for (const fixture of fixtures) {
82
- const destination = fixture.kind === "inline" ? fixture.file : fixture.dst;
83
- const key = `${fixture.visibility}:${destination}`;
84
- if (destinations.has(key)) throw new Error(`Duplicate ${fixture.visibility} fixture destination: ${destination}`);
85
- destinations.add(key);
188
+ return files;
189
+ }
190
+ async function digest(files, value) {
191
+ const hash = createHash("sha256");
192
+ const addFile = (file, data) => {
193
+ hash.update(`file ${file} ${data.byteLength}\n`);
194
+ hash.update(data);
195
+ };
196
+ if (files.kind === "directory") for (const file of await listFiles(files.source)) addFile(file, await readFile(path.join(files.source, file)));
197
+ else if (files.kind === "entries") for (const entry of [...files.entries].sort((a, b) => a.path.localeCompare(b.path))) addFile(entry.path, entry.kind === "bytes" ? entry.data : await readFile(entry.source));
198
+ if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
199
+ return `sha256:${hash.digest("hex")}`;
200
+ }
201
+ function trialDatasets(prepared) {
202
+ return prepared.map(({ dataset, digest }) => ({
203
+ id: dataset.id,
204
+ visibility: dataset.visibility,
205
+ source: dataset.source.kind,
206
+ digest
207
+ }));
208
+ }
209
+ /**
210
+ * The files a static dataset places in every workspace, relative to its
211
+ * mount. Computed datasets return `undefined`: their files only exist once
212
+ * `onLoad` runs, and browsing must never run project code.
213
+ */
214
+ async function staticDatasetFiles(dataset, root) {
215
+ const { source } = dataset;
216
+ if (source.kind === "computed") return void 0;
217
+ if (source.files === void 0) return [];
218
+ if (typeof source.files === "string") {
219
+ const directory = path.resolve(root, source.files);
220
+ return Promise.all((await listFiles(directory)).map(async (file) => ({
221
+ path: file,
222
+ size: (await stat(path.join(directory, file))).size
223
+ })));
86
224
  }
225
+ return Promise.all(Object.entries(source.files).sort(([a], [b]) => a.localeCompare(b)).map(async ([file, content]) => ({
226
+ path: file,
227
+ size: typeof content === "string" ? new TextEncoder().encode(content).byteLength : content instanceof Uint8Array ? content.byteLength : (await stat(path.resolve(root, content.file))).size
228
+ })));
87
229
  }
88
- /** Creates isolated candidate and evaluator directories and materializes fixtures into each. */
230
+ /** One file of a static dataset, by its path below the mount. */
231
+ async function staticDatasetFile(dataset, root, file) {
232
+ const { source } = dataset;
233
+ const missing = () => /* @__PURE__ */ new Error(`Dataset ${dataset.id} has no file ${JSON.stringify(file)}`);
234
+ if (source.kind === "computed" || source.files === void 0) throw missing();
235
+ if (typeof source.files === "string") {
236
+ const directory = path.resolve(root, source.files);
237
+ const resolved = path.resolve(directory, file);
238
+ if (!resolved.startsWith(`${directory}${path.sep}`)) throw missing();
239
+ if (!(await stat(resolved).catch(() => void 0))?.isFile()) throw missing();
240
+ return readFile(resolved);
241
+ }
242
+ if (!Object.hasOwn(source.files, file)) throw missing();
243
+ const content = source.files[file];
244
+ if (typeof content === "string") return new TextEncoder().encode(content);
245
+ if (content instanceof Uint8Array) return new Uint8Array(content);
246
+ return readFile(path.resolve(root, content.file));
247
+ }
248
+ //#endregion
249
+ //#region ../runner/src/workspace.ts
89
250
  function trialDirectoryName(trialId) {
90
251
  return trialId.replace(/[^a-zA-Z0-9._-]/g, "_");
91
252
  }
92
- async function createTrialWorkspace(context, fixtures = [], persistentRoot) {
93
- const root = persistentRoot ? path.join(path.resolve(persistentRoot), trialDirectoryName(context.trialId)) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
253
+ /**
254
+ * Creates isolated candidate and evaluator directories and writes each
255
+ * dataset's files below its mount, on the side its visibility allows.
256
+ * Two datasets writing the same file is an error, never an overwrite.
257
+ */
258
+ async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
259
+ const root = persistentRoot ? path.join(path.resolve(persistentRoot), trialDirectoryName(trialId)) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
94
260
  const persistent = persistentRoot !== void 0;
95
261
  const artifacts = {
96
262
  candidate: { root: path.join(root, "candidate") },
@@ -98,18 +264,9 @@ async function createTrialWorkspace(context, fixtures = [], persistentRoot) {
98
264
  };
99
265
  try {
100
266
  await Promise.all([mkdir(artifacts.candidate.root, { recursive: true }), mkdir(artifacts.evaluator.root, { recursive: true })]);
101
- const resolved = await resolveFixtures(fixtures, context);
102
- assertUniqueDestinations(resolved);
103
- for (const fixture of resolved) {
104
- const target = destinationPath(workspaceRoot(fixture.visibility, artifacts), fixture.kind === "inline" ? fixture.file : fixture.dst);
105
- await mkdir(path.dirname(target), { recursive: true });
106
- if (fixture.kind === "directory") await cp(fixture.src, target, {
107
- recursive: true,
108
- force: false,
109
- errorOnExist: true
110
- });
111
- else if (fixture.kind === "file") await copyFile(fixture.src, target);
112
- else await writeFile(target, fixture.data);
267
+ for (const prepared of datasets) {
268
+ const side = prepared.dataset.visibility === "candidate" ? artifacts.candidate.root : artifacts.evaluator.root;
269
+ await writeDatasetFiles(prepared, path.join(side, prepared.dataset.mount));
113
270
  }
114
271
  return {
115
272
  artifacts,
@@ -126,21 +283,50 @@ async function createTrialWorkspace(context, fixtures = [], persistentRoot) {
126
283
  throw error;
127
284
  }
128
285
  }
286
+ async function writeDatasetFiles(prepared, target) {
287
+ const { files, dataset } = prepared;
288
+ if (files.kind === "none") return;
289
+ await mkdir(target, { recursive: true });
290
+ const entries = files.kind === "directory" ? (await listFiles(files.source)).map((file) => ({
291
+ path: file,
292
+ kind: "file",
293
+ source: path.join(files.source, file)
294
+ })) : files.entries;
295
+ for (const entry of entries) await writeEntry(dataset.id, target, entry);
296
+ }
297
+ async function writeEntry(datasetId, target, entry) {
298
+ const destination = path.join(target, ...entry.path.split("/"));
299
+ await mkdir(path.dirname(destination), { recursive: true });
300
+ await (entry.kind === "bytes" ? writeFile(destination, entry.data, { flag: "wx" }) : copyFile(entry.source, destination, 1)).catch((cause) => {
301
+ throw collision(datasetId, entry.path, cause);
302
+ });
303
+ }
304
+ function collision(datasetId, file, cause) {
305
+ 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));
306
+ }
129
307
  //#endregion
130
308
  //#region ../runner/src/score.ts
131
- /** Shared normalization for inline and final predicate and judge rules. */
132
- function normalizeScore(value) {
309
+ /**
310
+ * Shared normalization for inline and final predicate and judge rules.
311
+ * `passAt` turns a numeric value into a verdict; an explicit `passed` wins.
312
+ */
313
+ function normalizeScore(value, passAt) {
314
+ if (typeof value === "object" && value !== null && "skipped" in value && value.skipped === true) return {
315
+ skipped: true,
316
+ explanation: value.reason
317
+ };
133
318
  const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
134
319
  if (!normalized || !Number.isFinite(normalized.value) || normalized.value < 0 || normalized.value > 1) throw new Error(`Score value must be a finite number from 0 to 1; received ${normalized?.value}`);
135
320
  if ("judge" in normalized && normalized.judge) {
136
321
  const { usage } = normalized.judge;
137
322
  if (usage && Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
138
323
  }
324
+ const passed = "passed" in normalized && normalized.passed !== void 0 ? normalized.passed : normalized.value >= (passAt ?? 1);
139
325
  return {
140
326
  value: normalized.value,
141
- passed: normalized.passed ?? normalized.value === 1,
142
- ...normalized.explanation ? { explanation: normalized.explanation } : {},
143
- ...normalized.evidence === void 0 ? {} : { evidence: normalized.evidence },
327
+ passed,
328
+ ..."explanation" in normalized && normalized.explanation ? { explanation: normalized.explanation } : {},
329
+ ..."evidence" in normalized && normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
144
330
  ..."judge" in normalized && normalized.judge !== void 0 ? { judge: normalized.judge } : {}
145
331
  };
146
332
  }
@@ -156,6 +342,7 @@ function runJudgeAgent(rule, agent, input, placement) {
156
342
  const events = [];
157
343
  const context = {
158
344
  ...input.context,
345
+ data: input.data,
159
346
  workspace: input.artifacts.evaluator,
160
347
  evaluatorWorkspace: input.artifacts.evaluator
161
348
  };
@@ -173,7 +360,7 @@ function runJudgeAgent(rule, agent, input, placement) {
173
360
  context,
174
361
  onEvent: (event) => {
175
362
  const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
176
- ...event,
363
+ ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
177
364
  source: "aut"
178
365
  });
179
366
  if (observed.source !== "aut") throw new Error("Invalid judge event source");
@@ -183,10 +370,13 @@ function runJudgeAgent(rule, agent, input, placement) {
183
370
  }),
184
371
  catch: (cause) => failure$2(rule.name, cause)
185
372
  }), (session) => Effect.tryPromise({
186
- try: () => session.send(prompt),
373
+ try: async () => {
374
+ const result = await session.send(prompt);
375
+ if (result !== void 0) events.push(...turnResultEvents(result, 1, () => /* @__PURE__ */ new Date()));
376
+ },
187
377
  catch: (cause) => failure$2(rule.name, cause)
188
378
  }), (session) => Effect.tryPromise({
189
- try: () => session.close(),
379
+ try: async () => session.close?.(),
190
380
  catch: (cause) => failure$2(rule.name, cause)
191
381
  }).pipe(Effect.catchAll((error) => Effect.sync(() => {
192
382
  closeError = error;
@@ -229,7 +419,7 @@ function evaluateRule(rule, input, judgeAgent, placement) {
229
419
  catch: (cause) => failure$1(rule.name, cause)
230
420
  }) } : yield* runJudgeAgent(rule, judgeAgent, input, placement);
231
421
  const score = yield* Effect.try({
232
- try: () => normalizeScore(evaluated.value),
422
+ try: () => normalizeScore(evaluated.value, rule.kind === "predicate" ? rule.passAt : void 0),
233
423
  catch: (cause) => failure$1(rule.name, cause)
234
424
  });
235
425
  const withJudge = rule.kind === "judge" && "judge" in evaluated ? {
@@ -242,6 +432,10 @@ function evaluateRule(rule, input, judgeAgent, placement) {
242
432
  durationMs: 0,
243
433
  ...withJudge
244
434
  }).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
435
+ if (validated.skipped) return {
436
+ skipped: true,
437
+ ...validated.explanation === void 0 ? {} : { explanation: validated.explanation }
438
+ };
245
439
  return {
246
440
  value: validated.value,
247
441
  passed: validated.passed,
@@ -315,24 +509,26 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
315
509
  try: () => normalizeScore(Boolean(match)),
316
510
  catch: (cause) => failure(index, step.name, cause)
317
511
  }) : yield* evaluateRule(step, input, judgeAgent, "transcript").pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
512
+ const status = score.skipped ? "skipped" : score.passed ? "passed" : "failed";
513
+ const { skipped: _, ...recorded } = score;
318
514
  const result = yield* Schema.decodeUnknown(CheckpointResultSchema)({
319
515
  step: index,
320
516
  kind: step.kind,
321
517
  name: step.name,
322
- status: score.passed ? "passed" : "failed",
518
+ status,
323
519
  durationMs: now().getTime() - started.getTime(),
324
- ...score,
520
+ ...recorded,
325
521
  ...match ? { matchedToolCall: {
326
522
  eventIndex: match.eventIndex,
327
523
  id: match.id
328
524
  } } : {}
329
525
  }).pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
330
- yield* Effect.logInfo("checkpoint completed").pipe(Effect.annotateLogs({ status: result.status }));
526
+ yield* Effect.logDebug("checkpoint completed").pipe(Effect.annotateLogs({ status: result.status }));
331
527
  return {
332
528
  ...result,
333
- status: score.passed ? "passed" : "failed"
529
+ status
334
530
  };
335
- }).pipe(Effect.tapError((error) => Effect.logError("checkpoint execution error").pipe(Effect.annotateLogs({ errorName: error._tag }))), Effect.annotateLogs(annotations));
531
+ }).pipe(Effect.tapError((error) => Effect.logDebug("checkpoint execution error").pipe(Effect.annotateLogs({ errorName: error._tag }))), Effect.annotateLogs(annotations));
336
532
  }
337
533
  //#endregion
338
534
  //#region ../runner/src/effect-boundary.ts
@@ -346,7 +542,9 @@ async function runBoundary(effect) {
346
542
  //#region ../runner/src/transcript.ts
347
543
  async function executeTranscript(definition, session, input) {
348
544
  const { now, emitRunner, progress } = input;
545
+ let turn = 0;
349
546
  for (const [index, step] of definition.transcript.entries()) {
547
+ if (input.signal.aborted) throw cancelled();
350
548
  await emitRunner({
351
549
  kind: "transcript-step-started",
352
550
  step: index,
@@ -354,7 +552,9 @@ async function executeTranscript(definition, session, input) {
354
552
  });
355
553
  if (step.kind === "user") {
356
554
  const cursor = input.events.length;
357
- await sendUserTurn(session, step.message.replaceAll("{{randomSeed}}", String(input.randomSeed)));
555
+ turn++;
556
+ const result = await sendUserTurn(session, step.message.replaceAll("{{randomSeed}}", String(input.randomSeed)), input.signal);
557
+ if (result !== void 0) for (const event of turnResultEvents(result, turn, now)) await input.emitAut(event);
358
558
  progress.lastTurn = turnView(input.events, cursor, input.events.length, index);
359
559
  } else if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") {
360
560
  const result = await runCheckpoint(definition, step, index, input);
@@ -370,7 +570,7 @@ async function executeTranscript(definition, session, input) {
370
570
  step: index,
371
571
  timestamp: now().toISOString()
372
572
  });
373
- if (!result.passed && definition.policy?.failfast) {
573
+ if (result.status === "failed" && definition.policy?.failfast) {
374
574
  progress.stoppedEarly = true;
375
575
  await skipRemainingSteps(definition, index, input);
376
576
  break;
@@ -384,10 +584,10 @@ async function executeTranscript(definition, session, input) {
384
584
  });
385
585
  }
386
586
  }
387
- async function sendUserTurn(session, message) {
388
- await runBoundary(Effect.tryPromise({
389
- try: () => session.send(message),
390
- catch: (cause) => cause instanceof ReportError ? cause : new AutExecutionError({
587
+ async function sendUserTurn(session, message, signal) {
588
+ return runBoundary(Effect.tryPromise({
589
+ try: () => unlessAborted(() => session.send(message), signal),
590
+ catch: (cause) => cause instanceof ReportError || cause instanceof CancelledError ? cause : new AutExecutionError({
391
591
  cause,
392
592
  message: cause instanceof Error ? cause.message : String(cause)
393
593
  })
@@ -407,7 +607,9 @@ async function runCheckpoint(definition, step, index, input) {
407
607
  context: input.context,
408
608
  artifacts: input.artifacts,
409
609
  trajectory: { events: input.events },
410
- turn: progress.lastTurn
610
+ turn: progress.lastTurn,
611
+ ...turnOutput(progress.lastTurn),
612
+ data: input.data
411
613
  }, now, definition.judge)).catch(async (error) => {
412
614
  const recorded = recordError(error);
413
615
  progress.checkpoints.push({
@@ -445,24 +647,29 @@ async function skipRemainingSteps(definition, after, { now, emitRunner, progress
445
647
  });
446
648
  }
447
649
  }
650
+ /** The scorer-facing `output`: the content of the turn's last assistant message. */
651
+ function turnOutput(turn) {
652
+ const last = turn?.assistantMessages.at(-1);
653
+ return last === void 0 ? {} : { output: last.content };
654
+ }
448
655
  //#endregion
449
656
  //#region ../runner/src/final-scoring.ts
450
657
  function scoreSummary(results, checkpoints, skippedScorers) {
451
- const valid = results.filter((result) => result.value !== void 0);
658
+ const valid = results.filter((result) => result.value !== void 0 && !result.skipped);
452
659
  const hasError = results.some((result) => result.error !== void 0);
453
660
  return {
454
661
  results,
455
662
  ...checkpoints.length ? { checkpoints } : {},
456
663
  ...skippedScorers.length ? { skippedScorers } : {},
457
664
  ...valid.length === 0 ? {} : { overall: valid.reduce((sum, result) => sum + (result.value ?? 0), 0) / valid.length },
458
- passed: !hasError && results.every((result) => result.passed === true) && checkpoints.every((result) => result.status === "passed") && !skippedScorers.length
665
+ passed: !hasError && results.every((result) => result.skipped || result.passed === true) && checkpoints.every((result) => result.status === "passed" || result.status === "skipped") && !skippedScorers.length
459
666
  };
460
667
  }
461
668
  /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
462
669
  async function evaluateFinalScorers(definition, input) {
463
670
  const { context, workspace, events, lastTurn, results, skippedScorers, now, emitRunner } = input;
464
671
  for (const scorer of definition.scoring) {
465
- if (input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
672
+ if (input.cancelled || input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
466
673
  skippedScorers.push(scorer.name);
467
674
  continue;
468
675
  }
@@ -478,7 +685,9 @@ async function evaluateFinalScorers(definition, input) {
478
685
  context,
479
686
  artifacts: workspace.artifacts,
480
687
  trajectory: { events },
481
- ...lastTurn ? { turn: lastTurn } : {}
688
+ ...lastTurn ? { turn: lastTurn } : {},
689
+ ...turnOutput(lastTurn),
690
+ data: input.data
482
691
  }, definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
483
692
  cause,
484
693
  message: cause.message
@@ -510,7 +719,7 @@ async function evaluateFinalScorers(definition, input) {
510
719
  await emitRunner({
511
720
  kind: "scorer-completed",
512
721
  scorer: scorer.name,
513
- value: result.value ?? 0,
722
+ ...result.skipped ? { skipped: true } : { value: result.value ?? 0 },
514
723
  timestamp: now().toISOString()
515
724
  });
516
725
  }
@@ -618,6 +827,20 @@ const readTrialScoring = (root, runId, trialId) => decode(join(root, runId, "tri
618
827
  async function readTrialEvents(root, runId, trialId) {
619
828
  return parseTrajectoryJsonl(await readFile(join(root, runId, "trials", trialId, "trajectory.jsonl"), "utf8"));
620
829
  }
830
+ /**
831
+ * True when a run's owning process is known to be gone: it ran on this host
832
+ * and its PID no longer exists. Runs from other hosts are never judged.
833
+ */
834
+ function ownerIsGone(owner) {
835
+ if (!owner || owner.hostname !== os.hostname()) return false;
836
+ if (owner.pid === process.pid) return false;
837
+ try {
838
+ process.kill(owner.pid, 0);
839
+ return false;
840
+ } catch (error) {
841
+ return error.code === "ESRCH";
842
+ }
843
+ }
621
844
  //#endregion
622
845
  //#region ../runner/src/matrix.ts
623
846
  /** Bounded workers pull cells from the iterator; no Cartesian job array is allocated. */
@@ -630,10 +853,11 @@ function runMatrix(matrix, options) {
630
853
  const summary = {
631
854
  cells: 0,
632
855
  passed: 0,
633
- failed: 0
856
+ failed: 0,
857
+ cancelled: 0
634
858
  };
635
859
  yield* Effect.all(Array.from({ length: Math.min(count, concurrency) }, () => Effect.gen(function* () {
636
- while (true) {
860
+ while (!options.signal?.aborted) {
637
861
  const next = iterator.next();
638
862
  if (next.done) return;
639
863
  const cell = next.value;
@@ -641,9 +865,11 @@ function runMatrix(matrix, options) {
641
865
  "local",
642
866
  "remote",
643
867
  "sandbox"
644
- ].find((name) => cell.eval.agent.runtimes?.[name]);
868
+ ].find((name) => agentRuntimes(cell.eval.agent)[name]);
869
+ const suiteId = options.suiteId ?? options.suiteFor?.(cell);
645
870
  const result = yield* runEval(cell.eval, {
646
871
  ...options,
872
+ ...suiteId ? { suiteId } : {},
647
873
  concurrency: 1,
648
874
  runtime,
649
875
  parameters: cell.parameters,
@@ -653,7 +879,8 @@ function runMatrix(matrix, options) {
653
879
  }
654
880
  });
655
881
  summary.cells++;
656
- if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
882
+ if (result.status === "cancelled") summary.cancelled++;
883
+ else if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
657
884
  else summary.failed++;
658
885
  if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, result));
659
886
  }
@@ -694,12 +921,16 @@ function assertUuid(value, label) {
694
921
  }
695
922
  function runMetadata(definition, options, runId, startedAt) {
696
923
  return {
697
- schemaVersion: 4,
924
+ schemaVersion: 5,
698
925
  runId,
699
926
  runUri: canonicalId("run", runId),
700
927
  evalId: definition.id,
701
928
  ...options.suiteId ? { suiteId: options.suiteId } : {},
702
929
  ...options.source ? { source: options.source } : {},
930
+ process: {
931
+ pid: process.pid,
932
+ hostname: os.hostname()
933
+ },
703
934
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
704
935
  parameters: options.parameters,
705
936
  matrix: options.matrix,
@@ -731,9 +962,10 @@ async function executeRun(definition, options) {
731
962
  const aggregateStartedAt = now();
732
963
  const runId = options.runId ?? createId("run");
733
964
  const runWriter = await options.report.startRun(runMetadata(definition, options, runId, aggregateStartedAt.toISOString()), { catalog: options.catalog });
965
+ const signal = options.signal;
734
966
  const trialEffects = Array.from({ length: requestedTrials }, (_, trialIndex) => {
735
967
  const trial = Effect.tryPromise({
736
- try: () => runTrial(definition, {
968
+ try: async () => signal?.aborted ? void 0 : runTrial(definition, {
737
969
  ...options,
738
970
  runId,
739
971
  trialId: createId("trial"),
@@ -745,18 +977,38 @@ async function executeRun(definition, options) {
745
977
  });
746
978
  return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
747
979
  });
748
- const trials = await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }));
749
- const failed = trials.filter((trial) => trial.status !== "completed" || !trial.scoring?.passed).length;
750
- const status = trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
980
+ const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
981
+ const counts = trialCounts(trials);
982
+ const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
751
983
  const endedAt = now();
752
984
  await runWriter.finalize({
753
985
  status,
754
986
  endedAt: endedAt.toISOString(),
755
987
  durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
756
988
  trialCount: trials.length,
757
- passed: trials.length - failed,
758
- failed
989
+ passed: counts.passed,
990
+ failed: counts.failed,
991
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {}
759
992
  });
993
+ if (!trials.length) return {
994
+ runId,
995
+ runUri: canonicalId("run", runId),
996
+ trialId: "",
997
+ trialUri: canonicalId("trial", runId),
998
+ trialIndex: 0,
999
+ evalId: authoringId(definition),
1000
+ status,
1001
+ reportLocation: runWriter.location,
1002
+ trialCount: 0,
1003
+ passed: 0,
1004
+ failed: 0,
1005
+ aggregateScoring: {
1006
+ passed: 0,
1007
+ failed: 0,
1008
+ passRate: 0
1009
+ },
1010
+ trials: []
1011
+ };
760
1012
  const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
761
1013
  return {
762
1014
  ...trials[0],
@@ -764,12 +1016,13 @@ async function executeRun(definition, options) {
764
1016
  evalId: authoringId(definition),
765
1017
  status,
766
1018
  trialCount: trials.length,
767
- passed: trials.length - failed,
768
- failed,
1019
+ passed: counts.passed,
1020
+ failed: counts.failed,
769
1021
  aggregateScoring: {
770
- passed: trials.length - failed,
771
- failed,
772
- passRate: (trials.length - failed) / trials.length,
1022
+ passed: counts.passed,
1023
+ failed: counts.failed,
1024
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1025
+ passRate: counts.passed / trials.length,
773
1026
  ...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
774
1027
  },
775
1028
  trials
@@ -781,25 +1034,45 @@ async function runTrial(definition, options) {
781
1034
  const trialId = options.trialId ?? createId("trial");
782
1035
  const startedAt = now().toISOString();
783
1036
  const selectedRuntime = options.runtime;
784
- const runtime = selectedRuntime ? definition.agent.runtimes?.[selectedRuntime] : void 0;
1037
+ const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
785
1038
  if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
786
1039
  const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
787
- const fixtureContext = {
1040
+ const trialIndex = options.trialIndex ?? 0;
1041
+ const signal = options.signal ?? new AbortController().signal;
1042
+ const root = options.root ?? process.cwd();
1043
+ const baseContext = {
788
1044
  runId,
789
1045
  evalId: authoringId(definition),
790
1046
  trialId,
791
- trialIndex: options.trialIndex ?? 0,
1047
+ trialIndex,
792
1048
  metadata: {
793
- ...definition.metadata,
1049
+ ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
794
1050
  randomSeed
795
1051
  },
796
1052
  ...options.parameters ? { parameters: options.parameters } : {},
797
1053
  ...selectedRuntime ? { runtime: selectedRuntime } : {}
798
1054
  };
1055
+ let datasetsError;
1056
+ let prepared = [];
1057
+ try {
1058
+ prepared = await prepareDatasets(definition.datasets ?? [], root, {
1059
+ evalId: baseContext.evalId,
1060
+ runId,
1061
+ trialId,
1062
+ trialIndex,
1063
+ seed: randomSeed,
1064
+ parameters: options.parameters ?? {},
1065
+ signal
1066
+ });
1067
+ } catch (error) {
1068
+ datasetsError = error;
1069
+ }
1070
+ const candidateData = datasetValues(prepared, "candidate");
1071
+ const scoringData = datasetValues(prepared, "all");
799
1072
  const events = [];
800
1073
  const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt), { catalog: options.catalog });
801
1074
  const trialWriter = await runWriter.startTrial({
802
- schemaVersion: 4,
1075
+ schemaVersion: 5,
803
1076
  runId,
804
1077
  runUri: canonicalId("run", runId),
805
1078
  trialId,
@@ -809,15 +1082,30 @@ async function runTrial(definition, options) {
809
1082
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
810
1083
  parameters: options.parameters,
811
1084
  matrix: options.matrix,
812
- startedAt
1085
+ startedAt,
1086
+ ...prepared.length ? { datasets: trialDatasets(prepared) } : {}
813
1087
  });
814
1088
  let writeChain = Promise.resolve();
815
1089
  let reportingFailed = false;
1090
+ const trialRef = {
1091
+ runId,
1092
+ trialId,
1093
+ trialIndex,
1094
+ evalId: definition.id,
1095
+ ...options.suiteId ? { suiteId: options.suiteId } : {},
1096
+ ...options.parameters ? { parameters: options.parameters } : {}
1097
+ };
1098
+ const observe = (event) => {
1099
+ try {
1100
+ options.onEvent?.(trialRef, event);
1101
+ } catch {}
1102
+ };
816
1103
  const append = async (event) => {
817
1104
  events.push(event);
818
1105
  writeChain = writeChain.then(() => trialWriter.appendEvent(event));
819
1106
  try {
820
1107
  await writeChain;
1108
+ observe(event);
821
1109
  } catch (cause) {
822
1110
  reportingFailed = true;
823
1111
  throw new ReportError({
@@ -830,10 +1118,11 @@ async function runTrial(definition, options) {
830
1118
  ...event,
831
1119
  source: "runner"
832
1120
  });
833
- const onAutEvent = (event) => append({
1121
+ const emitAut = (event) => append({
834
1122
  ...event,
835
1123
  source: "aut"
836
1124
  });
1125
+ const onAutEvent = (event) => emitAut(normalizeAutEvent(event, now));
837
1126
  let workspace;
838
1127
  let context;
839
1128
  let primaryError;
@@ -852,15 +1141,20 @@ async function runTrial(definition, options) {
852
1141
  kind: "trial-started",
853
1142
  timestamp: now().toISOString()
854
1143
  });
1144
+ if (signal.aborted) throw cancelled();
1145
+ if (datasetsError) throw new DatasetError({
1146
+ cause: datasetsError,
1147
+ message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1148
+ });
855
1149
  workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
856
- try: () => createTrialWorkspace(fixtureContext, definition.fixtures, options.workspaceRoot),
857
- catch: (cause) => new FixtureError({
1150
+ try: () => createTrialWorkspace(trialId, prepared, options.workspaceRoot),
1151
+ catch: (cause) => new DatasetError({
858
1152
  cause,
859
1153
  message: cause instanceof Error ? cause.message : String(cause)
860
1154
  })
861
1155
  }), (created) => Effect.tryPromise({
862
1156
  try: () => created.cleanup(),
863
- catch: (cause) => new FixtureError({
1157
+ catch: (cause) => new DatasetError({
864
1158
  cause,
865
1159
  message: cause instanceof Error ? cause.message : String(cause)
866
1160
  })
@@ -869,7 +1163,8 @@ async function runTrial(definition, options) {
869
1163
  }).pipe(Effect.tap(() => Effect.logError("trial workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
870
1164
  const trialWorkspace = workspace;
871
1165
  const trialContext = {
872
- ...fixtureContext,
1166
+ ...baseContext,
1167
+ data: candidateData,
873
1168
  workspace: trialWorkspace.artifacts.candidate,
874
1169
  evaluatorWorkspace: trialWorkspace.artifacts.evaluator
875
1170
  };
@@ -892,11 +1187,14 @@ async function runTrial(definition, options) {
892
1187
  now,
893
1188
  randomSeed,
894
1189
  emitRunner,
1190
+ emitAut,
1191
+ data: scoringData,
1192
+ signal,
895
1193
  progress
896
1194
  }),
897
1195
  catch: (cause) => cause
898
1196
  }), (opened) => Effect.tryPromise({
899
- try: () => opened.close(),
1197
+ try: async () => opened.close?.(),
900
1198
  catch: (cause) => new AutExecutionError({
901
1199
  cause,
902
1200
  message: cause instanceof Error ? cause.message : String(cause)
@@ -916,6 +1214,7 @@ async function runTrial(definition, options) {
916
1214
  if (!primaryError) primaryError = closeError;
917
1215
  await tryEmitError(closeError, emitRunner, now);
918
1216
  }
1217
+ const wasCancelled = primaryError instanceof CancelledError;
919
1218
  const { checkpoints, stoppedEarly, lastTurn } = progress;
920
1219
  const scoreResults = [];
921
1220
  const skippedScorers = [];
@@ -927,9 +1226,11 @@ async function runTrial(definition, options) {
927
1226
  lastTurn,
928
1227
  primaryError,
929
1228
  stoppedEarly,
1229
+ cancelled: wasCancelled,
930
1230
  reportingFailed,
931
1231
  results: scoreResults,
932
1232
  skippedScorers,
1233
+ data: scoringData,
933
1234
  now,
934
1235
  emitRunner
935
1236
  });
@@ -960,7 +1261,7 @@ async function runTrial(definition, options) {
960
1261
  } catch (error) {
961
1262
  primaryError = error;
962
1263
  }
963
- status = primaryError ? "failed" : "completed";
1264
+ status = wasCancelled ? "cancelled" : primaryError ? "failed" : "completed";
964
1265
  const error = primaryError ? recordError(primaryError) : void 0;
965
1266
  const endedAt = now().toISOString();
966
1267
  await trialWriter.finalize({
@@ -971,14 +1272,22 @@ async function runTrial(definition, options) {
971
1272
  ...artifacts ? { artifacts } : {},
972
1273
  ...error ? { error } : {}
973
1274
  });
974
- if (!options.runWriter) await runWriter.finalize({
975
- status,
976
- endedAt,
977
- trialCount: 1,
978
- passed: status === "completed" && scoring.passed ? 1 : 0,
979
- failed: status === "failed" || !scoring.passed ? 1 : 0,
980
- ...error ? { error } : {}
981
- });
1275
+ if (!options.runWriter) {
1276
+ const counts = trialCounts([{
1277
+ status,
1278
+ scoring
1279
+ }]);
1280
+ await runWriter.finalize({
1281
+ status,
1282
+ endedAt,
1283
+ durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1284
+ trialCount: 1,
1285
+ passed: counts.passed,
1286
+ failed: counts.failed,
1287
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1288
+ ...error ? { error } : {}
1289
+ });
1290
+ }
982
1291
  return {
983
1292
  runId,
984
1293
  runUri: canonicalId("run", runId),
@@ -993,7 +1302,28 @@ async function runTrial(definition, options) {
993
1302
  ...error ? { error } : {}
994
1303
  };
995
1304
  }
1305
+ function datasetValues(prepared, audience) {
1306
+ const values = {};
1307
+ for (const { dataset, value } of prepared) {
1308
+ if (value === void 0) continue;
1309
+ if (audience === "candidate" && dataset.visibility !== "candidate") continue;
1310
+ values[dataset.id] = value;
1311
+ }
1312
+ return values;
1313
+ }
1314
+ /** A cancelled trial is neither passed nor failed. */
1315
+ function trialCounts(trials) {
1316
+ let passed = 0;
1317
+ let failed = 0;
1318
+ let cancelledTrials = 0;
1319
+ for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
1320
+ else if (trial.status === "completed" && trial.scoring?.passed) passed++;
1321
+ else failed++;
1322
+ return {
1323
+ passed,
1324
+ failed,
1325
+ cancelled: cancelledTrials
1326
+ };
1327
+ }
996
1328
  //#endregion
997
- export { readTrialEvents as a, readTrialSummary as c, CheckpointExecutionError as d, FixtureError as f, readRunSummary as i, localReportStore as l, ScoringError as m, runMatrix as n, readTrialManifest as o, ReportError as p, readRunManifest as r, readTrialScoring as s, runEval as t, AutExecutionError as u };
998
-
999
- //# sourceMappingURL=src-DoPs9Yug.mjs.map
1329
+ export { ReportError as _, readRunSummary as a, readTrialScoring as c, staticDatasetFile as d, staticDatasetFiles as f, DatasetError as g, CheckpointExecutionError as h, readRunManifest as i, readTrialSummary as l, CancelledError as m, runMatrix as n, readTrialEvents as o, AutExecutionError as p, ownerIsGone as r, readTrialManifest as s, runEval as t, localReportStore as u, ScoringError as v };