@leostera/evalkit 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -1
- package/README.md +2 -2
- package/dist/cli.mjs +386 -365
- package/dist/dashboard/assets/index-By1SJ1on.js +66 -0
- package/dist/dashboard/assets/index-geXMXCx9.css +2 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{files-C-U0-PWW.mjs → files-DkDQs2JL.mjs} +3 -0
- package/dist/{index-db-vbqEGFaU.mjs → index-db-BbVblaHz.mjs} +333 -51
- package/dist/index.d.mts +239 -178
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +246 -169
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +10 -3
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +6 -3
- package/dist/share.d.mts +49 -7
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +4 -4
- package/dist/skills/evalkit/SKILL.md +3 -3
- package/dist/skills/evalkit-agents/SKILL.md +5 -3
- package/dist/skills/evalkit-datasets/SKILL.md +5 -5
- package/dist/skills/evalkit-evals/SKILL.md +5 -5
- package/dist/skills/evalkit-judges/SKILL.md +5 -3
- package/dist/skills/evalkit-matrices/SKILL.md +2 -2
- package/dist/skills/evalkit-results/SKILL.md +6 -6
- package/dist/{src-CZnDXP1K.mjs → src-Bn6fnZnF.mjs} +8 -3
- package/dist/{src-b2Wnmzbb.mjs → src-BrfIXfO2.mjs} +75 -54
- package/dist/{src-DhromKNu.mjs → src-CcdLZqFk.mjs} +487 -250
- package/dist/upgrade-60SIRius.mjs +110 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-BEBo_81Y.js +0 -34
- package/dist/dashboard/assets/index-C24IquYZ.css +0 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as relativeDatasetPath, A as RunManifestSchema, B as turnResultEvents, C as CheckpointResultSchema, F as RunSummarySchema, G as DEFAULT_ATTACHMENT_LIMITS, H as canonicalParameters, I as ScoreResultSchema, L as TrajectoryEventSchema, M as RunScoringSchema, U as turnTokens, W as DEFAULT_ARTIFACT_EXCLUDE, X as toJsonObject, Y as toJson, _ as parseTrajectoryJsonl, a as authoringId, i as agentRuntimes, j as RunMetadataSchema, ot as resourceUri, p as recordError, w as JsonValueSchema, z as normalizeAutEvent } from "./src-BrfIXfO2.mjs";
|
|
2
|
+
import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-60SIRius.mjs";
|
|
2
3
|
import * as Schema from "effect/Schema";
|
|
3
4
|
import { Data, Effect, Either, Exit, Scope } from "effect";
|
|
4
5
|
import os from "node:os";
|
|
5
|
-
import { appendFile, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { appendFile, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import path, { join } from "node:path";
|
|
7
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
9
|
//#region ../runner/src/errors.ts
|
|
@@ -12,7 +13,7 @@ var ReportError = class extends Data.TaggedError("ReportError") {};
|
|
|
12
13
|
var ScoringError = class extends Data.TaggedError("ScoringError") {};
|
|
13
14
|
var RuleExecutionError = class extends Data.TaggedError("RuleExecutionError") {};
|
|
14
15
|
var CheckpointExecutionError = class extends Data.TaggedError("CheckpointExecutionError") {};
|
|
15
|
-
/** The run was cancelled (Ctrl+C); the
|
|
16
|
+
/** The run was cancelled (Ctrl+C); the run stops and is finalized as cancelled. */
|
|
16
17
|
var CancelledError = class extends Data.TaggedError("CancelledError") {};
|
|
17
18
|
function cancelled() {
|
|
18
19
|
return new CancelledError({ message: "Cancelled" });
|
|
@@ -32,6 +33,79 @@ async function unlessAborted(work, signal) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
//#endregion
|
|
36
|
+
//#region ../runner/src/attachments.ts
|
|
37
|
+
const SEGMENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,99}$/;
|
|
38
|
+
const MAX_NAME = 200;
|
|
39
|
+
/**
|
|
40
|
+
* Checks an attachment name: a relative path whose segments are letters,
|
|
41
|
+
* digits, `.`, `_` and `-`, not starting with a dot. That rules out
|
|
42
|
+
* absolute paths, `..`, hidden files, separators other than `/`, and
|
|
43
|
+
* anything a filesystem or URL would read differently.
|
|
44
|
+
*/
|
|
45
|
+
function attachmentName(name) {
|
|
46
|
+
if (typeof name !== "string" || !name || name.length > MAX_NAME || !name.split("/").every((segment) => SEGMENT.test(segment))) throw new Error(`Attachment name ${JSON.stringify(name)} is not allowed: use a relative path of letters, digits, ".", "_" and "-", like "logs/agent.log"`);
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
/** The last `max` bytes, starting on a whole UTF-8 character when the content is text. */
|
|
50
|
+
function lastBytes(bytes, max, text) {
|
|
51
|
+
let start = bytes.byteLength - max;
|
|
52
|
+
if (text) while (start < bytes.byteLength && (bytes[start] & 192) === 128) start++;
|
|
53
|
+
return bytes.subarray(start);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Saves a run's attachments as `attachments/<name>` below its artifacts.
|
|
57
|
+
* Content is stored as given and never interpreted. A file over
|
|
58
|
+
* `maxFileBytes` keeps its end (where a log's errors are); one that would
|
|
59
|
+
* take the run past `maxRunBytes` is rejected.
|
|
60
|
+
*/
|
|
61
|
+
function attachmentStore(writer, limits = {}) {
|
|
62
|
+
const { maxFileBytes, maxRunBytes } = {
|
|
63
|
+
...DEFAULT_ATTACHMENT_LIMITS,
|
|
64
|
+
...limits
|
|
65
|
+
};
|
|
66
|
+
const entries = [];
|
|
67
|
+
const names = /* @__PURE__ */ new Set();
|
|
68
|
+
const pending = /* @__PURE__ */ new Set();
|
|
69
|
+
let used = 0;
|
|
70
|
+
let closed = false;
|
|
71
|
+
async function attach(name, content) {
|
|
72
|
+
if (closed) throw new Error(`Cannot attach ${JSON.stringify(name)}: the run has finished`);
|
|
73
|
+
attachmentName(name);
|
|
74
|
+
if (typeof content !== "string" && !(content instanceof Uint8Array)) throw new Error(`Attachment ${name} must be a string or a Uint8Array, not ${content === null ? "null" : typeof content}`);
|
|
75
|
+
if (names.has(name)) throw new Error(`Attachment ${name} was already attached`);
|
|
76
|
+
const text = typeof content === "string";
|
|
77
|
+
const original = text ? new TextEncoder().encode(content) : content;
|
|
78
|
+
const bytes = original.byteLength > maxFileBytes ? lastBytes(original, maxFileBytes, text) : original;
|
|
79
|
+
if (used + bytes.byteLength > maxRunBytes) throw new Error(`Attachment ${name} (${bytes.byteLength} bytes) would take this run past its ${maxRunBytes}-byte attachment limit (${used} used)`);
|
|
80
|
+
names.add(name);
|
|
81
|
+
used += bytes.byteLength;
|
|
82
|
+
const write = writer.writeArtifact(`attachments/${name}`, bytes);
|
|
83
|
+
pending.add(write);
|
|
84
|
+
try {
|
|
85
|
+
await write;
|
|
86
|
+
entries.push({
|
|
87
|
+
name,
|
|
88
|
+
size: bytes.byteLength,
|
|
89
|
+
...bytes.byteLength < original.byteLength ? { truncatedFrom: original.byteLength } : {}
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
names.delete(name);
|
|
93
|
+
used -= bytes.byteLength;
|
|
94
|
+
throw error;
|
|
95
|
+
} finally {
|
|
96
|
+
pending.delete(write);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
attach,
|
|
101
|
+
async close() {
|
|
102
|
+
closed = true;
|
|
103
|
+
await Promise.allSettled(pending);
|
|
104
|
+
return [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
35
109
|
//#region ../core/src/glob.ts
|
|
36
110
|
/**
|
|
37
111
|
* A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
|
|
@@ -63,41 +137,82 @@ function globMatcher(pattern) {
|
|
|
63
137
|
}
|
|
64
138
|
//#endregion
|
|
65
139
|
//#region ../runner/src/snapshot.ts
|
|
66
|
-
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
const GLOB_CHARS = /[*?{]/;
|
|
141
|
+
function ignoreRule(pattern) {
|
|
142
|
+
const negated = pattern.startsWith("!");
|
|
143
|
+
const trimmed = (negated ? pattern.slice(1) : pattern).replace(/^\.\//, "").replace(/\/$/, "");
|
|
144
|
+
const match = globMatcher(trimmed);
|
|
145
|
+
if (!trimmed.includes("/")) return {
|
|
146
|
+
negated,
|
|
147
|
+
match: (relativePath) => match(path.posix.basename(relativePath)),
|
|
148
|
+
mayMatchBelow: () => true
|
|
149
|
+
};
|
|
150
|
+
const segments = trimmed.split("/");
|
|
151
|
+
const firstGlob = segments.findIndex((segment) => GLOB_CHARS.test(segment));
|
|
152
|
+
const prefix = segments.slice(0, firstGlob === -1 ? segments.length : firstGlob).join("/");
|
|
153
|
+
return {
|
|
154
|
+
negated,
|
|
155
|
+
match,
|
|
156
|
+
mayMatchBelow: (directory) => prefix === "" || prefix.startsWith(`${directory}/`) || prefix === directory || directory.startsWith(`${prefix}/`)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Ignore rules for candidate snapshots, read like `.gitignore`: globs relative
|
|
161
|
+
* to the workspace root, where a pattern without `/` matches that name at any
|
|
162
|
+
* depth, and later patterns win. `!pattern` re-includes a path; unlike Git, it
|
|
163
|
+
* also works below an ignored directory (`_build` then `!_build/doc/**`).
|
|
164
|
+
*/
|
|
165
|
+
function ignoreRules(patterns) {
|
|
166
|
+
const rules = patterns.map(ignoreRule);
|
|
167
|
+
return {
|
|
168
|
+
/** Whether a path is ignored, given whether its directory was. */
|
|
169
|
+
ignored(relativePath, parentIgnored = false) {
|
|
170
|
+
let ignored = parentIgnored;
|
|
171
|
+
for (const rule of rules) if (rule.match(relativePath)) ignored = !rule.negated;
|
|
172
|
+
return ignored;
|
|
173
|
+
},
|
|
174
|
+
/** Whether an ignored directory must still be walked for re-included paths. */
|
|
175
|
+
mayReinclude(directory) {
|
|
176
|
+
return rules.some((rule) => rule.negated && rule.mayMatchBelow(directory));
|
|
177
|
+
}
|
|
178
|
+
};
|
|
74
179
|
}
|
|
75
180
|
/**
|
|
76
|
-
* Copies the candidate workspace's regular files into the
|
|
77
|
-
* leaving out
|
|
78
|
-
* never followed, so a snapshot cannot read outside the
|
|
181
|
+
* Copies the candidate workspace's regular files into the run report,
|
|
182
|
+
* leaving out ignored paths. Symbolic links and special files are skipped,
|
|
183
|
+
* never followed, so a snapshot cannot read outside the run. An ignored
|
|
184
|
+
* directory is walked only when a `!` pattern could re-include something in
|
|
185
|
+
* it, and is listed only if it ends up containing a snapshotted file.
|
|
79
186
|
*/
|
|
80
|
-
async function snapshotCandidateWorkspace(candidateRoot, writer,
|
|
187
|
+
async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAULT_ARTIFACT_EXCLUDE) {
|
|
81
188
|
const artifacts = [];
|
|
82
|
-
const
|
|
83
|
-
|
|
189
|
+
const rules = ignoreRules(ignore);
|
|
190
|
+
/** Returns whether anything below `directory` was snapshotted. */
|
|
191
|
+
async function visit(directory, relativeDirectory, directoryIgnored) {
|
|
84
192
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
85
193
|
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
194
|
+
let kept = false;
|
|
86
195
|
for (const entry of entries) {
|
|
87
196
|
const absolutePath = path.join(directory, entry.name);
|
|
88
197
|
const relativePath = path.join(relativeDirectory, entry.name);
|
|
89
|
-
|
|
198
|
+
const posixPath = relativePath.split(path.sep).join("/");
|
|
199
|
+
const ignored = rules.ignored(posixPath, directoryIgnored);
|
|
90
200
|
const stat = await lstat(absolutePath);
|
|
91
201
|
if (stat.isSymbolicLink()) continue;
|
|
92
202
|
if (stat.isDirectory()) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
203
|
+
if (ignored && !rules.mayReinclude(posixPath)) continue;
|
|
204
|
+
const entryIndex = artifacts.length;
|
|
205
|
+
const keptBelow = await visit(absolutePath, relativePath, ignored);
|
|
206
|
+
if (!ignored || keptBelow) {
|
|
207
|
+
artifacts.splice(entryIndex, 0, {
|
|
208
|
+
path: path.join("candidate", relativePath),
|
|
209
|
+
kind: "directory"
|
|
210
|
+
});
|
|
211
|
+
kept = true;
|
|
212
|
+
}
|
|
98
213
|
continue;
|
|
99
214
|
}
|
|
100
|
-
if (!stat.isFile()) continue;
|
|
215
|
+
if (ignored || !stat.isFile()) continue;
|
|
101
216
|
const data = await readFile(absolutePath);
|
|
102
217
|
const artifactPath = path.join("candidate", relativePath);
|
|
103
218
|
await writer.writeArtifact(artifactPath, data);
|
|
@@ -106,9 +221,11 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, exclude = DEFAU
|
|
|
106
221
|
kind: "file",
|
|
107
222
|
size: data.byteLength
|
|
108
223
|
});
|
|
224
|
+
kept = true;
|
|
109
225
|
}
|
|
226
|
+
return kept;
|
|
110
227
|
}
|
|
111
|
-
await visit(candidateRoot, "");
|
|
228
|
+
await visit(candidateRoot, "", false);
|
|
112
229
|
return artifacts;
|
|
113
230
|
}
|
|
114
231
|
//#endregion
|
|
@@ -116,7 +233,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, exclude = DEFAU
|
|
|
116
233
|
const loads = /* @__PURE__ */ new WeakMap();
|
|
117
234
|
/**
|
|
118
235
|
* Loads a dataset at most once per process and project root. A failed load is
|
|
119
|
-
* forgotten, so the next
|
|
236
|
+
* forgotten, so the next run that needs the dataset tries again.
|
|
120
237
|
*/
|
|
121
238
|
function loadDataset(dataset, root, signal) {
|
|
122
239
|
let byRoot = loads.get(dataset);
|
|
@@ -167,12 +284,12 @@ async function readValueFile(id, file) {
|
|
|
167
284
|
}
|
|
168
285
|
throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
|
|
169
286
|
}
|
|
170
|
-
/** Loads (cached), derives this
|
|
171
|
-
async function prepareDatasets(datasets, root,
|
|
287
|
+
/** Loads (cached), derives this run's content with `onRun`, and resolves it. */
|
|
288
|
+
async function prepareDatasets(datasets, root, run) {
|
|
172
289
|
const prepared = [];
|
|
173
290
|
for (const dataset of datasets) {
|
|
174
|
-
const loaded = await loadDataset(dataset, root,
|
|
175
|
-
const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded,
|
|
291
|
+
const loaded = await loadDataset(dataset, root, run.signal);
|
|
292
|
+
const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, run) : loaded;
|
|
176
293
|
if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
|
|
177
294
|
prepared.push(await resolveContent(dataset, content, root));
|
|
178
295
|
}
|
|
@@ -240,7 +357,7 @@ async function digest(files, value) {
|
|
|
240
357
|
if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
|
|
241
358
|
return `sha256:${hash.digest("hex")}`;
|
|
242
359
|
}
|
|
243
|
-
function
|
|
360
|
+
function runDatasets(prepared) {
|
|
244
361
|
return prepared.map(({ dataset, digest }) => ({
|
|
245
362
|
id: dataset.id,
|
|
246
363
|
visibility: dataset.visibility,
|
|
@@ -289,17 +406,17 @@ async function staticDatasetFile(dataset, root, file) {
|
|
|
289
406
|
}
|
|
290
407
|
//#endregion
|
|
291
408
|
//#region ../runner/src/workspace.ts
|
|
292
|
-
/** Where a
|
|
293
|
-
function
|
|
294
|
-
return path.join(path.resolve(root),
|
|
409
|
+
/** Where a run's persistent workspace lives below `root`. */
|
|
410
|
+
function runWorkspacePath(root, runId) {
|
|
411
|
+
return path.join(path.resolve(root), runId.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
295
412
|
}
|
|
296
413
|
/**
|
|
297
414
|
* Creates isolated candidate and evaluator directories and writes each
|
|
298
415
|
* dataset's files below its mount, on the side its visibility allows.
|
|
299
416
|
* Two datasets writing the same file is an error, never an overwrite.
|
|
300
417
|
*/
|
|
301
|
-
async function
|
|
302
|
-
const root = persistentRoot ?
|
|
418
|
+
async function createRunWorkspace(runId, datasets = [], persistentRoot) {
|
|
419
|
+
const root = persistentRoot ? runWorkspacePath(persistentRoot, runId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-run-"));
|
|
303
420
|
const persistent = persistentRoot !== void 0;
|
|
304
421
|
const artifacts = {
|
|
305
422
|
candidate: { root: path.join(root, "candidate") },
|
|
@@ -351,6 +468,20 @@ function collision(datasetId, file, cause) {
|
|
|
351
468
|
return cause.code === "EEXIST" ? /* @__PURE__ */ new Error(`Dataset ${datasetId} would overwrite ${file}, which another dataset already wrote`) : cause instanceof Error ? cause : new Error(String(cause));
|
|
352
469
|
}
|
|
353
470
|
//#endregion
|
|
471
|
+
//#region ../runner/src/checkpoint-lookup.ts
|
|
472
|
+
/**
|
|
473
|
+
* `checkpoint(rule)` for a scoring context: the latest result of the steps
|
|
474
|
+
* that are this very rule. A rule outside the transcript is a mistake in the
|
|
475
|
+
* scenario, so it throws rather than reading as "not run".
|
|
476
|
+
*/
|
|
477
|
+
function checkpointLookup(transcript, results) {
|
|
478
|
+
return ((rule) => {
|
|
479
|
+
const steps = new Set(transcript.flatMap((step, index) => step === rule ? [index] : []));
|
|
480
|
+
if (!steps.size) throw new Error(`checkpoint(${JSON.stringify(rule.name)}) is not a checkpoint in this scenario's transcript; pass the same rule object the transcript uses`);
|
|
481
|
+
for (let index = results.length - 1; index >= 0; index--) if (steps.has(results[index].step)) return results[index];
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
|
354
485
|
//#region ../runner/src/score.ts
|
|
355
486
|
/**
|
|
356
487
|
* Shared normalization for inline and final predicate and judge rules.
|
|
@@ -404,6 +535,7 @@ function runJudgeAgent(rule, agent, input, placement) {
|
|
|
404
535
|
yield* Effect.acquireUseRelease(Effect.tryPromise({
|
|
405
536
|
try: () => agent.start({
|
|
406
537
|
context,
|
|
538
|
+
attach: input.attach,
|
|
407
539
|
onEvent: (event) => {
|
|
408
540
|
const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
|
|
409
541
|
...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
|
|
@@ -540,7 +672,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
|
|
|
540
672
|
const annotations = {
|
|
541
673
|
evalId: input.context.evalId,
|
|
542
674
|
runId: input.context.runId,
|
|
543
|
-
trialId: input.context.trialId,
|
|
544
675
|
step: index,
|
|
545
676
|
checkpoint: step.name
|
|
546
677
|
};
|
|
@@ -639,7 +770,7 @@ function userMessage(step, index, { context, randomSeed }) {
|
|
|
639
770
|
parameters: context.parameters ?? {},
|
|
640
771
|
data: context.data,
|
|
641
772
|
seed: randomSeed,
|
|
642
|
-
|
|
773
|
+
...context.batch ? { batch: context.batch } : {}
|
|
643
774
|
});
|
|
644
775
|
if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
|
|
645
776
|
return message;
|
|
@@ -670,7 +801,9 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
670
801
|
turn: progress.lastTurn,
|
|
671
802
|
...turnOutput(progress.lastTurn),
|
|
672
803
|
data: input.data,
|
|
673
|
-
checkpoints: [...progress.checkpoints]
|
|
804
|
+
checkpoints: [...progress.checkpoints],
|
|
805
|
+
checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
|
|
806
|
+
attach: input.attach
|
|
674
807
|
}, now, definition.judge)).catch(async (error) => {
|
|
675
808
|
const recorded = recordError(error);
|
|
676
809
|
progress.checkpoints.push({
|
|
@@ -736,7 +869,9 @@ function scoringContext(input) {
|
|
|
736
869
|
...input.lastTurn ? { turn: input.lastTurn } : {},
|
|
737
870
|
...turnOutput(input.lastTurn),
|
|
738
871
|
data: input.data,
|
|
739
|
-
checkpoints: input.checkpoints
|
|
872
|
+
checkpoints: input.checkpoints,
|
|
873
|
+
checkpoint: checkpointLookup(input.transcript, input.checkpoints),
|
|
874
|
+
attach: input.attach
|
|
740
875
|
};
|
|
741
876
|
}
|
|
742
877
|
/** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
|
|
@@ -758,7 +893,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
758
893
|
const score = await runBoundary(evaluateRule(scorer, scoringContext({
|
|
759
894
|
...input,
|
|
760
895
|
context,
|
|
761
|
-
workspace
|
|
896
|
+
workspace,
|
|
897
|
+
transcript: definition.transcript
|
|
762
898
|
}), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
763
899
|
cause,
|
|
764
900
|
message: cause.message
|
|
@@ -798,7 +934,7 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
798
934
|
//#endregion
|
|
799
935
|
//#region ../runner/src/metrics.ts
|
|
800
936
|
/**
|
|
801
|
-
* What EvalKit measures for every
|
|
937
|
+
* What EvalKit measures for every run from its trajectory: the agent's time
|
|
802
938
|
* (first event to the last one before final scoring), tokens when the agent
|
|
803
939
|
* reported usage, and tool calls.
|
|
804
940
|
*/
|
|
@@ -814,8 +950,7 @@ function builtInMetrics(events) {
|
|
|
814
950
|
});
|
|
815
951
|
let tokens;
|
|
816
952
|
for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
|
|
817
|
-
const
|
|
818
|
-
const turn = totalTokens ?? (inputTokens === void 0 && outputTokens === void 0 ? void 0 : (inputTokens ?? 0) + (outputTokens ?? 0));
|
|
953
|
+
const turn = turnTokens(event.usage);
|
|
819
954
|
if (turn !== void 0) tokens = (tokens ?? 0) + turn;
|
|
820
955
|
}
|
|
821
956
|
if (tokens !== void 0) metrics.push({
|
|
@@ -860,7 +995,7 @@ async function evaluateMetrics(definition, context) {
|
|
|
860
995
|
//#endregion
|
|
861
996
|
//#region ../runner/src/local-report-store.ts
|
|
862
997
|
const MANIFEST_FILE = "manifest.json";
|
|
863
|
-
/** The project's evals and suites when the run started
|
|
998
|
+
/** The project's evals and suites when the run started. */
|
|
864
999
|
const CATALOG_FILE = "catalog.json";
|
|
865
1000
|
async function writeJson(filePath, value) {
|
|
866
1001
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -879,10 +1014,12 @@ function artifactPath(root, value) {
|
|
|
879
1014
|
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
|
|
880
1015
|
return path.join(root, normalized);
|
|
881
1016
|
}
|
|
882
|
-
var
|
|
1017
|
+
var LocalRunWriter = class {
|
|
1018
|
+
location;
|
|
883
1019
|
directory;
|
|
884
1020
|
metadata;
|
|
885
|
-
constructor(directory, metadata) {
|
|
1021
|
+
constructor(location, directory, metadata) {
|
|
1022
|
+
this.location = location;
|
|
886
1023
|
this.directory = directory;
|
|
887
1024
|
this.metadata = metadata;
|
|
888
1025
|
}
|
|
@@ -890,40 +1027,13 @@ var LocalTrialWriter = class {
|
|
|
890
1027
|
return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
|
|
891
1028
|
}
|
|
892
1029
|
writeScores(scoring) {
|
|
893
|
-
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(
|
|
1030
|
+
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
|
|
894
1031
|
}
|
|
895
1032
|
async writeArtifact(relativePath, data) {
|
|
896
1033
|
const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
|
|
897
1034
|
await mkdir(path.dirname(target), { recursive: true });
|
|
898
1035
|
await writeFile(target, data);
|
|
899
1036
|
}
|
|
900
|
-
async finalize(summary) {
|
|
901
|
-
await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(TrialSummarySchema)(summary));
|
|
902
|
-
await replaceJson(path.join(this.directory, MANIFEST_FILE), {
|
|
903
|
-
...Schema.encodeSync(TrialMetadataSchema)(this.metadata),
|
|
904
|
-
status: summary.status
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
var LocalRunWriter = class {
|
|
909
|
-
location;
|
|
910
|
-
directory;
|
|
911
|
-
metadata;
|
|
912
|
-
constructor(location, directory, metadata) {
|
|
913
|
-
this.location = location;
|
|
914
|
-
this.directory = directory;
|
|
915
|
-
this.metadata = metadata;
|
|
916
|
-
}
|
|
917
|
-
async startTrial(metadata) {
|
|
918
|
-
assertPathSegment(metadata.trialId, "trialId");
|
|
919
|
-
const directory = path.join(this.directory, "trials", metadata.trialId);
|
|
920
|
-
await mkdir(directory, { recursive: true });
|
|
921
|
-
await writeJson(path.join(directory, MANIFEST_FILE), {
|
|
922
|
-
...Schema.encodeSync(TrialMetadataSchema)(metadata),
|
|
923
|
-
status: "running"
|
|
924
|
-
});
|
|
925
|
-
return new LocalTrialWriter(directory, metadata);
|
|
926
|
-
}
|
|
927
1037
|
async finalize(summary) {
|
|
928
1038
|
await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
|
|
929
1039
|
await replaceJson(path.join(this.directory, MANIFEST_FILE), {
|
|
@@ -948,17 +1058,15 @@ function localReportStore(rootDirectory) {
|
|
|
948
1058
|
}
|
|
949
1059
|
//#endregion
|
|
950
1060
|
//#region ../runner/src/local-report-reader.ts
|
|
951
|
-
/**
|
|
1061
|
+
/** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
|
|
952
1062
|
async function decode(file, schema) {
|
|
953
1063
|
return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
|
|
954
1064
|
}
|
|
955
1065
|
const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
|
|
956
1066
|
const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
async function readTrialEvents(root, runId, trialId) {
|
|
961
|
-
return parseTrajectoryJsonl(await readFile(join(root, runId, "trials", trialId, "trajectory.jsonl"), "utf8"));
|
|
1067
|
+
const readRunScoring = (root, runId) => decode(join(root, runId, "scoring.json"), RunScoringSchema);
|
|
1068
|
+
async function readRunEvents(root, runId) {
|
|
1069
|
+
return parseTrajectoryJsonl(await readFile(join(root, runId, "trajectory.jsonl"), "utf8"));
|
|
962
1070
|
}
|
|
963
1071
|
/**
|
|
964
1072
|
* True when a run's owning process is known to be gone: it ran on this host
|
|
@@ -985,6 +1093,7 @@ function runMatrix(matrix, options) {
|
|
|
985
1093
|
const iterator = matrix.cells(options.selection);
|
|
986
1094
|
const summary = {
|
|
987
1095
|
cells: 0,
|
|
1096
|
+
runs: 0,
|
|
988
1097
|
passed: 0,
|
|
989
1098
|
failed: 0,
|
|
990
1099
|
cancelled: 0
|
|
@@ -1000,7 +1109,7 @@ function runMatrix(matrix, options) {
|
|
|
1000
1109
|
"sandbox"
|
|
1001
1110
|
].find((name) => agentRuntimes(cell.eval.agent)[name]);
|
|
1002
1111
|
const suiteId = options.suiteId ?? options.suiteFor?.(cell);
|
|
1003
|
-
const
|
|
1112
|
+
const runs = yield* runEval(cell.eval, {
|
|
1004
1113
|
...options,
|
|
1005
1114
|
...suiteId ? { suiteId } : {},
|
|
1006
1115
|
concurrency: 1,
|
|
@@ -1012,18 +1121,232 @@ function runMatrix(matrix, options) {
|
|
|
1012
1121
|
}
|
|
1013
1122
|
});
|
|
1014
1123
|
summary.cells++;
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1124
|
+
for (const run of runs) {
|
|
1125
|
+
summary.runs++;
|
|
1126
|
+
if (run.status === "cancelled") summary.cancelled++;
|
|
1127
|
+
else if (run.status === "passed") summary.passed++;
|
|
1128
|
+
else summary.failed++;
|
|
1129
|
+
}
|
|
1130
|
+
if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, runs));
|
|
1019
1131
|
}
|
|
1020
1132
|
})), { concurrency });
|
|
1021
1133
|
return summary;
|
|
1022
1134
|
});
|
|
1023
1135
|
}
|
|
1024
1136
|
//#endregion
|
|
1137
|
+
//#region ../runner/src/upgrade.ts
|
|
1138
|
+
/** A new run being written. */
|
|
1139
|
+
const TEMPORARY = ".upgrade-";
|
|
1140
|
+
/** An old run set aside while its replacement, with the same ID, moves in. */
|
|
1141
|
+
const RETIRED = ".upgraded-";
|
|
1142
|
+
const PER_RUN_FILES = ["archived.json"];
|
|
1143
|
+
/**
|
|
1144
|
+
* Rewrites results from report schema versions 2 to 5 into version 6, in
|
|
1145
|
+
* place (RFD0008). An old run of one trial (most of them) becomes a run with
|
|
1146
|
+
* the same ID, so links to it keep working. An old run of several trials
|
|
1147
|
+
* becomes runs with the trials' IDs, in a batch with the old run's ID.
|
|
1148
|
+
*
|
|
1149
|
+
* Each new run is written under a temporary name and renamed into place;
|
|
1150
|
+
* artifacts are moved, not copied, as the last step before that rename. An
|
|
1151
|
+
* old run is removed only after its new runs exist, and one replaced under
|
|
1152
|
+
* the same ID is set aside first, so a crash at any point leaves the old run
|
|
1153
|
+
* or complete new ones, and re-running finishes the job. Version 6 runs are
|
|
1154
|
+
* left alone.
|
|
1155
|
+
*/
|
|
1156
|
+
async function upgradeReports(root, options = {}) {
|
|
1157
|
+
const report = {
|
|
1158
|
+
upgraded: 0,
|
|
1159
|
+
runs: 0,
|
|
1160
|
+
batches: 0,
|
|
1161
|
+
running: []
|
|
1162
|
+
};
|
|
1163
|
+
let entries;
|
|
1164
|
+
try {
|
|
1165
|
+
entries = await readdir(root);
|
|
1166
|
+
} catch {
|
|
1167
|
+
return report;
|
|
1168
|
+
}
|
|
1169
|
+
if (!options.dryRun) await finishReplacements(root, entries);
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
if (entry.startsWith(".")) continue;
|
|
1172
|
+
const directory = join(root, entry);
|
|
1173
|
+
const manifest = await readJson(join(directory, "manifest.json"));
|
|
1174
|
+
if (!manifest || !UPGRADABLE_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) continue;
|
|
1175
|
+
const summary = await readJson(join(directory, "summary.json"));
|
|
1176
|
+
if (!summary && !ownerIsGone(manifest.process)) {
|
|
1177
|
+
report.running.push(entry);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const trials = await trialIds(directory);
|
|
1181
|
+
report.upgraded += 1;
|
|
1182
|
+
report.runs += Math.max(trials.length, 1);
|
|
1183
|
+
if (trials.length > 1) report.batches += 1;
|
|
1184
|
+
if (options.dryRun) continue;
|
|
1185
|
+
await upgradeRun(root, entry, manifest, summary, trials);
|
|
1186
|
+
}
|
|
1187
|
+
if (!options.dryRun) await removeLeftovers(root);
|
|
1188
|
+
return report;
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Finishes same-ID replacements a crash interrupted: the old run was set
|
|
1192
|
+
* aside, and its replacement may or may not have moved in.
|
|
1193
|
+
*/
|
|
1194
|
+
async function finishReplacements(root, entries) {
|
|
1195
|
+
for (const entry of entries) {
|
|
1196
|
+
if (!entry.startsWith(RETIRED)) continue;
|
|
1197
|
+
const id = entry.slice(10);
|
|
1198
|
+
const retired = join(root, entry);
|
|
1199
|
+
const directory = join(root, id);
|
|
1200
|
+
const replacement = join(root, `${TEMPORARY}${id}`);
|
|
1201
|
+
if (!await exists(directory)) await rename(await exists(replacement) ? replacement : retired, directory);
|
|
1202
|
+
await rm(retired, {
|
|
1203
|
+
recursive: true,
|
|
1204
|
+
force: true
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Temporary folders nothing will finish. One holding artifacts is kept: it
|
|
1210
|
+
* could be the only copy, and an old run would have moved them back.
|
|
1211
|
+
*/
|
|
1212
|
+
async function removeLeftovers(root) {
|
|
1213
|
+
for (const entry of await readdir(root)) if (entry.startsWith(TEMPORARY) && !await exists(join(root, entry, "artifacts"))) await rm(join(root, entry), {
|
|
1214
|
+
recursive: true,
|
|
1215
|
+
force: true
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
/** Swaps a fully written replacement in for the old run of the same ID. */
|
|
1219
|
+
async function replaceInPlace(root, id, replacement) {
|
|
1220
|
+
const retired = join(root, `${RETIRED}${id}`);
|
|
1221
|
+
await rename(join(root, id), retired);
|
|
1222
|
+
await rename(replacement, join(root, id));
|
|
1223
|
+
await rm(retired, {
|
|
1224
|
+
recursive: true,
|
|
1225
|
+
force: true
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
async function upgradeRun(root, oldRunId, runManifest, runSummary, trials) {
|
|
1229
|
+
const directory = join(root, oldRunId);
|
|
1230
|
+
const catalog = await readJson(join(directory, "catalog.json"));
|
|
1231
|
+
const extras = await presentFiles(directory, PER_RUN_FILES);
|
|
1232
|
+
if (!trials.length) {
|
|
1233
|
+
const target = join(root, `${TEMPORARY}${oldRunId}`);
|
|
1234
|
+
await mkdir(target, { recursive: true });
|
|
1235
|
+
const status = runSummary ? "cancelled" : "running";
|
|
1236
|
+
await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
|
|
1237
|
+
run: runManifest,
|
|
1238
|
+
runId: oldRunId,
|
|
1239
|
+
status
|
|
1240
|
+
}));
|
|
1241
|
+
if (runSummary) await writeJsonFile(join(target, "summary.json"), {
|
|
1242
|
+
status,
|
|
1243
|
+
endedAt: runSummary.endedAt,
|
|
1244
|
+
...runSummary.durationMs === void 0 ? {} : { durationMs: runSummary.durationMs },
|
|
1245
|
+
...runSummary.error ? { error: runSummary.error } : {}
|
|
1246
|
+
});
|
|
1247
|
+
await copyShared(directory, target, catalog, extras);
|
|
1248
|
+
await replaceInPlace(root, oldRunId, target);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const inPlace = trials.length === 1;
|
|
1252
|
+
for (const trialId of trials) {
|
|
1253
|
+
const runId = inPlace ? oldRunId : trialId;
|
|
1254
|
+
const destination = join(root, runId);
|
|
1255
|
+
if (!inPlace && await exists(join(destination, "manifest.json"))) continue;
|
|
1256
|
+
const source = join(directory, "trials", trialId);
|
|
1257
|
+
const trialManifest = await readJson(join(source, "manifest.json")) ?? {};
|
|
1258
|
+
const trialSummary = await readJson(join(source, "summary.json"));
|
|
1259
|
+
const passed = (trialSummary?.scoring)?.passed;
|
|
1260
|
+
const status = upgradeStatus(trialSummary?.status ?? trialManifest.status, passed);
|
|
1261
|
+
const index = typeof trialManifest.trialIndex === "number" ? trialManifest.trialIndex : trials.indexOf(trialId);
|
|
1262
|
+
const target = join(root, `${TEMPORARY}${runId}`);
|
|
1263
|
+
if (!await exists(join(source, "artifacts")) && await exists(join(target, "artifacts"))) await rename(join(target, "artifacts"), join(source, "artifacts"));
|
|
1264
|
+
await rm(target, {
|
|
1265
|
+
recursive: true,
|
|
1266
|
+
force: true
|
|
1267
|
+
});
|
|
1268
|
+
await mkdir(target, { recursive: true });
|
|
1269
|
+
await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
|
|
1270
|
+
run: runManifest,
|
|
1271
|
+
trial: trialManifest,
|
|
1272
|
+
runId,
|
|
1273
|
+
...!inPlace ? { batch: {
|
|
1274
|
+
id: oldRunId,
|
|
1275
|
+
index,
|
|
1276
|
+
size: trials.length
|
|
1277
|
+
} } : {},
|
|
1278
|
+
status
|
|
1279
|
+
}));
|
|
1280
|
+
if (trialSummary) await writeJsonFile(join(target, "summary.json"), {
|
|
1281
|
+
...trialSummary,
|
|
1282
|
+
status
|
|
1283
|
+
});
|
|
1284
|
+
const scoring = await readText(join(source, "scoring.json"));
|
|
1285
|
+
if (scoring !== void 0) await writeFile(join(target, "scoring.json"), scoring);
|
|
1286
|
+
const trajectory = await readText(join(source, "trajectory.jsonl"));
|
|
1287
|
+
if (trajectory !== void 0) await writeFile(join(target, "trajectory.jsonl"), upgradeTrajectory(trajectory));
|
|
1288
|
+
await copyShared(directory, target, catalog, extras);
|
|
1289
|
+
if (await exists(join(source, "artifacts"))) await rename(join(source, "artifacts"), join(target, "artifacts"));
|
|
1290
|
+
if (inPlace) await replaceInPlace(root, oldRunId, target);
|
|
1291
|
+
else await rename(target, destination);
|
|
1292
|
+
}
|
|
1293
|
+
if (!inPlace) await rm(directory, {
|
|
1294
|
+
recursive: true,
|
|
1295
|
+
force: true
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
async function copyShared(from, to, catalog, extras) {
|
|
1299
|
+
if (catalog) await writeJsonFile(join(to, "catalog.json"), upgradeCatalog(catalog));
|
|
1300
|
+
for (const file of extras) await cp(join(from, file), join(to, file));
|
|
1301
|
+
}
|
|
1302
|
+
async function trialIds(directory) {
|
|
1303
|
+
try {
|
|
1304
|
+
return (await readdir(join(directory, "trials"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1305
|
+
} catch {
|
|
1306
|
+
return [];
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async function presentFiles(directory, names) {
|
|
1310
|
+
const present = [];
|
|
1311
|
+
for (const name of names) if (await exists(join(directory, name))) present.push(name);
|
|
1312
|
+
return present;
|
|
1313
|
+
}
|
|
1314
|
+
async function exists(path) {
|
|
1315
|
+
try {
|
|
1316
|
+
await stat(path);
|
|
1317
|
+
return true;
|
|
1318
|
+
} catch {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async function readText(path) {
|
|
1323
|
+
try {
|
|
1324
|
+
return await readFile(path, "utf8");
|
|
1325
|
+
} catch {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async function readJson(path) {
|
|
1330
|
+
const text = await readText(path);
|
|
1331
|
+
if (text === void 0) return void 0;
|
|
1332
|
+
let value;
|
|
1333
|
+
try {
|
|
1334
|
+
value = JSON.parse(text);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1337
|
+
}
|
|
1338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1339
|
+
}
|
|
1340
|
+
async function writeJsonFile(path, value) {
|
|
1341
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
1342
|
+
}
|
|
1343
|
+
//#endregion
|
|
1025
1344
|
//#region ../runner/src/index.ts
|
|
1026
|
-
/**
|
|
1345
|
+
/**
|
|
1346
|
+
* Runs a scenario: once, or `repeat` times as one batch. Resolves to one
|
|
1347
|
+
* result per run started; runs never started because of cancellation are
|
|
1348
|
+
* left out.
|
|
1349
|
+
*/
|
|
1027
1350
|
function runEval(definition, options) {
|
|
1028
1351
|
return Effect.gen(function* () {
|
|
1029
1352
|
yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
|
|
@@ -1031,19 +1354,19 @@ function runEval(definition, options) {
|
|
|
1031
1354
|
...options.runId ? { runId: options.runId } : {},
|
|
1032
1355
|
...options.suiteId ? { suiteId: options.suiteId } : {}
|
|
1033
1356
|
}));
|
|
1034
|
-
const
|
|
1035
|
-
try: () =>
|
|
1357
|
+
const runs = yield* Effect.tryPromise({
|
|
1358
|
+
try: () => executeBatch(definition, options),
|
|
1036
1359
|
catch: (error) => error
|
|
1037
1360
|
});
|
|
1038
1361
|
yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
|
|
1039
1362
|
evalId: authoringId(definition),
|
|
1040
|
-
|
|
1041
|
-
|
|
1363
|
+
runs: runs.length,
|
|
1364
|
+
passed: runs.filter((run) => run.status === "passed").length
|
|
1042
1365
|
}));
|
|
1043
|
-
return
|
|
1366
|
+
return runs;
|
|
1044
1367
|
});
|
|
1045
1368
|
}
|
|
1046
|
-
function createId(
|
|
1369
|
+
function createId() {
|
|
1047
1370
|
return crypto.randomUUID();
|
|
1048
1371
|
}
|
|
1049
1372
|
function canonicalId(kind, id) {
|
|
@@ -1052,9 +1375,9 @@ function canonicalId(kind, id) {
|
|
|
1052
1375
|
function assertUuid(value, label) {
|
|
1053
1376
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) throw new Error(`${label} must be a UUID`);
|
|
1054
1377
|
}
|
|
1055
|
-
function runMetadata(definition, options, runId, startedAt) {
|
|
1378
|
+
function runMetadata(definition, options, runId, startedAt, extra) {
|
|
1056
1379
|
return {
|
|
1057
|
-
schemaVersion:
|
|
1380
|
+
schemaVersion: 6,
|
|
1058
1381
|
runId,
|
|
1059
1382
|
runUri: canonicalId("run", runId),
|
|
1060
1383
|
evalId: definition.id,
|
|
@@ -1067,7 +1390,9 @@ function runMetadata(definition, options, runId, startedAt) {
|
|
|
1067
1390
|
...definition.agent.identity ? { aut: definition.agent.identity } : {},
|
|
1068
1391
|
parameters: options.parameters,
|
|
1069
1392
|
matrix: options.matrix,
|
|
1070
|
-
startedAt
|
|
1393
|
+
startedAt,
|
|
1394
|
+
...options.batch ? { batch: options.batch } : {},
|
|
1395
|
+
...extra
|
|
1071
1396
|
};
|
|
1072
1397
|
}
|
|
1073
1398
|
/** Recording a secondary error must never mask the original failure or block cleanup. */
|
|
@@ -1080,104 +1405,52 @@ async function tryEmitError(error, emitRunner, now) {
|
|
|
1080
1405
|
});
|
|
1081
1406
|
} catch {}
|
|
1082
1407
|
}
|
|
1083
|
-
/**
|
|
1084
|
-
async function
|
|
1408
|
+
/** Starts `repeat` runs of a scenario, as one batch when there are several. */
|
|
1409
|
+
async function executeBatch(definition, options) {
|
|
1085
1410
|
let hasUser = false;
|
|
1086
1411
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
1087
1412
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
1088
1413
|
if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
|
|
1414
|
+
assertNoRemovedOptions(definition);
|
|
1089
1415
|
if (options.runId) assertUuid(options.runId, "runId");
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (
|
|
1093
|
-
if (
|
|
1094
|
-
const
|
|
1095
|
-
const aggregateStartedAt = now();
|
|
1096
|
-
const runId = options.runId ?? createId("run");
|
|
1097
|
-
const runWriter = await options.report.startRun(runMetadata(definition, options, runId, aggregateStartedAt.toISOString()), { catalog: options.catalog });
|
|
1416
|
+
const repeat = options.repeat ?? definition.repeat ?? 1;
|
|
1417
|
+
if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
|
|
1418
|
+
if (repeat === 1) return [await runOnce(definition, options)];
|
|
1419
|
+
if (options.runId) throw new Error("runId names one run; it cannot be used with repeat");
|
|
1420
|
+
const batchId = createId();
|
|
1098
1421
|
const signal = options.signal;
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
try: async () => signal?.aborted ? void 0 :
|
|
1422
|
+
const runs = Array.from({ length: repeat }, (_, index) => {
|
|
1423
|
+
const run = Effect.tryPromise({
|
|
1424
|
+
try: async () => signal?.aborted ? void 0 : runOnce(definition, {
|
|
1102
1425
|
...options,
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1426
|
+
batch: {
|
|
1427
|
+
id: batchId,
|
|
1428
|
+
index,
|
|
1429
|
+
size: repeat
|
|
1430
|
+
}
|
|
1108
1431
|
}),
|
|
1109
1432
|
catch: (error) => error
|
|
1110
1433
|
});
|
|
1111
|
-
return options.semaphore ? options.semaphore.withPermits(1)(
|
|
1112
|
-
});
|
|
1113
|
-
const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
|
|
1114
|
-
const counts = trialCounts(trials);
|
|
1115
|
-
const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
|
|
1116
|
-
const endedAt = now();
|
|
1117
|
-
await runWriter.finalize({
|
|
1118
|
-
status,
|
|
1119
|
-
endedAt: endedAt.toISOString(),
|
|
1120
|
-
durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
|
|
1121
|
-
trialCount: trials.length,
|
|
1122
|
-
passed: counts.passed,
|
|
1123
|
-
failed: counts.failed,
|
|
1124
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {}
|
|
1434
|
+
return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
|
|
1125
1435
|
});
|
|
1126
|
-
|
|
1127
|
-
runId,
|
|
1128
|
-
runUri: canonicalId("run", runId),
|
|
1129
|
-
trialId: "",
|
|
1130
|
-
trialUri: canonicalId("trial", runId),
|
|
1131
|
-
trialIndex: 0,
|
|
1132
|
-
evalId: authoringId(definition),
|
|
1133
|
-
status,
|
|
1134
|
-
reportLocation: runWriter.location,
|
|
1135
|
-
trialCount: 0,
|
|
1136
|
-
passed: 0,
|
|
1137
|
-
failed: 0,
|
|
1138
|
-
aggregateScoring: {
|
|
1139
|
-
passed: 0,
|
|
1140
|
-
failed: 0,
|
|
1141
|
-
passRate: 0
|
|
1142
|
-
},
|
|
1143
|
-
trials: []
|
|
1144
|
-
};
|
|
1145
|
-
const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
|
|
1146
|
-
return {
|
|
1147
|
-
...trials[0],
|
|
1148
|
-
runId,
|
|
1149
|
-
evalId: authoringId(definition),
|
|
1150
|
-
status,
|
|
1151
|
-
trialCount: trials.length,
|
|
1152
|
-
passed: counts.passed,
|
|
1153
|
-
failed: counts.failed,
|
|
1154
|
-
aggregateScoring: {
|
|
1155
|
-
passed: counts.passed,
|
|
1156
|
-
failed: counts.failed,
|
|
1157
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {},
|
|
1158
|
-
passRate: counts.passed / trials.length,
|
|
1159
|
-
...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
|
|
1160
|
-
},
|
|
1161
|
-
trials
|
|
1162
|
-
};
|
|
1436
|
+
return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
|
|
1163
1437
|
}
|
|
1164
|
-
|
|
1438
|
+
/** One execution: a session in its own sandbox, scored and reported. */
|
|
1439
|
+
async function runOnce(definition, options) {
|
|
1165
1440
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1166
|
-
const runId = options.runId ?? createId(
|
|
1167
|
-
const trialId = options.trialId ?? createId("trial");
|
|
1441
|
+
const runId = options.runId ?? createId();
|
|
1168
1442
|
const startedAt = now().toISOString();
|
|
1169
1443
|
const selectedRuntime = options.runtime;
|
|
1170
1444
|
const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
|
|
1171
1445
|
if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
|
|
1172
1446
|
const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
|
|
1173
|
-
const
|
|
1447
|
+
const batch = options.batch;
|
|
1174
1448
|
const signal = options.signal ?? new AbortController().signal;
|
|
1175
1449
|
const root = options.root ?? process.cwd();
|
|
1176
1450
|
const baseContext = {
|
|
1177
1451
|
runId,
|
|
1178
1452
|
evalId: authoringId(definition),
|
|
1179
|
-
|
|
1180
|
-
trialIndex,
|
|
1453
|
+
...batch ? { batch } : {},
|
|
1181
1454
|
metadata: {
|
|
1182
1455
|
...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
|
|
1183
1456
|
randomSeed
|
|
@@ -1191,8 +1464,7 @@ async function runTrial(definition, options) {
|
|
|
1191
1464
|
prepared = await prepareDatasets(definition.datasets ?? [], root, {
|
|
1192
1465
|
evalId: baseContext.evalId,
|
|
1193
1466
|
runId,
|
|
1194
|
-
|
|
1195
|
-
trialIndex,
|
|
1467
|
+
...batch ? { batch } : {},
|
|
1196
1468
|
seed: randomSeed,
|
|
1197
1469
|
parameters: options.parameters ?? {},
|
|
1198
1470
|
signal
|
|
@@ -1202,40 +1474,30 @@ async function runTrial(definition, options) {
|
|
|
1202
1474
|
}
|
|
1203
1475
|
const candidateData = datasetValues(prepared, "candidate");
|
|
1204
1476
|
const scoringData = datasetValues(prepared, "all");
|
|
1477
|
+
const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
|
|
1205
1478
|
const events = [];
|
|
1206
|
-
const runWriter =
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
trialId,
|
|
1212
|
-
trialUri: canonicalId("trial", trialId),
|
|
1213
|
-
trialIndex: options.trialIndex ?? 0,
|
|
1214
|
-
evalId: definition.id,
|
|
1215
|
-
...definition.agent.identity ? { aut: definition.agent.identity } : {},
|
|
1216
|
-
parameters: options.parameters,
|
|
1217
|
-
matrix: options.matrix,
|
|
1218
|
-
startedAt,
|
|
1219
|
-
...prepared.length ? { datasets: trialDatasets(prepared) } : {}
|
|
1220
|
-
});
|
|
1479
|
+
const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
|
|
1480
|
+
workspaceIgnore,
|
|
1481
|
+
...prepared.length ? { datasets: runDatasets(prepared) } : {}
|
|
1482
|
+
}), { catalog: options.catalog });
|
|
1483
|
+
const attachments = attachmentStore(runWriter, options.attachmentLimits);
|
|
1221
1484
|
let writeChain = Promise.resolve();
|
|
1222
1485
|
let reportingFailed = false;
|
|
1223
|
-
const
|
|
1486
|
+
const runRef = {
|
|
1224
1487
|
runId,
|
|
1225
|
-
|
|
1226
|
-
trialIndex,
|
|
1488
|
+
...batch ? { batch } : {},
|
|
1227
1489
|
evalId: definition.id,
|
|
1228
1490
|
...options.suiteId ? { suiteId: options.suiteId } : {},
|
|
1229
1491
|
...options.parameters ? { parameters: options.parameters } : {}
|
|
1230
1492
|
};
|
|
1231
1493
|
const observe = (event) => {
|
|
1232
1494
|
try {
|
|
1233
|
-
options.onEvent?.(
|
|
1495
|
+
options.onEvent?.(runRef, event);
|
|
1234
1496
|
} catch {}
|
|
1235
1497
|
};
|
|
1236
1498
|
const append = async (event) => {
|
|
1237
1499
|
events.push(event);
|
|
1238
|
-
writeChain = writeChain.then(() =>
|
|
1500
|
+
writeChain = writeChain.then(() => runWriter.appendEvent(event));
|
|
1239
1501
|
try {
|
|
1240
1502
|
await writeChain;
|
|
1241
1503
|
observe(event);
|
|
@@ -1271,7 +1533,7 @@ async function runTrial(definition, options) {
|
|
|
1271
1533
|
const workspaceScope = await Effect.runPromise(Scope.make());
|
|
1272
1534
|
try {
|
|
1273
1535
|
await emitRunner({
|
|
1274
|
-
kind: "
|
|
1536
|
+
kind: "run-started",
|
|
1275
1537
|
timestamp: now().toISOString()
|
|
1276
1538
|
});
|
|
1277
1539
|
if (signal.aborted) throw cancelled();
|
|
@@ -1280,7 +1542,7 @@ async function runTrial(definition, options) {
|
|
|
1280
1542
|
message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
|
|
1281
1543
|
});
|
|
1282
1544
|
workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
|
|
1283
|
-
try: () =>
|
|
1545
|
+
try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
|
|
1284
1546
|
catch: (cause) => new DatasetError({
|
|
1285
1547
|
cause,
|
|
1286
1548
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -1293,20 +1555,21 @@ async function runTrial(definition, options) {
|
|
|
1293
1555
|
})
|
|
1294
1556
|
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
1295
1557
|
cleanupError = error;
|
|
1296
|
-
}).pipe(Effect.tap(() => Effect.logError("
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1558
|
+
}).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
|
|
1559
|
+
const runWorkspace = workspace;
|
|
1560
|
+
const runContext = {
|
|
1299
1561
|
...baseContext,
|
|
1300
1562
|
data: candidateData,
|
|
1301
|
-
workspace:
|
|
1302
|
-
evaluatorWorkspace:
|
|
1563
|
+
workspace: runWorkspace.artifacts.candidate,
|
|
1564
|
+
evaluatorWorkspace: runWorkspace.artifacts.evaluator
|
|
1303
1565
|
};
|
|
1304
|
-
context =
|
|
1566
|
+
context = runContext;
|
|
1305
1567
|
await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
|
|
1306
1568
|
try: () => definition.agent.start({
|
|
1307
|
-
context:
|
|
1569
|
+
context: runContext,
|
|
1308
1570
|
...runtime ? { runtime } : {},
|
|
1309
|
-
onEvent: onAutEvent
|
|
1571
|
+
onEvent: onAutEvent,
|
|
1572
|
+
attach: attachments.attach
|
|
1310
1573
|
}),
|
|
1311
1574
|
catch: (cause) => new AutExecutionError({
|
|
1312
1575
|
cause,
|
|
@@ -1314,8 +1577,8 @@ async function runTrial(definition, options) {
|
|
|
1314
1577
|
})
|
|
1315
1578
|
}), (session) => Effect.tryPromise({
|
|
1316
1579
|
try: () => executeTranscript(definition, session, {
|
|
1317
|
-
context:
|
|
1318
|
-
artifacts:
|
|
1580
|
+
context: runContext,
|
|
1581
|
+
artifacts: runWorkspace.artifacts,
|
|
1319
1582
|
events,
|
|
1320
1583
|
now,
|
|
1321
1584
|
randomSeed,
|
|
@@ -1323,7 +1586,8 @@ async function runTrial(definition, options) {
|
|
|
1323
1586
|
emitAut,
|
|
1324
1587
|
data: scoringData,
|
|
1325
1588
|
signal,
|
|
1326
|
-
progress
|
|
1589
|
+
progress,
|
|
1590
|
+
attach: attachments.attach
|
|
1327
1591
|
}),
|
|
1328
1592
|
catch: (cause) => cause
|
|
1329
1593
|
}), (opened) => Effect.tryPromise({
|
|
@@ -1336,7 +1600,6 @@ async function runTrial(definition, options) {
|
|
|
1336
1600
|
closeError = error;
|
|
1337
1601
|
}).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
|
|
1338
1602
|
runId,
|
|
1339
|
-
trialId,
|
|
1340
1603
|
evalId: definition.id
|
|
1341
1604
|
})));
|
|
1342
1605
|
} catch (error) {
|
|
@@ -1366,6 +1629,7 @@ async function runTrial(definition, options) {
|
|
|
1366
1629
|
skippedScorers,
|
|
1367
1630
|
data: scoringData,
|
|
1368
1631
|
checkpoints,
|
|
1632
|
+
attach: attachments.attach,
|
|
1369
1633
|
now,
|
|
1370
1634
|
emitRunner
|
|
1371
1635
|
});
|
|
@@ -1376,12 +1640,14 @@ async function runTrial(definition, options) {
|
|
|
1376
1640
|
events,
|
|
1377
1641
|
lastTurn,
|
|
1378
1642
|
data: scoringData,
|
|
1379
|
-
checkpoints
|
|
1643
|
+
checkpoints,
|
|
1644
|
+
transcript: definition.transcript,
|
|
1645
|
+
attach: attachments.attach
|
|
1380
1646
|
})));
|
|
1381
1647
|
scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1382
|
-
await
|
|
1648
|
+
await runWriter.writeScores(scoring);
|
|
1383
1649
|
if (workspace) try {
|
|
1384
|
-
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root,
|
|
1650
|
+
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
|
|
1385
1651
|
} catch (captureError) {
|
|
1386
1652
|
if (!primaryError) primaryError = captureError;
|
|
1387
1653
|
await tryEmitError(captureError, emitRunner, now);
|
|
@@ -1399,49 +1665,34 @@ async function runTrial(definition, options) {
|
|
|
1399
1665
|
scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1400
1666
|
if (!primaryError) try {
|
|
1401
1667
|
await emitRunner({
|
|
1402
|
-
kind: "
|
|
1668
|
+
kind: "run-completed",
|
|
1403
1669
|
timestamp: now().toISOString()
|
|
1404
1670
|
});
|
|
1405
1671
|
} catch (error) {
|
|
1406
1672
|
primaryError = error;
|
|
1407
1673
|
}
|
|
1408
|
-
status = wasCancelled ? "cancelled" : primaryError ? "
|
|
1674
|
+
status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
|
|
1409
1675
|
const error = primaryError ? recordError(primaryError) : void 0;
|
|
1676
|
+
const attached = await attachments.close();
|
|
1410
1677
|
const endedAt = now().toISOString();
|
|
1411
|
-
|
|
1678
|
+
const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
1679
|
+
await runWriter.finalize({
|
|
1412
1680
|
status,
|
|
1413
1681
|
endedAt,
|
|
1414
|
-
durationMs
|
|
1682
|
+
durationMs,
|
|
1415
1683
|
scoring,
|
|
1416
1684
|
...artifacts ? { artifacts } : {},
|
|
1685
|
+
...attached.length ? { attachments: attached } : {},
|
|
1417
1686
|
...error ? { error } : {}
|
|
1418
1687
|
});
|
|
1419
|
-
if (!options.runWriter) {
|
|
1420
|
-
const counts = trialCounts([{
|
|
1421
|
-
status,
|
|
1422
|
-
scoring
|
|
1423
|
-
}]);
|
|
1424
|
-
await runWriter.finalize({
|
|
1425
|
-
status,
|
|
1426
|
-
endedAt,
|
|
1427
|
-
durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
|
|
1428
|
-
trialCount: 1,
|
|
1429
|
-
passed: counts.passed,
|
|
1430
|
-
failed: counts.failed,
|
|
1431
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {},
|
|
1432
|
-
...error ? { error } : {}
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
1688
|
return {
|
|
1436
1689
|
runId,
|
|
1437
1690
|
runUri: canonicalId("run", runId),
|
|
1438
|
-
trialId,
|
|
1439
|
-
trialUri: canonicalId("trial", trialId),
|
|
1440
|
-
trialIndex: options.trialIndex ?? 0,
|
|
1441
1691
|
evalId: definition.id,
|
|
1442
1692
|
status,
|
|
1693
|
+
...batch ? { batch } : {},
|
|
1443
1694
|
reportLocation: runWriter.location,
|
|
1444
|
-
durationMs
|
|
1695
|
+
durationMs,
|
|
1445
1696
|
scoring,
|
|
1446
1697
|
...error ? { error } : {}
|
|
1447
1698
|
};
|
|
@@ -1455,23 +1706,9 @@ function datasetValues(prepared, audience) {
|
|
|
1455
1706
|
}
|
|
1456
1707
|
return values;
|
|
1457
1708
|
}
|
|
1458
|
-
/**
|
|
1459
|
-
function trialCounts(trials) {
|
|
1460
|
-
let passed = 0;
|
|
1461
|
-
let failed = 0;
|
|
1462
|
-
let cancelledTrials = 0;
|
|
1463
|
-
for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
|
|
1464
|
-
else if (trial.status === "completed" && trial.scoring?.passed) passed++;
|
|
1465
|
-
else failed++;
|
|
1466
|
-
return {
|
|
1467
|
-
passed,
|
|
1468
|
-
failed,
|
|
1469
|
-
cancelled: cancelledTrials
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
/** Persistent workspaces of trials that did not pass are kept for debugging by default. */
|
|
1709
|
+
/** Persistent workspaces of runs that did not pass are kept for debugging by default. */
|
|
1473
1710
|
function keepWorkspace(policy = "failed", passed) {
|
|
1474
1711
|
return policy === "all" || policy === "failed" && !passed;
|
|
1475
1712
|
}
|
|
1476
1713
|
//#endregion
|
|
1477
|
-
export {
|
|
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 };
|