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