@leostera/evalkit 0.10.0 → 0.11.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.
@@ -1,1877 +0,0 @@
1
- import { $ as judgeInputNames, B as normalizeAutEvent, G as DEFAULT_ARTIFACT_EXCLUDE, I as RunSummarySchema, K as DEFAULT_ATTACHMENT_LIMITS, L as ScoreResultSchema, M as RunMetadataSchema, N as RunScoringSchema, R as TrajectoryEventSchema, T as JsonValueSchema, U as canonicalParameters, V as turnResultEvents, W as turnTokens, X as answerInstruction, Y as UnreadableAnswer, a as assertJudgesIndependent, at as toJson, et as judgePrompt, gt as resourceUri, i as agentRuntimes, j as RunManifestSchema, lt as relativeDatasetPath, m as recordError, nt as readJudgeAnswer, o as authoringId, ot as toJsonObject, rt as weightedMean, v as parseTrajectoryJsonl, w as CheckpointResultSchema } from "./src-B14fXAGp.mjs";
2
- import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-G-uFPJKg.mjs";
3
- import * as Schema from "effect/Schema";
4
- import { Data, Effect, Either, Exit, Scope } from "effect";
5
- import os from "node:os";
6
- import { appendFile, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
- import path, { join } from "node:path";
8
- import { createHash, randomUUID } from "node:crypto";
9
- //#region ../runner/src/errors.ts
10
- var AutExecutionError = class extends Data.TaggedError("AutExecutionError") {};
11
- var DatasetError = class extends Data.TaggedError("DatasetError") {};
12
- var ReportError = class extends Data.TaggedError("ReportError") {};
13
- var ScoringError = class extends Data.TaggedError("ScoringError") {};
14
- var RuleExecutionError = class extends Data.TaggedError("RuleExecutionError") {};
15
- var CheckpointExecutionError = class extends Data.TaggedError("CheckpointExecutionError") {};
16
- /** The run was cancelled (Ctrl+C); the run stops and is finalized as cancelled. */
17
- var CancelledError = class extends Data.TaggedError("CancelledError") {};
18
- function cancelled() {
19
- return new CancelledError({ message: "Cancelled" });
20
- }
21
- /** Settles like `work`, or rejects with `CancelledError` as soon as `signal` aborts. */
22
- async function unlessAborted(work, signal) {
23
- if (signal.aborted) throw cancelled();
24
- let onAbort;
25
- const aborted = new Promise((_, reject) => {
26
- onAbort = () => reject(cancelled());
27
- signal.addEventListener("abort", onAbort, { once: true });
28
- });
29
- try {
30
- return await Promise.race([work(), aborted]);
31
- } finally {
32
- signal.removeEventListener("abort", onAbort);
33
- }
34
- }
35
- //#endregion
36
- //#region ../runner/src/attachments.ts
37
- const SEGMENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
38
- const MAX_NAME = 200;
39
- /**
40
- * Checks an attachment name: a relative path whose segments are letters,
41
- * digits, `.`, `_` and `-`, not starting with a dot. That rules out
42
- * absolute paths, `..`, hidden files, separators other than `/`, and
43
- * anything a filesystem or URL would read differently.
44
- */
45
- function attachmentName(name) {
46
- if (typeof name !== "string" || !name || name.length > MAX_NAME || !name.split("/").every((segment) => SEGMENT.test(segment))) throw new Error(`Attachment name ${JSON.stringify(name)} is not allowed: use a relative path of letters, digits, ".", "_" and "-", like "logs/agent.log"`);
47
- return name;
48
- }
49
- /** The last `max` bytes, starting on a whole UTF-8 character when the content is text. */
50
- function lastBytes(bytes, max, text) {
51
- let start = bytes.byteLength - max;
52
- if (text) while (start < bytes.byteLength && (bytes[start] & 192) === 128) start++;
53
- return bytes.subarray(start);
54
- }
55
- /**
56
- * Saves a run's attachments as `attachments/<name>` below its artifacts.
57
- * Content is stored as given and never interpreted. A file over
58
- * `maxFileBytes` keeps its end (where a log's errors are); one that would
59
- * take the run past `maxRunBytes` is rejected.
60
- */
61
- function attachmentStore(writer, limits = {}) {
62
- const { maxFileBytes, maxRunBytes } = {
63
- ...DEFAULT_ATTACHMENT_LIMITS,
64
- ...limits
65
- };
66
- const entries = [];
67
- const names = /* @__PURE__ */ new Set();
68
- const pending = /* @__PURE__ */ new Set();
69
- let used = 0;
70
- let closed = false;
71
- async function attach(name, content) {
72
- if (closed) throw new Error(`Cannot attach ${JSON.stringify(name)}: the run has finished`);
73
- attachmentName(name);
74
- if (typeof content !== "string" && !(content instanceof Uint8Array)) throw new Error(`Attachment ${name} must be a string or a Uint8Array, not ${content === null ? "null" : typeof content}`);
75
- if (names.has(name)) throw new Error(`Attachment ${name} was already attached`);
76
- const text = typeof content === "string";
77
- const original = text ? new TextEncoder().encode(content) : content;
78
- const bytes = original.byteLength > maxFileBytes ? lastBytes(original, maxFileBytes, text) : original;
79
- if (used + bytes.byteLength > maxRunBytes) throw new Error(`Attachment ${name} (${bytes.byteLength} bytes) would take this run past its ${maxRunBytes}-byte attachment limit (${used} used)`);
80
- names.add(name);
81
- used += bytes.byteLength;
82
- const write = writer.writeArtifact(`attachments/${name}`, bytes);
83
- pending.add(write);
84
- try {
85
- await write;
86
- entries.push({
87
- name,
88
- size: bytes.byteLength,
89
- ...bytes.byteLength < original.byteLength ? { truncatedFrom: original.byteLength } : {}
90
- });
91
- } catch (error) {
92
- names.delete(name);
93
- used -= bytes.byteLength;
94
- throw error;
95
- } finally {
96
- pending.delete(write);
97
- }
98
- }
99
- return {
100
- attach,
101
- async close() {
102
- closed = true;
103
- await Promise.allSettled(pending);
104
- return [...entries].sort((a, b) => a.name.localeCompare(b.name));
105
- }
106
- };
107
- }
108
- /**
109
- * An `attach` that remembers what it saved, so a score or checkpoint can
110
- * link the files its scorer attached, even when the scorer then failed.
111
- */
112
- function trackAttachments(attach) {
113
- const attached = [];
114
- return {
115
- attached,
116
- async attach(name, content) {
117
- await attach(name, content);
118
- attached.push(name);
119
- }
120
- };
121
- }
122
- /** `{ attachments }` when a scorer attached any. */
123
- const attachedFields = (attached) => attached.length ? { attachments: [...attached] } : {};
124
- //#endregion
125
- //#region ../core/src/glob.ts
126
- /**
127
- * A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
128
- * spans segments, and `{a,b}` is an alternation.
129
- */
130
- function globMatcher(pattern) {
131
- let source = "";
132
- for (let index = 0; index < pattern.length; index++) {
133
- const char = pattern[index];
134
- if (char === "*" && pattern[index + 1] === "*") {
135
- index++;
136
- if (pattern[index + 1] === "/") {
137
- index++;
138
- source += "(?:.*/)?";
139
- } else source += ".*";
140
- } else if (char === "*") source += "[^/]*";
141
- else if (char === "?") source += "[^/]";
142
- else if (char === "{") {
143
- const end = pattern.indexOf("}", index);
144
- if (end === -1) source += "\\{";
145
- else {
146
- source += `(?:${pattern.slice(index + 1, end).split(",").map((part) => part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join("|")})`;
147
- index = end;
148
- }
149
- } else source += char.replace(/[.+^$()|[\]\\]/g, "\\$&");
150
- }
151
- const expression = new RegExp(`^${source}$`);
152
- return (path) => expression.test(path);
153
- }
154
- //#endregion
155
- //#region ../runner/src/snapshot.ts
156
- const GLOB_CHARS = /[*?{]/;
157
- function ignoreRule(pattern) {
158
- const negated = pattern.startsWith("!");
159
- const trimmed = (negated ? pattern.slice(1) : pattern).replace(/^\.\//, "").replace(/\/$/, "");
160
- const match = globMatcher(trimmed);
161
- if (!trimmed.includes("/")) return {
162
- negated,
163
- match: (relativePath) => match(path.posix.basename(relativePath)),
164
- mayMatchBelow: () => true
165
- };
166
- const segments = trimmed.split("/");
167
- const firstGlob = segments.findIndex((segment) => GLOB_CHARS.test(segment));
168
- const prefix = segments.slice(0, firstGlob === -1 ? segments.length : firstGlob).join("/");
169
- return {
170
- negated,
171
- match,
172
- mayMatchBelow: (directory) => prefix === "" || prefix.startsWith(`${directory}/`) || prefix === directory || directory.startsWith(`${prefix}/`)
173
- };
174
- }
175
- /**
176
- * Ignore rules for candidate snapshots, read like `.gitignore`: globs relative
177
- * to the workspace root, where a pattern without `/` matches that name at any
178
- * depth, and later patterns win. `!pattern` re-includes a path; unlike Git, it
179
- * also works below an ignored directory (`_build` then `!_build/doc/**`).
180
- */
181
- function ignoreRules(patterns) {
182
- const rules = patterns.map(ignoreRule);
183
- return {
184
- /** Whether a path is ignored, given whether its directory was. */
185
- ignored(relativePath, parentIgnored = false) {
186
- let ignored = parentIgnored;
187
- for (const rule of rules) if (rule.match(relativePath)) ignored = !rule.negated;
188
- return ignored;
189
- },
190
- /** Whether an ignored directory must still be walked for re-included paths. */
191
- mayReinclude(directory) {
192
- return rules.some((rule) => rule.negated && rule.mayMatchBelow(directory));
193
- }
194
- };
195
- }
196
- /**
197
- * Copies the candidate workspace's regular files into the run report,
198
- * leaving out ignored paths. Symbolic links and special files are skipped,
199
- * never followed, so a snapshot cannot read outside the run. An ignored
200
- * directory is walked only when a `!` pattern could re-include something in
201
- * it, and is listed only if it ends up containing a snapshotted file.
202
- */
203
- async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAULT_ARTIFACT_EXCLUDE) {
204
- const artifacts = [];
205
- const rules = ignoreRules(ignore);
206
- /** Returns whether anything below `directory` was snapshotted. */
207
- async function visit(directory, relativeDirectory, directoryIgnored) {
208
- const entries = await readdir(directory, { withFileTypes: true });
209
- entries.sort((left, right) => left.name.localeCompare(right.name));
210
- let kept = false;
211
- for (const entry of entries) {
212
- const absolutePath = path.join(directory, entry.name);
213
- const relativePath = path.join(relativeDirectory, entry.name);
214
- const posixPath = relativePath.split(path.sep).join("/");
215
- const ignored = rules.ignored(posixPath, directoryIgnored);
216
- const stat = await lstat(absolutePath);
217
- if (stat.isSymbolicLink()) continue;
218
- if (stat.isDirectory()) {
219
- if (ignored && !rules.mayReinclude(posixPath)) continue;
220
- const entryIndex = artifacts.length;
221
- const keptBelow = await visit(absolutePath, relativePath, ignored);
222
- if (!ignored || keptBelow) {
223
- artifacts.splice(entryIndex, 0, {
224
- path: path.join("candidate", relativePath),
225
- kind: "directory"
226
- });
227
- kept = true;
228
- }
229
- continue;
230
- }
231
- if (ignored || !stat.isFile()) continue;
232
- const data = await readFile(absolutePath);
233
- const artifactPath = path.join("candidate", relativePath);
234
- await writer.writeArtifact(artifactPath, data);
235
- artifacts.push({
236
- path: artifactPath,
237
- kind: "file",
238
- size: data.byteLength
239
- });
240
- kept = true;
241
- }
242
- return kept;
243
- }
244
- await visit(candidateRoot, "", false);
245
- return artifacts;
246
- }
247
- //#endregion
248
- //#region ../runner/src/datasets.ts
249
- const loads = /* @__PURE__ */ new WeakMap();
250
- /**
251
- * Loads a dataset at most once per process and project root. A failed load is
252
- * forgotten, so the next run that needs the dataset tries again.
253
- */
254
- function loadDataset(dataset, root, signal) {
255
- let byRoot = loads.get(dataset);
256
- if (!byRoot) loads.set(dataset, byRoot = /* @__PURE__ */ new Map());
257
- const cached = byRoot.get(root);
258
- if (cached) return cached;
259
- const loading = loadUncached(dataset, root, signal);
260
- byRoot.set(root, loading);
261
- loading.catch(() => {
262
- if (byRoot.get(root) === loading) byRoot.delete(root);
263
- });
264
- return loading;
265
- }
266
- async function loadUncached(dataset, root, signal) {
267
- const { source } = dataset;
268
- if (source.kind === "computed") {
269
- const loaded = await source.onLoad({
270
- root,
271
- signal
272
- });
273
- if (typeof loaded !== "object" || loaded === null) throw new Error(`Dataset ${dataset.id} onLoad must return { files?, value? }`);
274
- return loaded;
275
- }
276
- return {
277
- ...source.files !== void 0 ? { files: source.files } : {},
278
- ...source.valueFile !== void 0 ? { value: await readValueFile(dataset.id, path.resolve(root, source.valueFile)) } : source.value !== void 0 ? { value: source.value } : {}
279
- };
280
- }
281
- /** `.json` is one value; `.jsonl` is an array of one JSON object per non-blank line. */
282
- async function readValueFile(id, file) {
283
- const text = await readFile(file, "utf8").catch((cause) => {
284
- throw new Error(`Dataset ${id} cannot read ${file}: ${String(cause)}`);
285
- });
286
- if (file.endsWith(".jsonl")) return text.split("\n").flatMap((line, index) => {
287
- if (!line.trim()) return [];
288
- try {
289
- const row = JSON.parse(line);
290
- if (typeof row !== "object" || row === null || Array.isArray(row)) throw new Error("expected a JSON object");
291
- return [row];
292
- } catch (cause) {
293
- throw new Error(`Dataset ${id} ${file}:${index + 1}: ${cause instanceof Error ? cause.message : String(cause)}`);
294
- }
295
- });
296
- if (file.endsWith(".json")) try {
297
- return JSON.parse(text);
298
- } catch (cause) {
299
- throw new Error(`Dataset ${id} ${file}: ${cause instanceof Error ? cause.message : String(cause)}`);
300
- }
301
- throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
302
- }
303
- /** Loads (cached), derives this run's content with `onRun`, and resolves it. */
304
- async function prepareDatasets(datasets, root, run) {
305
- const prepared = [];
306
- for (const dataset of datasets) {
307
- const loaded = await loadDataset(dataset, root, run.signal);
308
- const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, run) : loaded;
309
- if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
310
- prepared.push(await resolveContent(dataset, content, root));
311
- }
312
- return prepared;
313
- }
314
- async function resolveContent(dataset, content, root) {
315
- const label = `Dataset ${dataset.id}`;
316
- const value = content.value === void 0 ? void 0 : toJson(content.value, `datasets.${dataset.id}.value`);
317
- let files = { kind: "none" };
318
- if (typeof content.files === "string") {
319
- const source = path.resolve(root, content.files);
320
- if (!(await stat(source).catch(() => void 0))?.isDirectory()) throw new Error(`${label} files directory does not exist: ${content.files}`);
321
- files = {
322
- kind: "directory",
323
- source
324
- };
325
- } else if (content.files !== void 0) files = {
326
- kind: "entries",
327
- entries: Object.entries(content.files).map(([file, source]) => preparedFile(relativeDatasetPath(file, `${label} file`), source, root, label))
328
- };
329
- return {
330
- dataset,
331
- files,
332
- ...value === void 0 ? {} : { value },
333
- digest: await digest(files, value)
334
- };
335
- }
336
- function preparedFile(file, source, root, label) {
337
- if (typeof source === "string") return {
338
- path: file,
339
- kind: "bytes",
340
- data: new TextEncoder().encode(source)
341
- };
342
- if (source instanceof Uint8Array) return {
343
- path: file,
344
- kind: "bytes",
345
- data: source
346
- };
347
- if (typeof source === "object" && source !== null && typeof source.file === "string") return {
348
- path: file,
349
- kind: "file",
350
- source: path.resolve(root, source.file)
351
- };
352
- throw new Error(`${label} file ${file} must be a string, bytes, or { file: 'path' }`);
353
- }
354
- /** Lists a directory's files as sorted relative POSIX paths. */
355
- async function listFiles(directory, prefix = "") {
356
- const entries = await readdir(path.join(directory, prefix), { withFileTypes: true });
357
- const files = [];
358
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
359
- const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
360
- if (entry.isDirectory()) files.push(...await listFiles(directory, relative));
361
- else if (entry.isFile()) files.push(relative);
362
- }
363
- return files;
364
- }
365
- async function digest(files, value) {
366
- const hash = createHash("sha256");
367
- const addFile = (file, data) => {
368
- hash.update(`file ${file} ${data.byteLength}\n`);
369
- hash.update(data);
370
- };
371
- if (files.kind === "directory") for (const file of await listFiles(files.source)) addFile(file, await readFile(path.join(files.source, file)));
372
- 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));
373
- if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
374
- return `sha256:${hash.digest("hex")}`;
375
- }
376
- function runDatasets(prepared) {
377
- return prepared.map(({ dataset, digest }) => ({
378
- id: dataset.id,
379
- visibility: dataset.visibility,
380
- source: dataset.source.kind,
381
- digest
382
- }));
383
- }
384
- /**
385
- * The files a static dataset places in every workspace, relative to its
386
- * mount. Computed datasets return `undefined`: their files only exist once
387
- * `onLoad` runs, and browsing must never run project code.
388
- */
389
- async function staticDatasetFiles(dataset, root) {
390
- const { source } = dataset;
391
- if (source.kind === "computed") return void 0;
392
- if (source.files === void 0) return [];
393
- if (typeof source.files === "string") {
394
- const directory = path.resolve(root, source.files);
395
- return Promise.all((await listFiles(directory)).map(async (file) => ({
396
- path: file,
397
- size: (await stat(path.join(directory, file))).size
398
- })));
399
- }
400
- return Promise.all(Object.entries(source.files).sort(([a], [b]) => a.localeCompare(b)).map(async ([file, content]) => ({
401
- path: file,
402
- size: typeof content === "string" ? new TextEncoder().encode(content).byteLength : content instanceof Uint8Array ? content.byteLength : (await stat(path.resolve(root, content.file))).size
403
- })));
404
- }
405
- /** One file of a static dataset, by its path below the mount. */
406
- async function staticDatasetFile(dataset, root, file) {
407
- const { source } = dataset;
408
- const missing = () => /* @__PURE__ */ new Error(`Dataset ${dataset.id} has no file ${JSON.stringify(file)}`);
409
- if (source.kind === "computed" || source.files === void 0) throw missing();
410
- if (typeof source.files === "string") {
411
- const directory = path.resolve(root, source.files);
412
- const resolved = path.resolve(directory, file);
413
- if (!resolved.startsWith(`${directory}${path.sep}`)) throw missing();
414
- if (!(await stat(resolved).catch(() => void 0))?.isFile()) throw missing();
415
- return readFile(resolved);
416
- }
417
- if (!Object.hasOwn(source.files, file)) throw missing();
418
- const content = source.files[file];
419
- if (typeof content === "string") return new TextEncoder().encode(content);
420
- if (content instanceof Uint8Array) return new Uint8Array(content);
421
- return readFile(path.resolve(root, content.file));
422
- }
423
- //#endregion
424
- //#region ../runner/src/workspace.ts
425
- /** Where a run's persistent workspace lives below `root`. */
426
- function runWorkspacePath(root, runId) {
427
- return path.join(path.resolve(root), runId.replace(/[^a-zA-Z0-9._-]/g, "_"));
428
- }
429
- /**
430
- * Creates isolated candidate and evaluator directories and writes each
431
- * dataset's files below its mount, on the side its visibility allows.
432
- * Two datasets writing the same file is an error, never an overwrite.
433
- */
434
- async function createRunWorkspace(runId, datasets = [], persistentRoot) {
435
- const root = persistentRoot ? runWorkspacePath(persistentRoot, runId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-run-"));
436
- const persistent = persistentRoot !== void 0;
437
- const artifacts = {
438
- candidate: { root: path.join(root, "candidate") },
439
- evaluator: { root: path.join(root, "evaluator") }
440
- };
441
- try {
442
- await Promise.all([mkdir(artifacts.candidate.root, { recursive: true }), mkdir(artifacts.evaluator.root, { recursive: true })]);
443
- for (const prepared of datasets) {
444
- const side = prepared.dataset.visibility === "candidate" ? artifacts.candidate.root : artifacts.evaluator.root;
445
- await writeDatasetFiles(prepared, path.join(side, prepared.dataset.mount));
446
- }
447
- return {
448
- artifacts,
449
- cleanup: async (keep = false) => {
450
- if (persistent && keep) return;
451
- await rm(root, {
452
- recursive: true,
453
- force: true
454
- });
455
- }
456
- };
457
- } catch (error) {
458
- await rm(root, {
459
- recursive: true,
460
- force: true
461
- });
462
- throw error;
463
- }
464
- }
465
- async function writeDatasetFiles(prepared, target) {
466
- const { files, dataset } = prepared;
467
- if (files.kind === "none") return;
468
- await mkdir(target, { recursive: true });
469
- const entries = files.kind === "directory" ? (await listFiles(files.source)).map((file) => ({
470
- path: file,
471
- kind: "file",
472
- source: path.join(files.source, file)
473
- })) : files.entries;
474
- for (const entry of entries) await writeEntry(dataset.id, target, entry);
475
- }
476
- async function writeEntry(datasetId, target, entry) {
477
- const destination = path.join(target, ...entry.path.split("/"));
478
- await mkdir(path.dirname(destination), { recursive: true });
479
- await (entry.kind === "bytes" ? writeFile(destination, entry.data, { flag: "wx" }) : copyFile(entry.source, destination, 1)).catch((cause) => {
480
- throw collision(datasetId, entry.path, cause);
481
- });
482
- }
483
- function collision(datasetId, file, cause) {
484
- return cause.code === "EEXIST" ? /* @__PURE__ */ new Error(`Dataset ${datasetId} would overwrite ${file}, which another dataset already wrote`) : cause instanceof Error ? cause : new Error(String(cause));
485
- }
486
- //#endregion
487
- //#region ../runner/src/checkpoint-lookup.ts
488
- /**
489
- * `checkpoint(rule)` for a scoring context: the latest result of the steps
490
- * that are this very rule. A rule outside the transcript is a mistake in the
491
- * scenario, so it throws rather than reading as "not run".
492
- */
493
- function checkpointLookup(transcript, results) {
494
- return ((rule) => {
495
- const steps = new Set(transcript.flatMap((step, index) => step === rule ? [index] : []));
496
- if (!steps.size) throw new Error(`checkpoint(${JSON.stringify(rule.name)}) is not a checkpoint in this scenario's transcript; pass the same rule object the transcript uses`);
497
- for (let index = results.length - 1; index >= 0; index--) if (steps.has(results[index].step)) return results[index];
498
- });
499
- }
500
- //#endregion
501
- //#region ../runner/src/score.ts
502
- const inUnitInterval = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
503
- /**
504
- * Criteria must be well formed before they can stand for the score. With
505
- * declared criteria, each takes its declared weight unless it sets one, a
506
- * declared one left out is not reached, and an undeclared one is an error,
507
- * so the checks shown before a run are the checks recorded after it.
508
- */
509
- function checkedCriteria(criteria, declared) {
510
- if (!Array.isArray(criteria) || !criteria.length && !declared?.length) throw new Error("criteria must be a non-empty array");
511
- const seen = /* @__PURE__ */ new Set();
512
- const results = criteria.map((entry, index) => {
513
- if (typeof entry?.name !== "string" || !entry.name.trim()) throw new Error(`criteria[${index}] needs a name`);
514
- const { name } = entry;
515
- if (seen.has(name)) throw new Error(`Criterion ${name} is listed twice`);
516
- seen.add(name);
517
- const declaration = declared?.find((criterion) => criterion.name === name);
518
- if (declared && !declaration) throw new Error(`Criterion ${name} is not declared; declared: ${declared.map((criterion) => criterion.name).join(", ")}`);
519
- const weight = entry.weight ?? declaration?.weight;
520
- if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) throw new Error(`Criterion ${name} weight must be a positive number; received ${String(weight)}`);
521
- const explanation = entry.explanation;
522
- if (explanation !== void 0 && typeof explanation !== "string") throw new Error(`Criterion ${name} explanation must be a string`);
523
- const common = {
524
- name,
525
- ...weight !== void 0 ? { weight } : {},
526
- ...explanation?.trim() ? { explanation: explanation.trim() } : {}
527
- };
528
- if ("reached" in entry) {
529
- if (entry.reached !== false) throw new Error(`Criterion ${name}: reached can only be false; leave it out for a check that ran`);
530
- return {
531
- ...common,
532
- reached: false
533
- };
534
- }
535
- const value = entry.value;
536
- if (!inUnitInterval(value)) throw new Error(`Criterion ${name} value must be a number from 0 to 1; received ${String(value)}`);
537
- const evidence = entry.evidence;
538
- return {
539
- ...common,
540
- value,
541
- ...evidence !== void 0 ? { evidence: toJson(evidence, `criteria.${name}.evidence`) } : {}
542
- };
543
- });
544
- const missing = (declared ?? []).filter((criterion) => !seen.has(criterion.name)).map((criterion) => ({
545
- name: criterion.name,
546
- weight: criterion.weight,
547
- reached: false
548
- }));
549
- return [...results, ...missing];
550
- }
551
- /**
552
- * Shared normalization for inline and final predicate and judge rules.
553
- * Without a `value`, the weighted mean of `criteria` is the value. `passAt`
554
- * turns the value into a verdict, and is recorded with it; an explicit
555
- * `passed` wins.
556
- */
557
- function normalizeScore(value, passAt, declared) {
558
- if (typeof value === "object" && value !== null && "skipped" in value && value.skipped === true) return {
559
- skipped: true,
560
- explanation: value.reason
561
- };
562
- const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
563
- if (!normalized || typeof normalized !== "object") throw new Error(`A scorer must return a boolean, a number from 0 to 1, or a score object; received ${String(value)}`);
564
- const criteria = normalized.criteria === void 0 ? void 0 : checkedCriteria(normalized.criteria, declared);
565
- const score = normalized.value ?? (criteria ? weightedMean(criteria) : void 0);
566
- if (!inUnitInterval(score)) throw new Error(`Score value must be a finite number from 0 to 1; received ${String(score)}`);
567
- if (normalized.judge?.usage) {
568
- const { usage } = normalized.judge;
569
- if (Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
570
- }
571
- const mark = passAt ?? 1;
572
- return {
573
- value: score,
574
- passed: normalized.passed ?? score >= mark,
575
- passAt: mark,
576
- ...normalized.explanation ? { explanation: normalized.explanation } : {},
577
- ...normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
578
- ...normalized.choice ? { choice: normalized.choice } : {},
579
- ...criteria ? { criteria } : {},
580
- ...normalized.judge !== void 0 ? { judge: normalized.judge } : {}
581
- };
582
- }
583
- //#endregion
584
- //#region ../runner/src/judge-agent.ts
585
- const failure$2 = (name, cause) => new RuleExecutionError({
586
- name,
587
- cause,
588
- message: cause instanceof Error ? cause.message : String(cause)
589
- });
590
- /** A long tool result or reply is cut to this many characters in a judge's conversation. */
591
- const MAX_PART = 4e3;
592
- /**
593
- * Drives one judge agent session for one assessment: builds the prompt from
594
- * the rule, sends it, reads the answer in the rule's format, and asks once
595
- * more if it cannot. Its events stay out of the AUT trajectory.
596
- */
597
- function runJudgeAgent(rule, agent, input, placement) {
598
- return Effect.tryPromise({
599
- try: () => judgeOnce(rule, agent, input, placement),
600
- catch: (cause) => cause instanceof RuleExecutionError ? cause : failure$2(rule.name, cause)
601
- });
602
- }
603
- async function judgeOnce(rule, agent, input, placement) {
604
- const inputs = judgeInputNames(rule);
605
- const values = await inputValues(rule, input, placement);
606
- const prompt = judgePrompt({
607
- rubric: rule.rubric,
608
- answer: rule.answer,
609
- inputs,
610
- values
611
- });
612
- const events = [];
613
- const context = {
614
- ...input.context,
615
- data: input.data,
616
- workspace: input.artifacts.evaluator,
617
- evaluatorWorkspace: input.artifacts.evaluator
618
- };
619
- const session = await agent.start({
620
- context,
621
- attach: input.attach,
622
- onEvent: (event) => {
623
- const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
624
- ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
625
- source: "aut"
626
- });
627
- if (observed.source !== "aut") throw new Error("Invalid judge event source");
628
- const { source: _, ...validated } = observed;
629
- events.push(validated);
630
- }
631
- });
632
- let reading;
633
- let reply;
634
- let attempts = 0;
635
- try {
636
- let message = prompt;
637
- let problem = "";
638
- while (attempts < 2 && !reading) {
639
- attempts += 1;
640
- const from = events.length;
641
- const result = await session.send(message);
642
- if (result !== void 0) events.push(...turnResultEvents(result, attempts, () => /* @__PURE__ */ new Date()));
643
- reply = replyIn(events.slice(from));
644
- if (reply === void 0) problem = "it did not reply";
645
- else try {
646
- reading = readJudgeAnswer(rule.answer, reply);
647
- } catch (error) {
648
- if (!(error instanceof UnreadableAnswer)) throw error;
649
- problem = error.message;
650
- }
651
- message = `Your answer could not be read: ${problem}. Answer again, only in this format.\n\n${answerInstruction(rule.answer)}`;
652
- }
653
- if (!reading) throw new Error(`Judge ${rule.name}'s answer could not be read after ${attempts} tries: ${problem}${reply === void 0 ? "" : `. Its last reply: ${preview(reply)}`}`);
654
- } finally {
655
- await session.close?.();
656
- }
657
- const recorded = Schema.decodeUnknownSync(JsonValueSchema)(events);
658
- const model = lastOf(events, (event) => event.kind === "turn-completed" && event.model);
659
- const usage = totalUsage(events);
660
- const judge = {
661
- ...agent.identity ? { agent: agent.identity } : {},
662
- ...model?.kind === "turn-completed" && model.model ? { model: model.model } : {},
663
- ...usage ? { usage } : {},
664
- prompt,
665
- ...reply !== void 0 ? { reply } : {},
666
- ...attempts > 1 ? { attempts } : {},
667
- events: recorded
668
- };
669
- return { value: {
670
- ...reading,
671
- judge
672
- } };
673
- }
674
- /** Each input's text: what the rule's function returned, or the defaults. */
675
- async function inputValues(rule, input, placement) {
676
- if (!rule.inputs) return {
677
- conversation: conversationText(input.trajectory.events, placement),
678
- reply: text(input.output)
679
- };
680
- const values = {};
681
- for (const [name, read] of Object.entries(rule.inputs)) values[name] = text(await read(input));
682
- return values;
683
- }
684
- /** The run's conversation as plain lines, for a judge to read: this turn at a checkpoint, else all of it. */
685
- function conversationText(events, placement) {
686
- let start = 0;
687
- if (placement === "transcript") events.forEach((event, index) => {
688
- if (event.kind === "transcript-step-started" && "message" in event) start = index;
689
- });
690
- const lines = [];
691
- for (const event of events.slice(start)) if (event.kind === "transcript-step-started" && "message" in event) lines.push(`User: ${cut(text(event.message))}`);
692
- else if (event.source !== "aut") continue;
693
- else if (event.kind === "message") lines.push(`${capitalize(event.role)}: ${cut(text(event.content))}`);
694
- else if (event.kind === "tool-call") lines.push(`Tool call ${event.name}: ${cut(text(event.arguments))}`);
695
- else if (event.kind === "tool-result") lines.push(`Tool result ${event.name ?? event.id}: ${cut(text(event.result))}`);
696
- return lines.join("\n") || "(nothing happened)";
697
- }
698
- /** The judge's answer in one attempt's events: its final output, else its last reply. */
699
- function replyIn(events) {
700
- const completed = lastOf(events, (event) => event.kind === "completed" && event.output !== void 0);
701
- if (completed?.kind === "completed") return completed.output;
702
- const assistant = lastOf(events, (event) => event.kind === "message" && event.role === "assistant");
703
- return assistant?.kind === "message" ? assistant.content : void 0;
704
- }
705
- function totalUsage(events) {
706
- let total;
707
- for (const event of events) {
708
- if (event.kind !== "turn-completed" || !event.usage) continue;
709
- total ??= {};
710
- for (const [key, tokens] of Object.entries(event.usage)) if (tokens !== void 0) total[key] = (total[key] ?? 0) + tokens;
711
- }
712
- return total;
713
- }
714
- function text(value) {
715
- if (value === void 0) return "(none)";
716
- return typeof value === "string" ? value : JSON.stringify(value, null, 2);
717
- }
718
- function cut(value) {
719
- return value.length > MAX_PART ? `${value.slice(0, MAX_PART)}… (${value.length - MAX_PART} more characters)` : value;
720
- }
721
- function preview(value) {
722
- const shown = text(value).replace(/\s+/g, " ");
723
- return shown.length > 300 ? `${shown.slice(0, 300)}…` : shown;
724
- }
725
- /** The last element matching, scanning from the end (no ES2023 lib here). */
726
- function lastOf(items, match) {
727
- for (let index = items.length - 1; index >= 0; index -= 1) if (match(items[index])) return items[index];
728
- }
729
- const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
730
- //#endregion
731
- //#region ../runner/src/evaluate-rule.ts
732
- const failure$1 = (name, cause) => new RuleExecutionError({
733
- name,
734
- cause,
735
- message: cause instanceof Error ? cause.message : String(cause)
736
- });
737
- /** Both placements call the same rule executor; only input timing and persistence differ. */
738
- function evaluateRule(rule, input, defaultJudge, placement) {
739
- return Effect.gen(function* () {
740
- const judgeAgent = rule.kind === "judge" ? rule.agent ?? defaultJudge : void 0;
741
- if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("This judge has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts")));
742
- const evaluated = rule.kind === "predicate" ? yield* Effect.tryPromise({
743
- try: async () => rule.run(input),
744
- catch: (cause) => failure$1(rule.name, cause)
745
- }) : (yield* runJudgeAgent(rule, judgeAgent, input, placement)).value;
746
- const score = yield* Effect.try({
747
- try: () => normalizeScore(evaluated, rule.passAt, rule.kind === "predicate" ? rule.criteria : void 0),
748
- catch: (cause) => failure$1(rule.name, cause)
749
- });
750
- const validated = yield* Schema.decodeUnknown(ScoreResultSchema)({
751
- name: rule.name,
752
- kind: rule.kind,
753
- durationMs: 0,
754
- ...score
755
- }).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
756
- if (validated.skipped) return {
757
- skipped: true,
758
- ...validated.explanation === void 0 ? {} : { explanation: validated.explanation }
759
- };
760
- const { name: _name, kind: _kind, durationMs: _duration, ...result } = validated;
761
- return result;
762
- });
763
- }
764
- //#endregion
765
- //#region ../runner/src/turn.ts
766
- /** Freeze the observation window at send() completion, before checkpoint events arrive. */
767
- function turnView(events, start, end, userStepIndex) {
768
- const window = events.slice(start, end);
769
- const autEvents = window.filter((event) => event.source === "aut");
770
- const assistantMessages = autEvents.filter((event) => event.kind === "message" && event.role === "assistant");
771
- const calls = window.flatMap((event, index) => event.source === "aut" && event.kind === "tool-call" ? [{
772
- event,
773
- eventIndex: start + index
774
- }] : []);
775
- const results = autEvents.filter((event) => event.kind === "tool-result");
776
- return {
777
- userStepIndex,
778
- events: autEvents,
779
- assistantMessages,
780
- toolCalls: calls.map(({ event, eventIndex }) => {
781
- const matches = results.filter((result) => result.id === event.id);
782
- const ambiguous = calls.filter((call) => call.event.id === event.id).length !== 1 || matches.length > 1;
783
- return {
784
- eventIndex,
785
- id: event.id,
786
- name: event.name,
787
- arguments: event.arguments,
788
- resultObservation: ambiguous ? "ambiguous" : matches.length ? "observed" : "absent",
789
- ...!ambiguous && matches.length ? { result: matches[0].result } : {}
790
- };
791
- }),
792
- ...typeof assistantMessages.at(-1)?.content === "string" ? { lastAssistantText: assistantMessages.at(-1).content } : {}
793
- };
794
- }
795
- /** Exact JSON matching: object keys do not matter, array order does. */
796
- function matchingToolCall(turn, name, args) {
797
- const expected = args === void 0 ? void 0 : canonicalParameters(args);
798
- return turn.toolCalls.find((call) => call.name === name && (expected === void 0 || canonicalParameters(call.arguments) === expected));
799
- }
800
- //#endregion
801
- //#region ../runner/src/checkpoint.ts
802
- const failure = (step, name, cause) => new CheckpointExecutionError({
803
- step,
804
- name,
805
- cause,
806
- message: cause instanceof Error ? cause.message : String(cause)
807
- });
808
- /** An assertion failure is a successful Effect containing a failed result; only execution errors enter the error channel. */
809
- function evaluateCheckpoint(step, index, input, now, judgeAgent) {
810
- const annotations = {
811
- evalId: input.context.evalId,
812
- runId: input.context.runId,
813
- step: index,
814
- checkpoint: step.name
815
- };
816
- return Effect.gen(function* () {
817
- const started = now();
818
- yield* Effect.logDebug("checkpoint started");
819
- const match = step.kind === "expect-tool-call" ? yield* Effect.try({
820
- try: () => matchingToolCall(input.turn, step.expected.name, step.expected.arguments),
821
- catch: (cause) => failure(index, step.name, cause)
822
- }) : void 0;
823
- const score = step.kind === "expect-tool-call" ? yield* Effect.try({
824
- try: () => normalizeScore(Boolean(match)),
825
- catch: (cause) => failure(index, step.name, cause)
826
- }) : yield* evaluateRule(step, input, judgeAgent, "transcript").pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
827
- const status = score.skipped ? "skipped" : score.passed ? "passed" : "failed";
828
- const { skipped: _, ...recorded } = score;
829
- const result = yield* Schema.decodeUnknown(CheckpointResultSchema)({
830
- step: index,
831
- kind: step.kind,
832
- name: step.name,
833
- status,
834
- durationMs: now().getTime() - started.getTime(),
835
- ...recorded,
836
- ...match ? { matchedToolCall: {
837
- eventIndex: match.eventIndex,
838
- id: match.id
839
- } } : {}
840
- }).pipe(Effect.mapError((cause) => failure(index, step.name, cause)));
841
- yield* Effect.logDebug("checkpoint completed").pipe(Effect.annotateLogs({ status: result.status }));
842
- return {
843
- ...result,
844
- status
845
- };
846
- }).pipe(Effect.tapError((error) => Effect.logDebug("checkpoint execution error").pipe(Effect.annotateLogs({ errorName: error._tag }))), Effect.annotateLogs(annotations));
847
- }
848
- //#endregion
849
- //#region ../runner/src/effect-boundary.ts
850
- /** Keep tagged Effect failures intact when bridging into the legacy async reporting lifecycle. Defects still reject. */
851
- async function runBoundary(effect) {
852
- const outcome = await Effect.runPromise(Effect.either(effect));
853
- if (Either.isLeft(outcome)) throw outcome.left;
854
- return outcome.right;
855
- }
856
- //#endregion
857
- //#region ../runner/src/transcript.ts
858
- async function executeTranscript(definition, session, input) {
859
- const { now, emitRunner, progress } = input;
860
- let turn = 0;
861
- for (const [index, step] of definition.transcript.entries()) {
862
- if (input.signal.aborted) throw cancelled();
863
- const message = step.kind === "user" ? userMessage(step, index, input) : void 0;
864
- await emitRunner({
865
- kind: "transcript-step-started",
866
- step: index,
867
- ...message === void 0 ? {} : { message },
868
- timestamp: now().toISOString()
869
- });
870
- if (message !== void 0) {
871
- const cursor = input.events.length;
872
- turn++;
873
- const result = await sendUserTurn(session, message, input.signal);
874
- if (result !== void 0) for (const event of turnResultEvents(result, turn, now)) await input.emitAut(event);
875
- progress.lastTurn = turnView(input.events, cursor, input.events.length, index);
876
- } else if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") {
877
- const result = await runCheckpoint(definition, step, index, input);
878
- progress.checkpoints.push(result);
879
- await emitRunner({
880
- kind: "checkpoint-completed",
881
- step: index,
882
- status: result.status,
883
- timestamp: now().toISOString()
884
- });
885
- await emitRunner({
886
- kind: "transcript-step-completed",
887
- step: index,
888
- timestamp: now().toISOString()
889
- });
890
- if (result.status === "failed" && definition.policy?.failfast) {
891
- progress.stoppedEarly = true;
892
- await skipRemainingSteps(definition, index, input);
893
- break;
894
- }
895
- continue;
896
- } else throw new Error(`Transcript step kind "${step.kind}" is not executable yet`);
897
- await emitRunner({
898
- kind: "transcript-step-completed",
899
- step: index,
900
- timestamp: now().toISOString()
901
- });
902
- }
903
- }
904
- /** A computed message sees the parameters and candidate data the agent also sees. */
905
- function userMessage(step, index, { context, randomSeed }) {
906
- if (typeof step.message === "string") return step.message;
907
- const message = step.message({
908
- parameters: context.parameters ?? {},
909
- data: context.data,
910
- seed: randomSeed,
911
- ...context.batch ? { batch: context.batch } : {}
912
- });
913
- if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
914
- return message;
915
- }
916
- async function sendUserTurn(session, message, signal) {
917
- return runBoundary(Effect.tryPromise({
918
- try: () => unlessAborted(() => session.send(message), signal),
919
- catch: (cause) => cause instanceof ReportError || cause instanceof CancelledError ? cause : new AutExecutionError({
920
- cause,
921
- message: cause instanceof Error ? cause.message : String(cause)
922
- })
923
- }));
924
- }
925
- async function runCheckpoint(definition, step, index, input) {
926
- const { now, emitRunner, progress } = input;
927
- if (!progress.lastTurn) throw new Error("Checkpoint requires a preceding user step");
928
- const started = now();
929
- await emitRunner({
930
- kind: "checkpoint-started",
931
- step: index,
932
- name: step.name,
933
- timestamp: started.toISOString()
934
- });
935
- const tracked = trackAttachments(input.attach);
936
- return runBoundary(evaluateCheckpoint(step, index, {
937
- context: input.context,
938
- artifacts: input.artifacts,
939
- trajectory: { events: input.events },
940
- turn: progress.lastTurn,
941
- ...turnOutput(progress.lastTurn),
942
- data: input.data,
943
- checkpoints: [...progress.checkpoints],
944
- checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
945
- attach: tracked.attach
946
- }, now, input.defaultJudge)).then((result) => ({
947
- ...result,
948
- ...attachedFields(tracked.attached)
949
- })).catch(async (error) => {
950
- const recorded = recordError(error);
951
- progress.checkpoints.push({
952
- step: index,
953
- kind: step.kind,
954
- name: step.name,
955
- status: "error",
956
- durationMs: now().getTime() - started.getTime(),
957
- error: recorded,
958
- ...attachedFields(tracked.attached)
959
- });
960
- try {
961
- await emitRunner({
962
- kind: "checkpoint-error",
963
- step: index,
964
- error: recorded,
965
- timestamp: now().toISOString()
966
- });
967
- } catch {}
968
- throw error;
969
- });
970
- }
971
- async function skipRemainingSteps(definition, after, { now, emitRunner, progress }) {
972
- for (let index = after + 1; index < definition.transcript.length; index++) {
973
- await emitRunner({
974
- kind: "transcript-step-skipped",
975
- step: index,
976
- timestamp: now().toISOString()
977
- });
978
- const step = definition.transcript[index];
979
- if (step.kind === "predicate" || step.kind === "judge" || step.kind === "expect-tool-call") progress.checkpoints.push({
980
- step: index,
981
- kind: step.kind,
982
- name: step.name,
983
- status: "skipped"
984
- });
985
- }
986
- }
987
- /** The scorer-facing `output`: the content of the turn's last assistant message. */
988
- function turnOutput(turn) {
989
- const last = turn?.assistantMessages.at(-1);
990
- return last === void 0 ? {} : { output: last.content };
991
- }
992
- //#endregion
993
- //#region ../runner/src/final-scoring.ts
994
- function scoreSummary(results, checkpoints, skippedScorers, metrics = []) {
995
- const valid = results.filter((result) => result.value !== void 0 && !result.skipped);
996
- const hasError = results.some((result) => result.error !== void 0);
997
- return {
998
- results,
999
- ...metrics.length ? { metrics } : {},
1000
- ...checkpoints.length ? { checkpoints } : {},
1001
- ...skippedScorers.length ? { skippedScorers } : {},
1002
- ...valid.length === 0 ? {} : { overall: valid.reduce((sum, result) => sum + (result.value ?? 0), 0) / valid.length },
1003
- passed: !hasError && results.every((result) => result.skipped || result.passed === true) && checkpoints.every((result) => result.status === "passed" || result.status === "skipped") && !skippedScorers.length
1004
- };
1005
- }
1006
- /** What final scorers and metrics receive. */
1007
- function scoringContext(input) {
1008
- return {
1009
- context: input.context,
1010
- artifacts: input.workspace.artifacts,
1011
- trajectory: { events: input.events },
1012
- ...input.lastTurn ? { turn: input.lastTurn } : {},
1013
- ...turnOutput(input.lastTurn),
1014
- data: input.data,
1015
- checkpoints: input.checkpoints,
1016
- checkpoint: checkpointLookup(input.transcript, input.checkpoints),
1017
- attach: input.attach
1018
- };
1019
- }
1020
- /** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
1021
- async function evaluateFinalScorers(definition, input) {
1022
- const { context, workspace, results, skippedScorers, now, emitRunner } = input;
1023
- for (const scorer of definition.scoring) {
1024
- if (input.cancelled || input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
1025
- skippedScorers.push(scorer.name);
1026
- continue;
1027
- }
1028
- const started = now();
1029
- await emitRunner({
1030
- kind: "scorer-started",
1031
- scorer: scorer.name,
1032
- timestamp: started.toISOString()
1033
- });
1034
- let result;
1035
- const tracked = trackAttachments(input.attach);
1036
- try {
1037
- const score = await runBoundary(evaluateRule(scorer, scoringContext({
1038
- ...input,
1039
- attach: tracked.attach,
1040
- context,
1041
- workspace,
1042
- transcript: definition.transcript
1043
- }), input.defaultJudge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
1044
- cause,
1045
- message: cause.message
1046
- }))));
1047
- result = {
1048
- name: scorer.name,
1049
- kind: scorer.kind,
1050
- durationMs: now().getTime() - started.getTime(),
1051
- ...score,
1052
- ...attachedFields(tracked.attached)
1053
- };
1054
- results.push(result);
1055
- } catch (error) {
1056
- const recorded = recordError(error);
1057
- results.push({
1058
- name: scorer.name,
1059
- kind: scorer.kind,
1060
- durationMs: now().getTime() - started.getTime(),
1061
- error: recorded,
1062
- passed: false,
1063
- ...attachedFields(tracked.attached)
1064
- });
1065
- await emitRunner({
1066
- kind: "scorer-failed",
1067
- scorer: scorer.name,
1068
- error: recorded,
1069
- timestamp: now().toISOString()
1070
- });
1071
- continue;
1072
- }
1073
- await emitRunner({
1074
- kind: "scorer-completed",
1075
- scorer: scorer.name,
1076
- ...result.skipped ? { skipped: true } : { value: result.value ?? 0 },
1077
- timestamp: now().toISOString()
1078
- });
1079
- }
1080
- }
1081
- //#endregion
1082
- //#region ../runner/src/metrics.ts
1083
- /**
1084
- * What EvalKit measures for every run: from its trajectory, the agent's time
1085
- * (first event to the last one before final scoring), tokens when the agent
1086
- * reported usage, and tool calls; from its scores and checkpoints, the
1087
- * tokens its judges used, kept apart from the agent's.
1088
- */
1089
- function builtInMetrics(events, verdicts = []) {
1090
- const times = events.filter((event) => !event.kind.startsWith("scorer-")).map((event) => Date.parse(event.timestamp)).filter((time) => !Number.isNaN(time));
1091
- const metrics = [];
1092
- if (times.length) metrics.push({
1093
- name: "agent time",
1094
- value: Math.max(...times) - Math.min(...times),
1095
- unit: "ms",
1096
- better: "lower",
1097
- builtIn: true
1098
- });
1099
- let tokens;
1100
- for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
1101
- const turn = turnTokens(event.usage);
1102
- if (turn !== void 0) tokens = (tokens ?? 0) + turn;
1103
- }
1104
- if (tokens !== void 0) metrics.push({
1105
- name: "tokens",
1106
- value: tokens,
1107
- unit: "tokens",
1108
- better: "lower",
1109
- builtIn: true
1110
- });
1111
- metrics.push({
1112
- name: "tool calls",
1113
- value: events.filter((event) => event.source === "aut" && event.kind === "tool-call").length,
1114
- builtIn: true
1115
- });
1116
- let judgeTokens;
1117
- for (const verdict of verdicts) {
1118
- const used = verdict.judge?.usage && turnTokens(verdict.judge.usage);
1119
- if (used !== void 0) judgeTokens = (judgeTokens ?? 0) + used;
1120
- }
1121
- if (judgeTokens !== void 0) metrics.push({
1122
- name: "judge tokens",
1123
- value: judgeTokens,
1124
- unit: "tokens",
1125
- better: "lower",
1126
- builtIn: true
1127
- });
1128
- return metrics;
1129
- }
1130
- /** Runs the scenario's metrics; a failing metric records its error and the rest still run. */
1131
- async function evaluateMetrics(definition, context) {
1132
- const results = [];
1133
- for (const entry of definition.metrics ?? []) {
1134
- const shape = {
1135
- name: entry.name,
1136
- ...entry.unit ? { unit: entry.unit } : {},
1137
- ...entry.better ? { better: entry.better } : {}
1138
- };
1139
- try {
1140
- const value = await entry.run(context);
1141
- if (value !== void 0 && !Number.isFinite(value)) throw new Error(`Metric ${entry.name} must return a finite number or undefined; got ${String(value)}`);
1142
- results.push({
1143
- ...shape,
1144
- ...value === void 0 ? {} : { value }
1145
- });
1146
- } catch (error) {
1147
- results.push({
1148
- ...shape,
1149
- error: recordError(error)
1150
- });
1151
- }
1152
- }
1153
- return results;
1154
- }
1155
- //#endregion
1156
- //#region ../runner/src/local-report-store.ts
1157
- const MANIFEST_FILE = "manifest.json";
1158
- /** The project's evals and suites when the run started. */
1159
- const CATALOG_FILE = "catalog.json";
1160
- async function writeJson(filePath, value) {
1161
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
1162
- }
1163
- async function replaceJson(filePath, value) {
1164
- const temporary = `${filePath}.${randomUUID()}.tmp`;
1165
- await writeJson(temporary, value);
1166
- await rename(temporary, filePath);
1167
- }
1168
- function assertPathSegment(value, name) {
1169
- if (!value || value === "." || value === ".." || value.includes("/") || value.includes("\\")) throw new Error(`${name} must be a single relative path segment`);
1170
- }
1171
- function artifactPath(root, value) {
1172
- if (!value || path.isAbsolute(value)) throw new Error(`Artifact path must be a non-empty relative path: ${value}`);
1173
- const normalized = path.normalize(value);
1174
- if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
1175
- return path.join(root, normalized);
1176
- }
1177
- var LocalRunWriter = class {
1178
- location;
1179
- directory;
1180
- metadata;
1181
- constructor(location, directory, metadata) {
1182
- this.location = location;
1183
- this.directory = directory;
1184
- this.metadata = metadata;
1185
- }
1186
- appendEvent(event) {
1187
- return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
1188
- }
1189
- writeScores(scoring) {
1190
- return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
1191
- }
1192
- async writeArtifact(relativePath, data) {
1193
- const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
1194
- await mkdir(path.dirname(target), { recursive: true });
1195
- await writeFile(target, data);
1196
- }
1197
- async finalize(summary) {
1198
- await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
1199
- await replaceJson(path.join(this.directory, MANIFEST_FILE), {
1200
- ...Schema.encodeSync(RunMetadataSchema)(this.metadata),
1201
- status: summary.status
1202
- });
1203
- }
1204
- };
1205
- /** Persists each run as a local tree rooted below `rootDirectory`. */
1206
- function localReportStore(rootDirectory) {
1207
- return { async startRun(metadata, context) {
1208
- assertPathSegment(metadata.runId, "runId");
1209
- const directory = path.resolve(rootDirectory, metadata.runId);
1210
- await mkdir(directory, { recursive: true });
1211
- if (context?.catalog) await writeJson(path.join(directory, CATALOG_FILE), context.catalog);
1212
- await writeJson(path.join(directory, MANIFEST_FILE), {
1213
- ...Schema.encodeSync(RunMetadataSchema)(metadata),
1214
- status: "running"
1215
- });
1216
- return new LocalRunWriter(path.relative(process.cwd(), directory), directory, metadata);
1217
- } };
1218
- }
1219
- //#endregion
1220
- //#region ../runner/src/local-report-reader.ts
1221
- /** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
1222
- async function decode(file, schema) {
1223
- return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
1224
- }
1225
- const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
1226
- const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
1227
- const readRunScoring = (root, runId) => decode(join(root, runId, "scoring.json"), RunScoringSchema);
1228
- async function readRunEvents(root, runId) {
1229
- return parseTrajectoryJsonl(await readFile(join(root, runId, "trajectory.jsonl"), "utf8"));
1230
- }
1231
- /**
1232
- * True when a run's owning process is known to be gone: it ran on this host
1233
- * and its PID no longer exists. Runs from other hosts are never judged.
1234
- */
1235
- function ownerIsGone(owner) {
1236
- if (!owner || owner.hostname !== os.hostname()) return false;
1237
- if (owner.pid === process.pid) return false;
1238
- try {
1239
- process.kill(owner.pid, 0);
1240
- return false;
1241
- } catch (error) {
1242
- return error.code === "ESRCH";
1243
- }
1244
- }
1245
- //#endregion
1246
- //#region ../runner/src/matrix.ts
1247
- /** Bounded workers pull cells from the iterator; no Cartesian job array is allocated. */
1248
- function runMatrix(matrix, options) {
1249
- return Effect.gen(function* () {
1250
- const concurrency = options.concurrency ?? 4;
1251
- if (!Number.isSafeInteger(concurrency) || concurrency < 1) throw new Error("Concurrency must be a positive integer");
1252
- const count = matrix.count(options.selection);
1253
- const iterator = matrix.cells(options.selection);
1254
- const summary = {
1255
- cells: 0,
1256
- runs: 0,
1257
- passed: 0,
1258
- failed: 0,
1259
- cancelled: 0
1260
- };
1261
- yield* Effect.all(Array.from({ length: Math.min(count, concurrency) }, () => Effect.gen(function* () {
1262
- while (!options.signal?.aborted) {
1263
- const next = iterator.next();
1264
- if (next.done) return;
1265
- const cell = next.value;
1266
- const runtime = options.runtime ?? [
1267
- "local",
1268
- "remote",
1269
- "sandbox"
1270
- ].find((name) => agentRuntimes(cell.eval.agent)[name]);
1271
- const suiteId = options.suiteId ?? options.suiteFor?.(cell);
1272
- const runs = yield* runEval(cell.eval, {
1273
- ...options,
1274
- ...suiteId ? { suiteId } : {},
1275
- concurrency: 1,
1276
- runtime,
1277
- parameters: cell.parameters,
1278
- matrix: {
1279
- id: matrix.id,
1280
- cellKey: cell.key
1281
- }
1282
- });
1283
- summary.cells++;
1284
- for (const run of runs) {
1285
- summary.runs++;
1286
- if (run.status === "cancelled") summary.cancelled++;
1287
- else if (run.status === "passed") summary.passed++;
1288
- else summary.failed++;
1289
- }
1290
- if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, runs));
1291
- }
1292
- })), { concurrency });
1293
- return summary;
1294
- });
1295
- }
1296
- //#endregion
1297
- //#region ../runner/src/upgrade.ts
1298
- /** A new run being written. */
1299
- const TEMPORARY = ".upgrade-";
1300
- /** An old run set aside while its replacement, with the same ID, moves in. */
1301
- const RETIRED = ".upgraded-";
1302
- const PER_RUN_FILES = ["archived.json"];
1303
- /**
1304
- * Rewrites results from report schema versions 2 to 5 into version 6, in
1305
- * place (RFD0008). An old run of one trial (most of them) becomes a run with
1306
- * the same ID, so links to it keep working. An old run of several trials
1307
- * becomes runs with the trials' IDs, in a batch with the old run's ID.
1308
- *
1309
- * Each new run is written under a temporary name and renamed into place;
1310
- * artifacts are moved, not copied, as the last step before that rename. An
1311
- * old run is removed only after its new runs exist, and one replaced under
1312
- * the same ID is set aside first, so a crash at any point leaves the old run
1313
- * or complete new ones, and re-running finishes the job. Version 6 runs are
1314
- * left alone.
1315
- */
1316
- async function upgradeReports(root, options = {}) {
1317
- const report = {
1318
- upgraded: 0,
1319
- runs: 0,
1320
- batches: 0,
1321
- running: []
1322
- };
1323
- let entries;
1324
- try {
1325
- entries = await readdir(root);
1326
- } catch {
1327
- return report;
1328
- }
1329
- if (!options.dryRun) await finishReplacements(root, entries);
1330
- for (const entry of entries) {
1331
- if (entry.startsWith(".")) continue;
1332
- const directory = join(root, entry);
1333
- const manifest = await readJson(join(directory, "manifest.json"));
1334
- if (!manifest || !UPGRADABLE_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) continue;
1335
- const summary = await readJson(join(directory, "summary.json"));
1336
- if (!summary && !ownerIsGone(manifest.process)) {
1337
- report.running.push(entry);
1338
- continue;
1339
- }
1340
- const trials = await trialIds(directory);
1341
- report.upgraded += 1;
1342
- report.runs += Math.max(trials.length, 1);
1343
- if (trials.length > 1) report.batches += 1;
1344
- if (options.dryRun) continue;
1345
- await upgradeRun(root, entry, manifest, summary, trials);
1346
- }
1347
- if (!options.dryRun) await removeLeftovers(root);
1348
- return report;
1349
- }
1350
- /**
1351
- * Finishes same-ID replacements a crash interrupted: the old run was set
1352
- * aside, and its replacement may or may not have moved in.
1353
- */
1354
- async function finishReplacements(root, entries) {
1355
- for (const entry of entries) {
1356
- if (!entry.startsWith(RETIRED)) continue;
1357
- const id = entry.slice(10);
1358
- const retired = join(root, entry);
1359
- const directory = join(root, id);
1360
- const replacement = join(root, `${TEMPORARY}${id}`);
1361
- if (!await exists(directory)) await rename(await exists(replacement) ? replacement : retired, directory);
1362
- await rm(retired, {
1363
- recursive: true,
1364
- force: true
1365
- });
1366
- }
1367
- }
1368
- /**
1369
- * Temporary folders nothing will finish. One holding artifacts is kept: it
1370
- * could be the only copy, and an old run would have moved them back.
1371
- */
1372
- async function removeLeftovers(root) {
1373
- for (const entry of await readdir(root)) if (entry.startsWith(TEMPORARY) && !await exists(join(root, entry, "artifacts"))) await rm(join(root, entry), {
1374
- recursive: true,
1375
- force: true
1376
- });
1377
- }
1378
- /** Swaps a fully written replacement in for the old run of the same ID. */
1379
- async function replaceInPlace(root, id, replacement) {
1380
- const retired = join(root, `${RETIRED}${id}`);
1381
- await rename(join(root, id), retired);
1382
- await rename(replacement, join(root, id));
1383
- await rm(retired, {
1384
- recursive: true,
1385
- force: true
1386
- });
1387
- }
1388
- async function upgradeRun(root, oldRunId, runManifest, runSummary, trials) {
1389
- const directory = join(root, oldRunId);
1390
- const catalog = await readJson(join(directory, "catalog.json"));
1391
- const extras = await presentFiles(directory, PER_RUN_FILES);
1392
- if (!trials.length) {
1393
- const target = join(root, `${TEMPORARY}${oldRunId}`);
1394
- await mkdir(target, { recursive: true });
1395
- const status = runSummary ? "cancelled" : "running";
1396
- await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1397
- run: runManifest,
1398
- runId: oldRunId,
1399
- status
1400
- }));
1401
- if (runSummary) await writeJsonFile(join(target, "summary.json"), {
1402
- status,
1403
- endedAt: runSummary.endedAt,
1404
- ...runSummary.durationMs === void 0 ? {} : { durationMs: runSummary.durationMs },
1405
- ...runSummary.error ? { error: runSummary.error } : {}
1406
- });
1407
- await copyShared(directory, target, catalog, extras);
1408
- await replaceInPlace(root, oldRunId, target);
1409
- return;
1410
- }
1411
- const inPlace = trials.length === 1;
1412
- for (const trialId of trials) {
1413
- const runId = inPlace ? oldRunId : trialId;
1414
- const destination = join(root, runId);
1415
- if (!inPlace && await exists(join(destination, "manifest.json"))) continue;
1416
- const source = join(directory, "trials", trialId);
1417
- const trialManifest = await readJson(join(source, "manifest.json")) ?? {};
1418
- const trialSummary = await readJson(join(source, "summary.json"));
1419
- const passed = (trialSummary?.scoring)?.passed;
1420
- const status = upgradeStatus(trialSummary?.status ?? trialManifest.status, passed);
1421
- const index = typeof trialManifest.trialIndex === "number" ? trialManifest.trialIndex : trials.indexOf(trialId);
1422
- const target = join(root, `${TEMPORARY}${runId}`);
1423
- if (!await exists(join(source, "artifacts")) && await exists(join(target, "artifacts"))) await rename(join(target, "artifacts"), join(source, "artifacts"));
1424
- await rm(target, {
1425
- recursive: true,
1426
- force: true
1427
- });
1428
- await mkdir(target, { recursive: true });
1429
- await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
1430
- run: runManifest,
1431
- trial: trialManifest,
1432
- runId,
1433
- ...!inPlace ? { batch: {
1434
- id: oldRunId,
1435
- index,
1436
- size: trials.length
1437
- } } : {},
1438
- status
1439
- }));
1440
- if (trialSummary) await writeJsonFile(join(target, "summary.json"), {
1441
- ...trialSummary,
1442
- status
1443
- });
1444
- const scoring = await readText(join(source, "scoring.json"));
1445
- if (scoring !== void 0) await writeFile(join(target, "scoring.json"), scoring);
1446
- const trajectory = await readText(join(source, "trajectory.jsonl"));
1447
- if (trajectory !== void 0) await writeFile(join(target, "trajectory.jsonl"), upgradeTrajectory(trajectory));
1448
- await copyShared(directory, target, catalog, extras);
1449
- if (await exists(join(source, "artifacts"))) await rename(join(source, "artifacts"), join(target, "artifacts"));
1450
- if (inPlace) await replaceInPlace(root, oldRunId, target);
1451
- else await rename(target, destination);
1452
- }
1453
- if (!inPlace) await rm(directory, {
1454
- recursive: true,
1455
- force: true
1456
- });
1457
- }
1458
- async function copyShared(from, to, catalog, extras) {
1459
- if (catalog) await writeJsonFile(join(to, "catalog.json"), upgradeCatalog(catalog));
1460
- for (const file of extras) await cp(join(from, file), join(to, file));
1461
- }
1462
- async function trialIds(directory) {
1463
- try {
1464
- return (await readdir(join(directory, "trials"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1465
- } catch {
1466
- return [];
1467
- }
1468
- }
1469
- async function presentFiles(directory, names) {
1470
- const present = [];
1471
- for (const name of names) if (await exists(join(directory, name))) present.push(name);
1472
- return present;
1473
- }
1474
- async function exists(path) {
1475
- try {
1476
- await stat(path);
1477
- return true;
1478
- } catch {
1479
- return false;
1480
- }
1481
- }
1482
- async function readText(path) {
1483
- try {
1484
- return await readFile(path, "utf8");
1485
- } catch {
1486
- return;
1487
- }
1488
- }
1489
- async function readJson(path) {
1490
- const text = await readText(path);
1491
- if (text === void 0) return void 0;
1492
- let value;
1493
- try {
1494
- value = JSON.parse(text);
1495
- } catch (error) {
1496
- throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1497
- }
1498
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1499
- }
1500
- async function writeJsonFile(path, value) {
1501
- await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
1502
- }
1503
- //#endregion
1504
- //#region ../runner/src/index.ts
1505
- /**
1506
- * Runs a scenario: once, or `repeat` times as one batch. Resolves to one
1507
- * result per run started; runs never started because of cancellation are
1508
- * left out.
1509
- */
1510
- function runEval(definition, options) {
1511
- return Effect.gen(function* () {
1512
- yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
1513
- evalId: authoringId(definition),
1514
- ...options.runId ? { runId: options.runId } : {},
1515
- ...options.suiteId ? { suiteId: options.suiteId } : {}
1516
- }));
1517
- const runs = yield* Effect.tryPromise({
1518
- try: () => executeBatch(definition, options),
1519
- catch: (error) => error
1520
- });
1521
- yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
1522
- evalId: authoringId(definition),
1523
- runs: runs.length,
1524
- passed: runs.filter((run) => run.status === "passed").length
1525
- }));
1526
- return runs;
1527
- });
1528
- }
1529
- function createId() {
1530
- return crypto.randomUUID();
1531
- }
1532
- function canonicalId(kind, id) {
1533
- return resourceUri(kind, id);
1534
- }
1535
- function assertUuid(value, label) {
1536
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) throw new Error(`${label} must be a UUID`);
1537
- }
1538
- function runMetadata(definition, options, runId, startedAt, extra) {
1539
- return {
1540
- schemaVersion: 6,
1541
- runId,
1542
- runUri: canonicalId("run", runId),
1543
- evalId: definition.id,
1544
- ...options.suiteId ? { suiteId: options.suiteId } : {},
1545
- ...options.source ? { source: options.source } : {},
1546
- process: {
1547
- pid: process.pid,
1548
- hostname: os.hostname()
1549
- },
1550
- ...definition.agent.identity ? { aut: definition.agent.identity } : {},
1551
- parameters: options.parameters,
1552
- matrix: options.matrix,
1553
- startedAt,
1554
- ...options.batch ? { batch: options.batch } : {},
1555
- ...extra
1556
- };
1557
- }
1558
- /** Recording a secondary error must never mask the original failure or block cleanup. */
1559
- async function tryEmitError(error, emitRunner, now) {
1560
- try {
1561
- await emitRunner({
1562
- kind: "error",
1563
- error: recordError(error),
1564
- timestamp: now().toISOString()
1565
- });
1566
- } catch {}
1567
- }
1568
- /** Starts `repeat` runs of a scenario, as one batch when there are several. */
1569
- async function executeBatch(definition, options) {
1570
- let hasUser = false;
1571
- for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
1572
- else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
1573
- assertNoRemovedOptions(definition);
1574
- assertJudgesIndependent(definition, options.judge);
1575
- for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && !rule.agent && !options.judge) throw new Error(`Judge ${rule.name} in ${definition.id} has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts`);
1576
- if (options.runId) assertUuid(options.runId, "runId");
1577
- const repeat = options.repeat ?? definition.repeat ?? 1;
1578
- if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
1579
- if (repeat === 1) return [await runOnce(definition, options)];
1580
- if (options.runId) throw new Error("runId names one run; it cannot be used with repeat");
1581
- const batchId = createId();
1582
- const signal = options.signal;
1583
- const runs = Array.from({ length: repeat }, (_, index) => {
1584
- const run = Effect.tryPromise({
1585
- try: async () => signal?.aborted ? void 0 : runOnce(definition, {
1586
- ...options,
1587
- batch: {
1588
- id: batchId,
1589
- index,
1590
- size: repeat
1591
- }
1592
- }),
1593
- catch: (error) => error
1594
- });
1595
- return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
1596
- });
1597
- return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
1598
- }
1599
- /** One execution: a session in its own sandbox, scored and reported. */
1600
- async function runOnce(definition, options) {
1601
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
1602
- const runId = options.runId ?? createId();
1603
- const startedAt = now().toISOString();
1604
- const selectedRuntime = options.runtime;
1605
- const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
1606
- if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
1607
- const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
1608
- const batch = options.batch;
1609
- const signal = options.signal ?? new AbortController().signal;
1610
- const root = options.root ?? process.cwd();
1611
- const baseContext = {
1612
- runId,
1613
- evalId: authoringId(definition),
1614
- ...batch ? { batch } : {},
1615
- metadata: {
1616
- ...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
1617
- randomSeed
1618
- },
1619
- ...options.parameters ? { parameters: options.parameters } : {},
1620
- ...selectedRuntime ? { runtime: selectedRuntime } : {}
1621
- };
1622
- let datasetsError;
1623
- let prepared = [];
1624
- try {
1625
- prepared = await prepareDatasets(definition.datasets ?? [], root, {
1626
- evalId: baseContext.evalId,
1627
- runId,
1628
- ...batch ? { batch } : {},
1629
- seed: randomSeed,
1630
- parameters: options.parameters ?? {},
1631
- signal
1632
- });
1633
- } catch (error) {
1634
- datasetsError = error;
1635
- }
1636
- const candidateData = datasetValues(prepared, "candidate");
1637
- const scoringData = datasetValues(prepared, "all");
1638
- const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
1639
- const events = [];
1640
- const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
1641
- workspaceIgnore,
1642
- ...prepared.length ? { datasets: runDatasets(prepared) } : {}
1643
- }), { catalog: options.catalog });
1644
- const attachments = attachmentStore(runWriter, options.attachmentLimits);
1645
- let writeChain = Promise.resolve();
1646
- let reportingFailed = false;
1647
- const runRef = {
1648
- runId,
1649
- ...batch ? { batch } : {},
1650
- evalId: definition.id,
1651
- ...options.suiteId ? { suiteId: options.suiteId } : {},
1652
- ...options.parameters ? { parameters: options.parameters } : {}
1653
- };
1654
- const observe = (event) => {
1655
- try {
1656
- options.onEvent?.(runRef, event);
1657
- } catch {}
1658
- };
1659
- const append = async (event) => {
1660
- events.push(event);
1661
- writeChain = writeChain.then(() => runWriter.appendEvent(event));
1662
- try {
1663
- await writeChain;
1664
- observe(event);
1665
- } catch (cause) {
1666
- reportingFailed = true;
1667
- throw new ReportError({
1668
- cause,
1669
- message: cause instanceof Error ? cause.message : String(cause)
1670
- });
1671
- }
1672
- };
1673
- const emitRunner = (event) => append({
1674
- ...event,
1675
- source: "runner"
1676
- });
1677
- const emitAut = (event) => append({
1678
- ...event,
1679
- source: "aut"
1680
- });
1681
- const onAutEvent = (event) => emitAut(normalizeAutEvent(event, now));
1682
- let workspace;
1683
- let context;
1684
- let primaryError;
1685
- let closeError;
1686
- let scoring;
1687
- let artifacts;
1688
- let status = "running";
1689
- const progress = {
1690
- checkpoints: [],
1691
- stoppedEarly: false
1692
- };
1693
- let cleanupError;
1694
- const workspaceScope = await Effect.runPromise(Scope.make());
1695
- try {
1696
- await emitRunner({
1697
- kind: "run-started",
1698
- timestamp: now().toISOString()
1699
- });
1700
- if (signal.aborted) throw cancelled();
1701
- if (datasetsError) throw new DatasetError({
1702
- cause: datasetsError,
1703
- message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
1704
- });
1705
- workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
1706
- try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
1707
- catch: (cause) => new DatasetError({
1708
- cause,
1709
- message: cause instanceof Error ? cause.message : String(cause)
1710
- })
1711
- }), (created) => Effect.tryPromise({
1712
- try: () => created.cleanup(keepWorkspace(options.keepWorkspaces, scoring?.passed === true)),
1713
- catch: (cause) => new DatasetError({
1714
- cause,
1715
- message: cause instanceof Error ? cause.message : String(cause)
1716
- })
1717
- }).pipe(Effect.catchAll((error) => Effect.sync(() => {
1718
- cleanupError = error;
1719
- }).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
1720
- const runWorkspace = workspace;
1721
- const runContext = {
1722
- ...baseContext,
1723
- data: candidateData,
1724
- workspace: runWorkspace.artifacts.candidate,
1725
- evaluatorWorkspace: runWorkspace.artifacts.evaluator
1726
- };
1727
- context = runContext;
1728
- await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
1729
- try: () => definition.agent.start({
1730
- context: runContext,
1731
- ...runtime ? { runtime } : {},
1732
- onEvent: onAutEvent,
1733
- attach: attachments.attach
1734
- }),
1735
- catch: (cause) => new AutExecutionError({
1736
- cause,
1737
- message: cause instanceof Error ? cause.message : String(cause)
1738
- })
1739
- }), (session) => Effect.tryPromise({
1740
- try: () => executeTranscript(definition, session, {
1741
- context: runContext,
1742
- artifacts: runWorkspace.artifacts,
1743
- events,
1744
- now,
1745
- randomSeed,
1746
- emitRunner,
1747
- emitAut,
1748
- data: scoringData,
1749
- signal,
1750
- progress,
1751
- attach: attachments.attach,
1752
- defaultJudge: options.judge
1753
- }),
1754
- catch: (cause) => cause
1755
- }), (opened) => Effect.tryPromise({
1756
- try: async () => opened.close?.(),
1757
- catch: (cause) => new AutExecutionError({
1758
- cause,
1759
- message: cause instanceof Error ? cause.message : String(cause)
1760
- })
1761
- }).pipe(Effect.catchAll((error) => Effect.sync(() => {
1762
- closeError = error;
1763
- }).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
1764
- runId,
1765
- evalId: definition.id
1766
- })));
1767
- } catch (error) {
1768
- primaryError = error;
1769
- await tryEmitError(error, emitRunner, now);
1770
- }
1771
- if (closeError) {
1772
- if (!primaryError) primaryError = closeError;
1773
- await tryEmitError(closeError, emitRunner, now);
1774
- }
1775
- const wasCancelled = primaryError instanceof CancelledError;
1776
- const { checkpoints, stoppedEarly, lastTurn } = progress;
1777
- const scoreResults = [];
1778
- const skippedScorers = [];
1779
- const metrics = [];
1780
- try {
1781
- await evaluateFinalScorers(definition, {
1782
- context,
1783
- workspace,
1784
- events,
1785
- lastTurn,
1786
- primaryError,
1787
- stoppedEarly,
1788
- cancelled: wasCancelled,
1789
- reportingFailed,
1790
- results: scoreResults,
1791
- skippedScorers,
1792
- data: scoringData,
1793
- checkpoints,
1794
- attach: attachments.attach,
1795
- defaultJudge: options.judge,
1796
- now,
1797
- emitRunner
1798
- });
1799
- metrics.push(...builtInMetrics(events, [...scoreResults, ...checkpoints]));
1800
- if (context && workspace && !wasCancelled) metrics.push(...await evaluateMetrics(definition, scoringContext({
1801
- context,
1802
- workspace,
1803
- events,
1804
- lastTurn,
1805
- data: scoringData,
1806
- checkpoints,
1807
- transcript: definition.transcript,
1808
- attach: attachments.attach
1809
- })));
1810
- scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1811
- await runWriter.writeScores(scoring);
1812
- if (workspace) try {
1813
- artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
1814
- } catch (captureError) {
1815
- if (!primaryError) primaryError = captureError;
1816
- await tryEmitError(captureError, emitRunner, now);
1817
- }
1818
- } catch (error) {
1819
- if (!primaryError) primaryError = error;
1820
- await tryEmitError(error, emitRunner, now);
1821
- } finally {
1822
- await Effect.runPromise(Scope.close(workspaceScope, Exit.succeed(void 0)));
1823
- if (cleanupError) {
1824
- if (!primaryError) primaryError = cleanupError;
1825
- await tryEmitError(cleanupError, emitRunner, now);
1826
- }
1827
- }
1828
- scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
1829
- if (!primaryError) try {
1830
- await emitRunner({
1831
- kind: "run-completed",
1832
- timestamp: now().toISOString()
1833
- });
1834
- } catch (error) {
1835
- primaryError = error;
1836
- }
1837
- status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
1838
- const error = primaryError ? recordError(primaryError) : void 0;
1839
- const attached = await attachments.close();
1840
- const endedAt = now().toISOString();
1841
- const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
1842
- await runWriter.finalize({
1843
- status,
1844
- endedAt,
1845
- durationMs,
1846
- scoring,
1847
- ...artifacts ? { artifacts } : {},
1848
- ...attached.length ? { attachments: attached } : {},
1849
- ...error ? { error } : {}
1850
- });
1851
- return {
1852
- runId,
1853
- runUri: canonicalId("run", runId),
1854
- evalId: definition.id,
1855
- status,
1856
- ...batch ? { batch } : {},
1857
- reportLocation: runWriter.location,
1858
- durationMs,
1859
- scoring,
1860
- ...error ? { error } : {}
1861
- };
1862
- }
1863
- function datasetValues(prepared, audience) {
1864
- const values = {};
1865
- for (const { dataset, value } of prepared) {
1866
- if (value === void 0) continue;
1867
- if (audience === "candidate" && dataset.visibility !== "candidate") continue;
1868
- values[dataset.id] = value;
1869
- }
1870
- return values;
1871
- }
1872
- /** Persistent workspaces of runs that did not pass are kept for debugging by default. */
1873
- function keepWorkspace(policy = "failed", passed) {
1874
- return policy === "all" || policy === "failed" && !passed;
1875
- }
1876
- //#endregion
1877
- export { DatasetError as _, readRunEvents as a, readRunSummary as c, staticDatasetFile as d, staticDatasetFiles as f, CheckpointExecutionError as g, CancelledError as h, ownerIsGone as i, localReportStore as l, AutExecutionError as m, upgradeReports as n, readRunManifest as o, globMatcher as p, runMatrix as r, readRunScoring as s, runEval as t, runWorkspacePath as u, ReportError as v, ScoringError as y };