@leostera/evalkit 0.5.0 → 0.6.1

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 +70 -6
  2. package/README.md +10 -8
  3. package/dist/cli.mjs +1227 -258
  4. package/dist/dashboard/assets/index-CTJpd8q8.js +34 -0
  5. package/dist/dashboard/assets/index-YAabTN3X.css +2 -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 +1346 -2
  10. package/dist/index.d.mts.map +1 -1
  11. package/dist/index.mjs +2 -4
  12. package/dist/runner.d.mts +809 -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-CKqOzeBi.mjs} +448 -116
  29. package/dist/{src-ButXPY8P.mjs → src-CZPufWN_.mjs} +419 -94
  30. package/dist/{src-DzZueFn3.mjs → src-CZnDXP1K.mjs} +46 -10
  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-CZPufWN_.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,15 +542,21 @@ 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();
548
+ const message = step.kind === "user" ? step.message.replaceAll("{{randomSeed}}", String(input.randomSeed)) : void 0;
350
549
  await emitRunner({
351
550
  kind: "transcript-step-started",
352
551
  step: index,
552
+ ...message === void 0 ? {} : { message },
353
553
  timestamp: now().toISOString()
354
554
  });
355
- if (step.kind === "user") {
555
+ if (message !== void 0) {
356
556
  const cursor = input.events.length;
357
- await sendUserTurn(session, step.message.replaceAll("{{randomSeed}}", String(input.randomSeed)));
557
+ turn++;
558
+ const result = await sendUserTurn(session, message, input.signal);
559
+ if (result !== void 0) for (const event of turnResultEvents(result, turn, now)) await input.emitAut(event);
358
560
  progress.lastTurn = turnView(input.events, cursor, input.events.length, index);
359
561
  } else if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") {
360
562
  const result = await runCheckpoint(definition, step, index, input);
@@ -370,7 +572,7 @@ async function executeTranscript(definition, session, input) {
370
572
  step: index,
371
573
  timestamp: now().toISOString()
372
574
  });
373
- if (!result.passed && definition.policy?.failfast) {
575
+ if (result.status === "failed" && definition.policy?.failfast) {
374
576
  progress.stoppedEarly = true;
375
577
  await skipRemainingSteps(definition, index, input);
376
578
  break;
@@ -384,10 +586,10 @@ async function executeTranscript(definition, session, input) {
384
586
  });
385
587
  }
386
588
  }
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({
589
+ async function sendUserTurn(session, message, signal) {
590
+ return runBoundary(Effect.tryPromise({
591
+ try: () => unlessAborted(() => session.send(message), signal),
592
+ catch: (cause) => cause instanceof ReportError || cause instanceof CancelledError ? cause : new AutExecutionError({
391
593
  cause,
392
594
  message: cause instanceof Error ? cause.message : String(cause)
393
595
  })
@@ -407,7 +609,9 @@ async function runCheckpoint(definition, step, index, input) {
407
609
  context: input.context,
408
610
  artifacts: input.artifacts,
409
611
  trajectory: { events: input.events },
410
- turn: progress.lastTurn
612
+ turn: progress.lastTurn,
613
+ ...turnOutput(progress.lastTurn),
614
+ data: input.data
411
615
  }, now, definition.judge)).catch(async (error) => {
412
616
  const recorded = recordError(error);
413
617
  progress.checkpoints.push({
@@ -445,24 +649,29 @@ async function skipRemainingSteps(definition, after, { now, emitRunner, progress
445
649
  });
446
650
  }
447
651
  }
652
+ /** The scorer-facing `output`: the content of the turn's last assistant message. */
653
+ function turnOutput(turn) {
654
+ const last = turn?.assistantMessages.at(-1);
655
+ return last === void 0 ? {} : { output: last.content };
656
+ }
448
657
  //#endregion
449
658
  //#region ../runner/src/final-scoring.ts
450
659
  function scoreSummary(results, checkpoints, skippedScorers) {
451
- const valid = results.filter((result) => result.value !== void 0);
660
+ const valid = results.filter((result) => result.value !== void 0 && !result.skipped);
452
661
  const hasError = results.some((result) => result.error !== void 0);
453
662
  return {
454
663
  results,
455
664
  ...checkpoints.length ? { checkpoints } : {},
456
665
  ...skippedScorers.length ? { skippedScorers } : {},
457
666
  ...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
667
+ passed: !hasError && results.every((result) => result.skipped || result.passed === true) && checkpoints.every((result) => result.status === "passed" || result.status === "skipped") && !skippedScorers.length
459
668
  };
460
669
  }
461
670
  /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
462
671
  async function evaluateFinalScorers(definition, input) {
463
672
  const { context, workspace, events, lastTurn, results, skippedScorers, now, emitRunner } = input;
464
673
  for (const scorer of definition.scoring) {
465
- if (input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
674
+ if (input.cancelled || input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
466
675
  skippedScorers.push(scorer.name);
467
676
  continue;
468
677
  }
@@ -478,7 +687,9 @@ async function evaluateFinalScorers(definition, input) {
478
687
  context,
479
688
  artifacts: workspace.artifacts,
480
689
  trajectory: { events },
481
- ...lastTurn ? { turn: lastTurn } : {}
690
+ ...lastTurn ? { turn: lastTurn } : {},
691
+ ...turnOutput(lastTurn),
692
+ data: input.data
482
693
  }, definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
483
694
  cause,
484
695
  message: cause.message
@@ -510,7 +721,7 @@ async function evaluateFinalScorers(definition, input) {
510
721
  await emitRunner({
511
722
  kind: "scorer-completed",
512
723
  scorer: scorer.name,
513
- value: result.value ?? 0,
724
+ ...result.skipped ? { skipped: true } : { value: result.value ?? 0 },
514
725
  timestamp: now().toISOString()
515
726
  });
516
727
  }
@@ -618,6 +829,20 @@ const readTrialScoring = (root, runId, trialId) => decode(join(root, runId, "tri
618
829
  async function readTrialEvents(root, runId, trialId) {
619
830
  return parseTrajectoryJsonl(await readFile(join(root, runId, "trials", trialId, "trajectory.jsonl"), "utf8"));
620
831
  }
832
+ /**
833
+ * True when a run's owning process is known to be gone: it ran on this host
834
+ * and its PID no longer exists. Runs from other hosts are never judged.
835
+ */
836
+ function ownerIsGone(owner) {
837
+ if (!owner || owner.hostname !== os.hostname()) return false;
838
+ if (owner.pid === process.pid) return false;
839
+ try {
840
+ process.kill(owner.pid, 0);
841
+ return false;
842
+ } catch (error) {
843
+ return error.code === "ESRCH";
844
+ }
845
+ }
621
846
  //#endregion
622
847
  //#region ../runner/src/matrix.ts
623
848
  /** Bounded workers pull cells from the iterator; no Cartesian job array is allocated. */
@@ -630,10 +855,11 @@ function runMatrix(matrix, options) {
630
855
  const summary = {
631
856
  cells: 0,
632
857
  passed: 0,
633
- failed: 0
858
+ failed: 0,
859
+ cancelled: 0
634
860
  };
635
861
  yield* Effect.all(Array.from({ length: Math.min(count, concurrency) }, () => Effect.gen(function* () {
636
- while (true) {
862
+ while (!options.signal?.aborted) {
637
863
  const next = iterator.next();
638
864
  if (next.done) return;
639
865
  const cell = next.value;
@@ -641,9 +867,11 @@ function runMatrix(matrix, options) {
641
867
  "local",
642
868
  "remote",
643
869
  "sandbox"
644
- ].find((name) => cell.eval.agent.runtimes?.[name]);
870
+ ].find((name) => agentRuntimes(cell.eval.agent)[name]);
871
+ const suiteId = options.suiteId ?? options.suiteFor?.(cell);
645
872
  const result = yield* runEval(cell.eval, {
646
873
  ...options,
874
+ ...suiteId ? { suiteId } : {},
647
875
  concurrency: 1,
648
876
  runtime,
649
877
  parameters: cell.parameters,
@@ -653,7 +881,8 @@ function runMatrix(matrix, options) {
653
881
  }
654
882
  });
655
883
  summary.cells++;
656
- if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
884
+ if (result.status === "cancelled") summary.cancelled++;
885
+ else if (result.status === "completed" && (result.aggregateScoring?.passRate === 1 || result.scoring?.passed)) summary.passed++;
657
886
  else summary.failed++;
658
887
  if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, result));
659
888
  }
@@ -694,12 +923,16 @@ function assertUuid(value, label) {
694
923
  }
695
924
  function runMetadata(definition, options, runId, startedAt) {
696
925
  return {
697
- schemaVersion: 4,
926
+ schemaVersion: 5,
698
927
  runId,
699
928
  runUri: canonicalId("run", runId),
700
929
  evalId: definition.id,
701
930
  ...options.suiteId ? { suiteId: options.suiteId } : {},
702
931
  ...options.source ? { source: options.source } : {},
932
+ process: {
933
+ pid: process.pid,
934
+ hostname: os.hostname()
935
+ },
703
936
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
704
937
  parameters: options.parameters,
705
938
  matrix: options.matrix,
@@ -731,9 +964,10 @@ async function executeRun(definition, options) {
731
964
  const aggregateStartedAt = now();
732
965
  const runId = options.runId ?? createId("run");
733
966
  const runWriter = await options.report.startRun(runMetadata(definition, options, runId, aggregateStartedAt.toISOString()), { catalog: options.catalog });
967
+ const signal = options.signal;
734
968
  const trialEffects = Array.from({ length: requestedTrials }, (_, trialIndex) => {
735
969
  const trial = Effect.tryPromise({
736
- try: () => runTrial(definition, {
970
+ try: async () => signal?.aborted ? void 0 : runTrial(definition, {
737
971
  ...options,
738
972
  runId,
739
973
  trialId: createId("trial"),
@@ -745,18 +979,38 @@ async function executeRun(definition, options) {
745
979
  });
746
980
  return options.semaphore ? options.semaphore.withPermits(1)(trial) : trial;
747
981
  });
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";
982
+ const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
983
+ const counts = trialCounts(trials);
984
+ const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
751
985
  const endedAt = now();
752
986
  await runWriter.finalize({
753
987
  status,
754
988
  endedAt: endedAt.toISOString(),
755
989
  durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
756
990
  trialCount: trials.length,
757
- passed: trials.length - failed,
758
- failed
991
+ passed: counts.passed,
992
+ failed: counts.failed,
993
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {}
759
994
  });
995
+ if (!trials.length) return {
996
+ runId,
997
+ runUri: canonicalId("run", runId),
998
+ trialId: "",
999
+ trialUri: canonicalId("trial", runId),
1000
+ trialIndex: 0,
1001
+ evalId: authoringId(definition),
1002
+ status,
1003
+ reportLocation: runWriter.location,
1004
+ trialCount: 0,
1005
+ passed: 0,
1006
+ failed: 0,
1007
+ aggregateScoring: {
1008
+ passed: 0,
1009
+ failed: 0,
1010
+ passRate: 0
1011
+ },
1012
+ trials: []
1013
+ };
760
1014
  const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
761
1015
  return {
762
1016
  ...trials[0],
@@ -764,12 +1018,13 @@ async function executeRun(definition, options) {
764
1018
  evalId: authoringId(definition),
765
1019
  status,
766
1020
  trialCount: trials.length,
767
- passed: trials.length - failed,
768
- failed,
1021
+ passed: counts.passed,
1022
+ failed: counts.failed,
769
1023
  aggregateScoring: {
770
- passed: trials.length - failed,
771
- failed,
772
- passRate: (trials.length - failed) / trials.length,
1024
+ passed: counts.passed,
1025
+ failed: counts.failed,
1026
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1027
+ passRate: counts.passed / trials.length,
773
1028
  ...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
774
1029
  },
775
1030
  trials
@@ -781,25 +1036,45 @@ async function runTrial(definition, options) {
781
1036
  const trialId = options.trialId ?? createId("trial");
782
1037
  const startedAt = now().toISOString();
783
1038
  const selectedRuntime = options.runtime;
784
- const runtime = selectedRuntime ? definition.agent.runtimes?.[selectedRuntime] : void 0;
1039
+ const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
785
1040
  if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
786
1041
  const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
787
- const fixtureContext = {
1042
+ const trialIndex = options.trialIndex ?? 0;
1043
+ const signal = options.signal ?? new AbortController().signal;
1044
+ const root = options.root ?? process.cwd();
1045
+ const baseContext = {
788
1046
  runId,
789
1047
  evalId: authoringId(definition),
790
1048
  trialId,
791
- trialIndex: options.trialIndex ?? 0,
1049
+ trialIndex,
792
1050
  metadata: {
793
- ...definition.metadata,
1051
+ ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
794
1052
  randomSeed
795
1053
  },
796
1054
  ...options.parameters ? { parameters: options.parameters } : {},
797
1055
  ...selectedRuntime ? { runtime: selectedRuntime } : {}
798
1056
  };
1057
+ let datasetsError;
1058
+ let prepared = [];
1059
+ try {
1060
+ prepared = await prepareDatasets(definition.datasets ?? [], root, {
1061
+ evalId: baseContext.evalId,
1062
+ runId,
1063
+ trialId,
1064
+ trialIndex,
1065
+ seed: randomSeed,
1066
+ parameters: options.parameters ?? {},
1067
+ signal
1068
+ });
1069
+ } catch (error) {
1070
+ datasetsError = error;
1071
+ }
1072
+ const candidateData = datasetValues(prepared, "candidate");
1073
+ const scoringData = datasetValues(prepared, "all");
799
1074
  const events = [];
800
1075
  const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt), { catalog: options.catalog });
801
1076
  const trialWriter = await runWriter.startTrial({
802
- schemaVersion: 4,
1077
+ schemaVersion: 5,
803
1078
  runId,
804
1079
  runUri: canonicalId("run", runId),
805
1080
  trialId,
@@ -809,15 +1084,30 @@ async function runTrial(definition, options) {
809
1084
  ...definition.agent.identity ? { aut: definition.agent.identity } : {},
810
1085
  parameters: options.parameters,
811
1086
  matrix: options.matrix,
812
- startedAt
1087
+ startedAt,
1088
+ ...prepared.length ? { datasets: trialDatasets(prepared) } : {}
813
1089
  });
814
1090
  let writeChain = Promise.resolve();
815
1091
  let reportingFailed = false;
1092
+ const trialRef = {
1093
+ runId,
1094
+ trialId,
1095
+ trialIndex,
1096
+ evalId: definition.id,
1097
+ ...options.suiteId ? { suiteId: options.suiteId } : {},
1098
+ ...options.parameters ? { parameters: options.parameters } : {}
1099
+ };
1100
+ const observe = (event) => {
1101
+ try {
1102
+ options.onEvent?.(trialRef, event);
1103
+ } catch {}
1104
+ };
816
1105
  const append = async (event) => {
817
1106
  events.push(event);
818
1107
  writeChain = writeChain.then(() => trialWriter.appendEvent(event));
819
1108
  try {
820
1109
  await writeChain;
1110
+ observe(event);
821
1111
  } catch (cause) {
822
1112
  reportingFailed = true;
823
1113
  throw new ReportError({
@@ -830,10 +1120,11 @@ async function runTrial(definition, options) {
830
1120
  ...event,
831
1121
  source: "runner"
832
1122
  });
833
- const onAutEvent = (event) => append({
1123
+ const emitAut = (event) => append({
834
1124
  ...event,
835
1125
  source: "aut"
836
1126
  });
1127
+ const onAutEvent = (event) => emitAut(normalizeAutEvent(event, now));
837
1128
  let workspace;
838
1129
  let context;
839
1130
  let primaryError;
@@ -852,15 +1143,20 @@ async function runTrial(definition, options) {
852
1143
  kind: "trial-started",
853
1144
  timestamp: now().toISOString()
854
1145
  });
1146
+ if (signal.aborted) throw cancelled();
1147
+ if (datasetsError) throw new DatasetError({
1148
+ cause: datasetsError,
1149
+ message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1150
+ });
855
1151
  workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
856
- try: () => createTrialWorkspace(fixtureContext, definition.fixtures, options.workspaceRoot),
857
- catch: (cause) => new FixtureError({
1152
+ try: () => createTrialWorkspace(trialId, prepared, options.workspaceRoot),
1153
+ catch: (cause) => new DatasetError({
858
1154
  cause,
859
1155
  message: cause instanceof Error ? cause.message : String(cause)
860
1156
  })
861
1157
  }), (created) => Effect.tryPromise({
862
1158
  try: () => created.cleanup(),
863
- catch: (cause) => new FixtureError({
1159
+ catch: (cause) => new DatasetError({
864
1160
  cause,
865
1161
  message: cause instanceof Error ? cause.message : String(cause)
866
1162
  })
@@ -869,7 +1165,8 @@ async function runTrial(definition, options) {
869
1165
  }).pipe(Effect.tap(() => Effect.logError("trial workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
870
1166
  const trialWorkspace = workspace;
871
1167
  const trialContext = {
872
- ...fixtureContext,
1168
+ ...baseContext,
1169
+ data: candidateData,
873
1170
  workspace: trialWorkspace.artifacts.candidate,
874
1171
  evaluatorWorkspace: trialWorkspace.artifacts.evaluator
875
1172
  };
@@ -892,11 +1189,14 @@ async function runTrial(definition, options) {
892
1189
  now,
893
1190
  randomSeed,
894
1191
  emitRunner,
1192
+ emitAut,
1193
+ data: scoringData,
1194
+ signal,
895
1195
  progress
896
1196
  }),
897
1197
  catch: (cause) => cause
898
1198
  }), (opened) => Effect.tryPromise({
899
- try: () => opened.close(),
1199
+ try: async () => opened.close?.(),
900
1200
  catch: (cause) => new AutExecutionError({
901
1201
  cause,
902
1202
  message: cause instanceof Error ? cause.message : String(cause)
@@ -916,6 +1216,7 @@ async function runTrial(definition, options) {
916
1216
  if (!primaryError) primaryError = closeError;
917
1217
  await tryEmitError(closeError, emitRunner, now);
918
1218
  }
1219
+ const wasCancelled = primaryError instanceof CancelledError;
919
1220
  const { checkpoints, stoppedEarly, lastTurn } = progress;
920
1221
  const scoreResults = [];
921
1222
  const skippedScorers = [];
@@ -927,9 +1228,11 @@ async function runTrial(definition, options) {
927
1228
  lastTurn,
928
1229
  primaryError,
929
1230
  stoppedEarly,
1231
+ cancelled: wasCancelled,
930
1232
  reportingFailed,
931
1233
  results: scoreResults,
932
1234
  skippedScorers,
1235
+ data: scoringData,
933
1236
  now,
934
1237
  emitRunner
935
1238
  });
@@ -960,7 +1263,7 @@ async function runTrial(definition, options) {
960
1263
  } catch (error) {
961
1264
  primaryError = error;
962
1265
  }
963
- status = primaryError ? "failed" : "completed";
1266
+ status = wasCancelled ? "cancelled" : primaryError ? "failed" : "completed";
964
1267
  const error = primaryError ? recordError(primaryError) : void 0;
965
1268
  const endedAt = now().toISOString();
966
1269
  await trialWriter.finalize({
@@ -971,14 +1274,22 @@ async function runTrial(definition, options) {
971
1274
  ...artifacts ? { artifacts } : {},
972
1275
  ...error ? { error } : {}
973
1276
  });
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
- });
1277
+ if (!options.runWriter) {
1278
+ const counts = trialCounts([{
1279
+ status,
1280
+ scoring
1281
+ }]);
1282
+ await runWriter.finalize({
1283
+ status,
1284
+ endedAt,
1285
+ durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
1286
+ trialCount: 1,
1287
+ passed: counts.passed,
1288
+ failed: counts.failed,
1289
+ ...counts.cancelled ? { cancelled: counts.cancelled } : {},
1290
+ ...error ? { error } : {}
1291
+ });
1292
+ }
982
1293
  return {
983
1294
  runId,
984
1295
  runUri: canonicalId("run", runId),
@@ -993,7 +1304,28 @@ async function runTrial(definition, options) {
993
1304
  ...error ? { error } : {}
994
1305
  };
995
1306
  }
1307
+ function datasetValues(prepared, audience) {
1308
+ const values = {};
1309
+ for (const { dataset, value } of prepared) {
1310
+ if (value === void 0) continue;
1311
+ if (audience === "candidate" && dataset.visibility !== "candidate") continue;
1312
+ values[dataset.id] = value;
1313
+ }
1314
+ return values;
1315
+ }
1316
+ /** A cancelled trial is neither passed nor failed. */
1317
+ function trialCounts(trials) {
1318
+ let passed = 0;
1319
+ let failed = 0;
1320
+ let cancelledTrials = 0;
1321
+ for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
1322
+ else if (trial.status === "completed" && trial.scoring?.passed) passed++;
1323
+ else failed++;
1324
+ return {
1325
+ passed,
1326
+ failed,
1327
+ cancelled: cancelledTrials
1328
+ };
1329
+ }
996
1330
  //#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
1331
+ 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 };