@leostera/evalkit 0.8.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 +46 -2
- 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 +220 -178
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +226 -168
- 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-LeGSyDD2.mjs → src-BrfIXfO2.mjs} +72 -54
- package/dist/{src-mm8L_IxM.mjs → src-CcdLZqFk.mjs} +422 -230
- package/dist/upgrade-60SIRius.mjs +110 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-C12IeG2y.css +0 -2
- package/dist/dashboard/assets/index-xlVsW6ZR.js +0 -37
|
@@ -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, `**`
|
|
@@ -104,9 +178,9 @@ function ignoreRules(patterns) {
|
|
|
104
178
|
};
|
|
105
179
|
}
|
|
106
180
|
/**
|
|
107
|
-
* Copies the candidate workspace's regular files into the
|
|
181
|
+
* Copies the candidate workspace's regular files into the run report,
|
|
108
182
|
* leaving out ignored paths. Symbolic links and special files are skipped,
|
|
109
|
-
* never followed, so a snapshot cannot read outside the
|
|
183
|
+
* never followed, so a snapshot cannot read outside the run. An ignored
|
|
110
184
|
* directory is walked only when a `!` pattern could re-include something in
|
|
111
185
|
* it, and is listed only if it ends up containing a snapshotted file.
|
|
112
186
|
*/
|
|
@@ -159,7 +233,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAUL
|
|
|
159
233
|
const loads = /* @__PURE__ */ new WeakMap();
|
|
160
234
|
/**
|
|
161
235
|
* Loads a dataset at most once per process and project root. A failed load is
|
|
162
|
-
* forgotten, so the next
|
|
236
|
+
* forgotten, so the next run that needs the dataset tries again.
|
|
163
237
|
*/
|
|
164
238
|
function loadDataset(dataset, root, signal) {
|
|
165
239
|
let byRoot = loads.get(dataset);
|
|
@@ -210,12 +284,12 @@ async function readValueFile(id, file) {
|
|
|
210
284
|
}
|
|
211
285
|
throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
|
|
212
286
|
}
|
|
213
|
-
/** Loads (cached), derives this
|
|
214
|
-
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) {
|
|
215
289
|
const prepared = [];
|
|
216
290
|
for (const dataset of datasets) {
|
|
217
|
-
const loaded = await loadDataset(dataset, root,
|
|
218
|
-
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;
|
|
219
293
|
if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
|
|
220
294
|
prepared.push(await resolveContent(dataset, content, root));
|
|
221
295
|
}
|
|
@@ -283,7 +357,7 @@ async function digest(files, value) {
|
|
|
283
357
|
if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
|
|
284
358
|
return `sha256:${hash.digest("hex")}`;
|
|
285
359
|
}
|
|
286
|
-
function
|
|
360
|
+
function runDatasets(prepared) {
|
|
287
361
|
return prepared.map(({ dataset, digest }) => ({
|
|
288
362
|
id: dataset.id,
|
|
289
363
|
visibility: dataset.visibility,
|
|
@@ -332,17 +406,17 @@ async function staticDatasetFile(dataset, root, file) {
|
|
|
332
406
|
}
|
|
333
407
|
//#endregion
|
|
334
408
|
//#region ../runner/src/workspace.ts
|
|
335
|
-
/** Where a
|
|
336
|
-
function
|
|
337
|
-
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, "_"));
|
|
338
412
|
}
|
|
339
413
|
/**
|
|
340
414
|
* Creates isolated candidate and evaluator directories and writes each
|
|
341
415
|
* dataset's files below its mount, on the side its visibility allows.
|
|
342
416
|
* Two datasets writing the same file is an error, never an overwrite.
|
|
343
417
|
*/
|
|
344
|
-
async function
|
|
345
|
-
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-"));
|
|
346
420
|
const persistent = persistentRoot !== void 0;
|
|
347
421
|
const artifacts = {
|
|
348
422
|
candidate: { root: path.join(root, "candidate") },
|
|
@@ -394,6 +468,20 @@ function collision(datasetId, file, cause) {
|
|
|
394
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));
|
|
395
469
|
}
|
|
396
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
|
|
397
485
|
//#region ../runner/src/score.ts
|
|
398
486
|
/**
|
|
399
487
|
* Shared normalization for inline and final predicate and judge rules.
|
|
@@ -447,6 +535,7 @@ function runJudgeAgent(rule, agent, input, placement) {
|
|
|
447
535
|
yield* Effect.acquireUseRelease(Effect.tryPromise({
|
|
448
536
|
try: () => agent.start({
|
|
449
537
|
context,
|
|
538
|
+
attach: input.attach,
|
|
450
539
|
onEvent: (event) => {
|
|
451
540
|
const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
|
|
452
541
|
...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
|
|
@@ -583,7 +672,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
|
|
|
583
672
|
const annotations = {
|
|
584
673
|
evalId: input.context.evalId,
|
|
585
674
|
runId: input.context.runId,
|
|
586
|
-
trialId: input.context.trialId,
|
|
587
675
|
step: index,
|
|
588
676
|
checkpoint: step.name
|
|
589
677
|
};
|
|
@@ -682,7 +770,7 @@ function userMessage(step, index, { context, randomSeed }) {
|
|
|
682
770
|
parameters: context.parameters ?? {},
|
|
683
771
|
data: context.data,
|
|
684
772
|
seed: randomSeed,
|
|
685
|
-
|
|
773
|
+
...context.batch ? { batch: context.batch } : {}
|
|
686
774
|
});
|
|
687
775
|
if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
|
|
688
776
|
return message;
|
|
@@ -713,7 +801,9 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
713
801
|
turn: progress.lastTurn,
|
|
714
802
|
...turnOutput(progress.lastTurn),
|
|
715
803
|
data: input.data,
|
|
716
|
-
checkpoints: [...progress.checkpoints]
|
|
804
|
+
checkpoints: [...progress.checkpoints],
|
|
805
|
+
checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
|
|
806
|
+
attach: input.attach
|
|
717
807
|
}, now, definition.judge)).catch(async (error) => {
|
|
718
808
|
const recorded = recordError(error);
|
|
719
809
|
progress.checkpoints.push({
|
|
@@ -779,7 +869,9 @@ function scoringContext(input) {
|
|
|
779
869
|
...input.lastTurn ? { turn: input.lastTurn } : {},
|
|
780
870
|
...turnOutput(input.lastTurn),
|
|
781
871
|
data: input.data,
|
|
782
|
-
checkpoints: input.checkpoints
|
|
872
|
+
checkpoints: input.checkpoints,
|
|
873
|
+
checkpoint: checkpointLookup(input.transcript, input.checkpoints),
|
|
874
|
+
attach: input.attach
|
|
783
875
|
};
|
|
784
876
|
}
|
|
785
877
|
/** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
|
|
@@ -801,7 +893,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
801
893
|
const score = await runBoundary(evaluateRule(scorer, scoringContext({
|
|
802
894
|
...input,
|
|
803
895
|
context,
|
|
804
|
-
workspace
|
|
896
|
+
workspace,
|
|
897
|
+
transcript: definition.transcript
|
|
805
898
|
}), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
806
899
|
cause,
|
|
807
900
|
message: cause.message
|
|
@@ -841,7 +934,7 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
841
934
|
//#endregion
|
|
842
935
|
//#region ../runner/src/metrics.ts
|
|
843
936
|
/**
|
|
844
|
-
* What EvalKit measures for every
|
|
937
|
+
* What EvalKit measures for every run from its trajectory: the agent's time
|
|
845
938
|
* (first event to the last one before final scoring), tokens when the agent
|
|
846
939
|
* reported usage, and tool calls.
|
|
847
940
|
*/
|
|
@@ -857,8 +950,7 @@ function builtInMetrics(events) {
|
|
|
857
950
|
});
|
|
858
951
|
let tokens;
|
|
859
952
|
for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
|
|
860
|
-
const
|
|
861
|
-
const turn = totalTokens ?? (inputTokens === void 0 && outputTokens === void 0 ? void 0 : (inputTokens ?? 0) + (outputTokens ?? 0));
|
|
953
|
+
const turn = turnTokens(event.usage);
|
|
862
954
|
if (turn !== void 0) tokens = (tokens ?? 0) + turn;
|
|
863
955
|
}
|
|
864
956
|
if (tokens !== void 0) metrics.push({
|
|
@@ -903,7 +995,7 @@ async function evaluateMetrics(definition, context) {
|
|
|
903
995
|
//#endregion
|
|
904
996
|
//#region ../runner/src/local-report-store.ts
|
|
905
997
|
const MANIFEST_FILE = "manifest.json";
|
|
906
|
-
/** The project's evals and suites when the run started
|
|
998
|
+
/** The project's evals and suites when the run started. */
|
|
907
999
|
const CATALOG_FILE = "catalog.json";
|
|
908
1000
|
async function writeJson(filePath, value) {
|
|
909
1001
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -922,10 +1014,12 @@ function artifactPath(root, value) {
|
|
|
922
1014
|
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
|
|
923
1015
|
return path.join(root, normalized);
|
|
924
1016
|
}
|
|
925
|
-
var
|
|
1017
|
+
var LocalRunWriter = class {
|
|
1018
|
+
location;
|
|
926
1019
|
directory;
|
|
927
1020
|
metadata;
|
|
928
|
-
constructor(directory, metadata) {
|
|
1021
|
+
constructor(location, directory, metadata) {
|
|
1022
|
+
this.location = location;
|
|
929
1023
|
this.directory = directory;
|
|
930
1024
|
this.metadata = metadata;
|
|
931
1025
|
}
|
|
@@ -933,40 +1027,13 @@ var LocalTrialWriter = class {
|
|
|
933
1027
|
return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
|
|
934
1028
|
}
|
|
935
1029
|
writeScores(scoring) {
|
|
936
|
-
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(
|
|
1030
|
+
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
|
|
937
1031
|
}
|
|
938
1032
|
async writeArtifact(relativePath, data) {
|
|
939
1033
|
const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
|
|
940
1034
|
await mkdir(path.dirname(target), { recursive: true });
|
|
941
1035
|
await writeFile(target, data);
|
|
942
1036
|
}
|
|
943
|
-
async finalize(summary) {
|
|
944
|
-
await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(TrialSummarySchema)(summary));
|
|
945
|
-
await replaceJson(path.join(this.directory, MANIFEST_FILE), {
|
|
946
|
-
...Schema.encodeSync(TrialMetadataSchema)(this.metadata),
|
|
947
|
-
status: summary.status
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
var LocalRunWriter = class {
|
|
952
|
-
location;
|
|
953
|
-
directory;
|
|
954
|
-
metadata;
|
|
955
|
-
constructor(location, directory, metadata) {
|
|
956
|
-
this.location = location;
|
|
957
|
-
this.directory = directory;
|
|
958
|
-
this.metadata = metadata;
|
|
959
|
-
}
|
|
960
|
-
async startTrial(metadata) {
|
|
961
|
-
assertPathSegment(metadata.trialId, "trialId");
|
|
962
|
-
const directory = path.join(this.directory, "trials", metadata.trialId);
|
|
963
|
-
await mkdir(directory, { recursive: true });
|
|
964
|
-
await writeJson(path.join(directory, MANIFEST_FILE), {
|
|
965
|
-
...Schema.encodeSync(TrialMetadataSchema)(metadata),
|
|
966
|
-
status: "running"
|
|
967
|
-
});
|
|
968
|
-
return new LocalTrialWriter(directory, metadata);
|
|
969
|
-
}
|
|
970
1037
|
async finalize(summary) {
|
|
971
1038
|
await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
|
|
972
1039
|
await replaceJson(path.join(this.directory, MANIFEST_FILE), {
|
|
@@ -991,17 +1058,15 @@ function localReportStore(rootDirectory) {
|
|
|
991
1058
|
}
|
|
992
1059
|
//#endregion
|
|
993
1060
|
//#region ../runner/src/local-report-reader.ts
|
|
994
|
-
/**
|
|
1061
|
+
/** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
|
|
995
1062
|
async function decode(file, schema) {
|
|
996
1063
|
return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
|
|
997
1064
|
}
|
|
998
1065
|
const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
|
|
999
1066
|
const readRunSummary = (root, runId) => decode(join(root, runId, "summary.json"), RunSummarySchema);
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
async function readTrialEvents(root, runId, trialId) {
|
|
1004
|
-
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"));
|
|
1005
1070
|
}
|
|
1006
1071
|
/**
|
|
1007
1072
|
* True when a run's owning process is known to be gone: it ran on this host
|
|
@@ -1028,6 +1093,7 @@ function runMatrix(matrix, options) {
|
|
|
1028
1093
|
const iterator = matrix.cells(options.selection);
|
|
1029
1094
|
const summary = {
|
|
1030
1095
|
cells: 0,
|
|
1096
|
+
runs: 0,
|
|
1031
1097
|
passed: 0,
|
|
1032
1098
|
failed: 0,
|
|
1033
1099
|
cancelled: 0
|
|
@@ -1043,7 +1109,7 @@ function runMatrix(matrix, options) {
|
|
|
1043
1109
|
"sandbox"
|
|
1044
1110
|
].find((name) => agentRuntimes(cell.eval.agent)[name]);
|
|
1045
1111
|
const suiteId = options.suiteId ?? options.suiteFor?.(cell);
|
|
1046
|
-
const
|
|
1112
|
+
const runs = yield* runEval(cell.eval, {
|
|
1047
1113
|
...options,
|
|
1048
1114
|
...suiteId ? { suiteId } : {},
|
|
1049
1115
|
concurrency: 1,
|
|
@@ -1055,18 +1121,232 @@ function runMatrix(matrix, options) {
|
|
|
1055
1121
|
}
|
|
1056
1122
|
});
|
|
1057
1123
|
summary.cells++;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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));
|
|
1062
1131
|
}
|
|
1063
1132
|
})), { concurrency });
|
|
1064
1133
|
return summary;
|
|
1065
1134
|
});
|
|
1066
1135
|
}
|
|
1067
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
|
|
1068
1344
|
//#region ../runner/src/index.ts
|
|
1069
|
-
/**
|
|
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
|
+
*/
|
|
1070
1350
|
function runEval(definition, options) {
|
|
1071
1351
|
return Effect.gen(function* () {
|
|
1072
1352
|
yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
|
|
@@ -1074,19 +1354,19 @@ function runEval(definition, options) {
|
|
|
1074
1354
|
...options.runId ? { runId: options.runId } : {},
|
|
1075
1355
|
...options.suiteId ? { suiteId: options.suiteId } : {}
|
|
1076
1356
|
}));
|
|
1077
|
-
const
|
|
1078
|
-
try: () =>
|
|
1357
|
+
const runs = yield* Effect.tryPromise({
|
|
1358
|
+
try: () => executeBatch(definition, options),
|
|
1079
1359
|
catch: (error) => error
|
|
1080
1360
|
});
|
|
1081
1361
|
yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
|
|
1082
1362
|
evalId: authoringId(definition),
|
|
1083
|
-
|
|
1084
|
-
|
|
1363
|
+
runs: runs.length,
|
|
1364
|
+
passed: runs.filter((run) => run.status === "passed").length
|
|
1085
1365
|
}));
|
|
1086
|
-
return
|
|
1366
|
+
return runs;
|
|
1087
1367
|
});
|
|
1088
1368
|
}
|
|
1089
|
-
function createId(
|
|
1369
|
+
function createId() {
|
|
1090
1370
|
return crypto.randomUUID();
|
|
1091
1371
|
}
|
|
1092
1372
|
function canonicalId(kind, id) {
|
|
@@ -1095,9 +1375,9 @@ function canonicalId(kind, id) {
|
|
|
1095
1375
|
function assertUuid(value, label) {
|
|
1096
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`);
|
|
1097
1377
|
}
|
|
1098
|
-
function runMetadata(definition, options, runId, startedAt) {
|
|
1378
|
+
function runMetadata(definition, options, runId, startedAt, extra) {
|
|
1099
1379
|
return {
|
|
1100
|
-
schemaVersion:
|
|
1380
|
+
schemaVersion: 6,
|
|
1101
1381
|
runId,
|
|
1102
1382
|
runUri: canonicalId("run", runId),
|
|
1103
1383
|
evalId: definition.id,
|
|
@@ -1110,7 +1390,9 @@ function runMetadata(definition, options, runId, startedAt) {
|
|
|
1110
1390
|
...definition.agent.identity ? { aut: definition.agent.identity } : {},
|
|
1111
1391
|
parameters: options.parameters,
|
|
1112
1392
|
matrix: options.matrix,
|
|
1113
|
-
startedAt
|
|
1393
|
+
startedAt,
|
|
1394
|
+
...options.batch ? { batch: options.batch } : {},
|
|
1395
|
+
...extra
|
|
1114
1396
|
};
|
|
1115
1397
|
}
|
|
1116
1398
|
/** Recording a secondary error must never mask the original failure or block cleanup. */
|
|
@@ -1123,104 +1405,52 @@ async function tryEmitError(error, emitRunner, now) {
|
|
|
1123
1405
|
});
|
|
1124
1406
|
} catch {}
|
|
1125
1407
|
}
|
|
1126
|
-
/**
|
|
1127
|
-
async function
|
|
1408
|
+
/** Starts `repeat` runs of a scenario, as one batch when there are several. */
|
|
1409
|
+
async function executeBatch(definition, options) {
|
|
1128
1410
|
let hasUser = false;
|
|
1129
1411
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
1130
1412
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
1131
1413
|
if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
|
|
1414
|
+
assertNoRemovedOptions(definition);
|
|
1132
1415
|
if (options.runId) assertUuid(options.runId, "runId");
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
if (
|
|
1136
|
-
if (
|
|
1137
|
-
const
|
|
1138
|
-
const aggregateStartedAt = now();
|
|
1139
|
-
const runId = options.runId ?? createId("run");
|
|
1140
|
-
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();
|
|
1141
1421
|
const signal = options.signal;
|
|
1142
|
-
const
|
|
1143
|
-
const
|
|
1144
|
-
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, {
|
|
1145
1425
|
...options,
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1426
|
+
batch: {
|
|
1427
|
+
id: batchId,
|
|
1428
|
+
index,
|
|
1429
|
+
size: repeat
|
|
1430
|
+
}
|
|
1151
1431
|
}),
|
|
1152
1432
|
catch: (error) => error
|
|
1153
1433
|
});
|
|
1154
|
-
return options.semaphore ? options.semaphore.withPermits(1)(
|
|
1434
|
+
return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
|
|
1155
1435
|
});
|
|
1156
|
-
|
|
1157
|
-
const counts = trialCounts(trials);
|
|
1158
|
-
const status = counts.cancelled || signal?.aborted ? "cancelled" : trials.some((trial) => trial.status === "failed") ? "failed" : "completed";
|
|
1159
|
-
const endedAt = now();
|
|
1160
|
-
await runWriter.finalize({
|
|
1161
|
-
status,
|
|
1162
|
-
endedAt: endedAt.toISOString(),
|
|
1163
|
-
durationMs: endedAt.getTime() - aggregateStartedAt.getTime(),
|
|
1164
|
-
trialCount: trials.length,
|
|
1165
|
-
passed: counts.passed,
|
|
1166
|
-
failed: counts.failed,
|
|
1167
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {}
|
|
1168
|
-
});
|
|
1169
|
-
if (!trials.length) return {
|
|
1170
|
-
runId,
|
|
1171
|
-
runUri: canonicalId("run", runId),
|
|
1172
|
-
trialId: "",
|
|
1173
|
-
trialUri: canonicalId("trial", runId),
|
|
1174
|
-
trialIndex: 0,
|
|
1175
|
-
evalId: authoringId(definition),
|
|
1176
|
-
status,
|
|
1177
|
-
reportLocation: runWriter.location,
|
|
1178
|
-
trialCount: 0,
|
|
1179
|
-
passed: 0,
|
|
1180
|
-
failed: 0,
|
|
1181
|
-
aggregateScoring: {
|
|
1182
|
-
passed: 0,
|
|
1183
|
-
failed: 0,
|
|
1184
|
-
passRate: 0
|
|
1185
|
-
},
|
|
1186
|
-
trials: []
|
|
1187
|
-
};
|
|
1188
|
-
const overallScores = trials.map((trial) => trial.scoring?.overall).filter((score) => score !== void 0);
|
|
1189
|
-
return {
|
|
1190
|
-
...trials[0],
|
|
1191
|
-
runId,
|
|
1192
|
-
evalId: authoringId(definition),
|
|
1193
|
-
status,
|
|
1194
|
-
trialCount: trials.length,
|
|
1195
|
-
passed: counts.passed,
|
|
1196
|
-
failed: counts.failed,
|
|
1197
|
-
aggregateScoring: {
|
|
1198
|
-
passed: counts.passed,
|
|
1199
|
-
failed: counts.failed,
|
|
1200
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {},
|
|
1201
|
-
passRate: counts.passed / trials.length,
|
|
1202
|
-
...overallScores.length ? { overall: overallScores.reduce((sum, score) => sum + score, 0) / overallScores.length } : {}
|
|
1203
|
-
},
|
|
1204
|
-
trials
|
|
1205
|
-
};
|
|
1436
|
+
return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
|
|
1206
1437
|
}
|
|
1207
|
-
|
|
1438
|
+
/** One execution: a session in its own sandbox, scored and reported. */
|
|
1439
|
+
async function runOnce(definition, options) {
|
|
1208
1440
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1209
|
-
const runId = options.runId ?? createId(
|
|
1210
|
-
const trialId = options.trialId ?? createId("trial");
|
|
1441
|
+
const runId = options.runId ?? createId();
|
|
1211
1442
|
const startedAt = now().toISOString();
|
|
1212
1443
|
const selectedRuntime = options.runtime;
|
|
1213
1444
|
const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
|
|
1214
1445
|
if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
|
|
1215
1446
|
const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
|
|
1216
|
-
const
|
|
1447
|
+
const batch = options.batch;
|
|
1217
1448
|
const signal = options.signal ?? new AbortController().signal;
|
|
1218
1449
|
const root = options.root ?? process.cwd();
|
|
1219
1450
|
const baseContext = {
|
|
1220
1451
|
runId,
|
|
1221
1452
|
evalId: authoringId(definition),
|
|
1222
|
-
|
|
1223
|
-
trialIndex,
|
|
1453
|
+
...batch ? { batch } : {},
|
|
1224
1454
|
metadata: {
|
|
1225
1455
|
...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
|
|
1226
1456
|
randomSeed
|
|
@@ -1234,8 +1464,7 @@ async function runTrial(definition, options) {
|
|
|
1234
1464
|
prepared = await prepareDatasets(definition.datasets ?? [], root, {
|
|
1235
1465
|
evalId: baseContext.evalId,
|
|
1236
1466
|
runId,
|
|
1237
|
-
|
|
1238
|
-
trialIndex,
|
|
1467
|
+
...batch ? { batch } : {},
|
|
1239
1468
|
seed: randomSeed,
|
|
1240
1469
|
parameters: options.parameters ?? {},
|
|
1241
1470
|
signal
|
|
@@ -1247,40 +1476,28 @@ async function runTrial(definition, options) {
|
|
|
1247
1476
|
const scoringData = datasetValues(prepared, "all");
|
|
1248
1477
|
const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
|
|
1249
1478
|
const events = [];
|
|
1250
|
-
const runWriter =
|
|
1251
|
-
const trialWriter = await runWriter.startTrial({
|
|
1252
|
-
schemaVersion: 5,
|
|
1253
|
-
runId,
|
|
1254
|
-
runUri: canonicalId("run", runId),
|
|
1255
|
-
trialId,
|
|
1256
|
-
trialUri: canonicalId("trial", trialId),
|
|
1257
|
-
trialIndex: options.trialIndex ?? 0,
|
|
1258
|
-
evalId: definition.id,
|
|
1259
|
-
...definition.agent.identity ? { aut: definition.agent.identity } : {},
|
|
1260
|
-
parameters: options.parameters,
|
|
1261
|
-
matrix: options.matrix,
|
|
1262
|
-
startedAt,
|
|
1479
|
+
const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
|
|
1263
1480
|
workspaceIgnore,
|
|
1264
|
-
...prepared.length ? { datasets:
|
|
1265
|
-
});
|
|
1481
|
+
...prepared.length ? { datasets: runDatasets(prepared) } : {}
|
|
1482
|
+
}), { catalog: options.catalog });
|
|
1483
|
+
const attachments = attachmentStore(runWriter, options.attachmentLimits);
|
|
1266
1484
|
let writeChain = Promise.resolve();
|
|
1267
1485
|
let reportingFailed = false;
|
|
1268
|
-
const
|
|
1486
|
+
const runRef = {
|
|
1269
1487
|
runId,
|
|
1270
|
-
|
|
1271
|
-
trialIndex,
|
|
1488
|
+
...batch ? { batch } : {},
|
|
1272
1489
|
evalId: definition.id,
|
|
1273
1490
|
...options.suiteId ? { suiteId: options.suiteId } : {},
|
|
1274
1491
|
...options.parameters ? { parameters: options.parameters } : {}
|
|
1275
1492
|
};
|
|
1276
1493
|
const observe = (event) => {
|
|
1277
1494
|
try {
|
|
1278
|
-
options.onEvent?.(
|
|
1495
|
+
options.onEvent?.(runRef, event);
|
|
1279
1496
|
} catch {}
|
|
1280
1497
|
};
|
|
1281
1498
|
const append = async (event) => {
|
|
1282
1499
|
events.push(event);
|
|
1283
|
-
writeChain = writeChain.then(() =>
|
|
1500
|
+
writeChain = writeChain.then(() => runWriter.appendEvent(event));
|
|
1284
1501
|
try {
|
|
1285
1502
|
await writeChain;
|
|
1286
1503
|
observe(event);
|
|
@@ -1316,7 +1533,7 @@ async function runTrial(definition, options) {
|
|
|
1316
1533
|
const workspaceScope = await Effect.runPromise(Scope.make());
|
|
1317
1534
|
try {
|
|
1318
1535
|
await emitRunner({
|
|
1319
|
-
kind: "
|
|
1536
|
+
kind: "run-started",
|
|
1320
1537
|
timestamp: now().toISOString()
|
|
1321
1538
|
});
|
|
1322
1539
|
if (signal.aborted) throw cancelled();
|
|
@@ -1325,7 +1542,7 @@ async function runTrial(definition, options) {
|
|
|
1325
1542
|
message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
|
|
1326
1543
|
});
|
|
1327
1544
|
workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
|
|
1328
|
-
try: () =>
|
|
1545
|
+
try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
|
|
1329
1546
|
catch: (cause) => new DatasetError({
|
|
1330
1547
|
cause,
|
|
1331
1548
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -1338,20 +1555,21 @@ async function runTrial(definition, options) {
|
|
|
1338
1555
|
})
|
|
1339
1556
|
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
1340
1557
|
cleanupError = error;
|
|
1341
|
-
}).pipe(Effect.tap(() => Effect.logError("
|
|
1342
|
-
const
|
|
1343
|
-
const
|
|
1558
|
+
}).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
|
|
1559
|
+
const runWorkspace = workspace;
|
|
1560
|
+
const runContext = {
|
|
1344
1561
|
...baseContext,
|
|
1345
1562
|
data: candidateData,
|
|
1346
|
-
workspace:
|
|
1347
|
-
evaluatorWorkspace:
|
|
1563
|
+
workspace: runWorkspace.artifacts.candidate,
|
|
1564
|
+
evaluatorWorkspace: runWorkspace.artifacts.evaluator
|
|
1348
1565
|
};
|
|
1349
|
-
context =
|
|
1566
|
+
context = runContext;
|
|
1350
1567
|
await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
|
|
1351
1568
|
try: () => definition.agent.start({
|
|
1352
|
-
context:
|
|
1569
|
+
context: runContext,
|
|
1353
1570
|
...runtime ? { runtime } : {},
|
|
1354
|
-
onEvent: onAutEvent
|
|
1571
|
+
onEvent: onAutEvent,
|
|
1572
|
+
attach: attachments.attach
|
|
1355
1573
|
}),
|
|
1356
1574
|
catch: (cause) => new AutExecutionError({
|
|
1357
1575
|
cause,
|
|
@@ -1359,8 +1577,8 @@ async function runTrial(definition, options) {
|
|
|
1359
1577
|
})
|
|
1360
1578
|
}), (session) => Effect.tryPromise({
|
|
1361
1579
|
try: () => executeTranscript(definition, session, {
|
|
1362
|
-
context:
|
|
1363
|
-
artifacts:
|
|
1580
|
+
context: runContext,
|
|
1581
|
+
artifacts: runWorkspace.artifacts,
|
|
1364
1582
|
events,
|
|
1365
1583
|
now,
|
|
1366
1584
|
randomSeed,
|
|
@@ -1368,7 +1586,8 @@ async function runTrial(definition, options) {
|
|
|
1368
1586
|
emitAut,
|
|
1369
1587
|
data: scoringData,
|
|
1370
1588
|
signal,
|
|
1371
|
-
progress
|
|
1589
|
+
progress,
|
|
1590
|
+
attach: attachments.attach
|
|
1372
1591
|
}),
|
|
1373
1592
|
catch: (cause) => cause
|
|
1374
1593
|
}), (opened) => Effect.tryPromise({
|
|
@@ -1381,7 +1600,6 @@ async function runTrial(definition, options) {
|
|
|
1381
1600
|
closeError = error;
|
|
1382
1601
|
}).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
|
|
1383
1602
|
runId,
|
|
1384
|
-
trialId,
|
|
1385
1603
|
evalId: definition.id
|
|
1386
1604
|
})));
|
|
1387
1605
|
} catch (error) {
|
|
@@ -1411,6 +1629,7 @@ async function runTrial(definition, options) {
|
|
|
1411
1629
|
skippedScorers,
|
|
1412
1630
|
data: scoringData,
|
|
1413
1631
|
checkpoints,
|
|
1632
|
+
attach: attachments.attach,
|
|
1414
1633
|
now,
|
|
1415
1634
|
emitRunner
|
|
1416
1635
|
});
|
|
@@ -1421,12 +1640,14 @@ async function runTrial(definition, options) {
|
|
|
1421
1640
|
events,
|
|
1422
1641
|
lastTurn,
|
|
1423
1642
|
data: scoringData,
|
|
1424
|
-
checkpoints
|
|
1643
|
+
checkpoints,
|
|
1644
|
+
transcript: definition.transcript,
|
|
1645
|
+
attach: attachments.attach
|
|
1425
1646
|
})));
|
|
1426
1647
|
scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1427
|
-
await
|
|
1648
|
+
await runWriter.writeScores(scoring);
|
|
1428
1649
|
if (workspace) try {
|
|
1429
|
-
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root,
|
|
1650
|
+
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
|
|
1430
1651
|
} catch (captureError) {
|
|
1431
1652
|
if (!primaryError) primaryError = captureError;
|
|
1432
1653
|
await tryEmitError(captureError, emitRunner, now);
|
|
@@ -1444,49 +1665,34 @@ async function runTrial(definition, options) {
|
|
|
1444
1665
|
scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1445
1666
|
if (!primaryError) try {
|
|
1446
1667
|
await emitRunner({
|
|
1447
|
-
kind: "
|
|
1668
|
+
kind: "run-completed",
|
|
1448
1669
|
timestamp: now().toISOString()
|
|
1449
1670
|
});
|
|
1450
1671
|
} catch (error) {
|
|
1451
1672
|
primaryError = error;
|
|
1452
1673
|
}
|
|
1453
|
-
status = wasCancelled ? "cancelled" : primaryError ? "
|
|
1674
|
+
status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
|
|
1454
1675
|
const error = primaryError ? recordError(primaryError) : void 0;
|
|
1676
|
+
const attached = await attachments.close();
|
|
1455
1677
|
const endedAt = now().toISOString();
|
|
1456
|
-
|
|
1678
|
+
const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
1679
|
+
await runWriter.finalize({
|
|
1457
1680
|
status,
|
|
1458
1681
|
endedAt,
|
|
1459
|
-
durationMs
|
|
1682
|
+
durationMs,
|
|
1460
1683
|
scoring,
|
|
1461
1684
|
...artifacts ? { artifacts } : {},
|
|
1685
|
+
...attached.length ? { attachments: attached } : {},
|
|
1462
1686
|
...error ? { error } : {}
|
|
1463
1687
|
});
|
|
1464
|
-
if (!options.runWriter) {
|
|
1465
|
-
const counts = trialCounts([{
|
|
1466
|
-
status,
|
|
1467
|
-
scoring
|
|
1468
|
-
}]);
|
|
1469
|
-
await runWriter.finalize({
|
|
1470
|
-
status,
|
|
1471
|
-
endedAt,
|
|
1472
|
-
durationMs: new Date(endedAt).getTime() - new Date(startedAt).getTime(),
|
|
1473
|
-
trialCount: 1,
|
|
1474
|
-
passed: counts.passed,
|
|
1475
|
-
failed: counts.failed,
|
|
1476
|
-
...counts.cancelled ? { cancelled: counts.cancelled } : {},
|
|
1477
|
-
...error ? { error } : {}
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
1688
|
return {
|
|
1481
1689
|
runId,
|
|
1482
1690
|
runUri: canonicalId("run", runId),
|
|
1483
|
-
trialId,
|
|
1484
|
-
trialUri: canonicalId("trial", trialId),
|
|
1485
|
-
trialIndex: options.trialIndex ?? 0,
|
|
1486
1691
|
evalId: definition.id,
|
|
1487
1692
|
status,
|
|
1693
|
+
...batch ? { batch } : {},
|
|
1488
1694
|
reportLocation: runWriter.location,
|
|
1489
|
-
durationMs
|
|
1695
|
+
durationMs,
|
|
1490
1696
|
scoring,
|
|
1491
1697
|
...error ? { error } : {}
|
|
1492
1698
|
};
|
|
@@ -1500,23 +1706,9 @@ function datasetValues(prepared, audience) {
|
|
|
1500
1706
|
}
|
|
1501
1707
|
return values;
|
|
1502
1708
|
}
|
|
1503
|
-
/**
|
|
1504
|
-
function trialCounts(trials) {
|
|
1505
|
-
let passed = 0;
|
|
1506
|
-
let failed = 0;
|
|
1507
|
-
let cancelledTrials = 0;
|
|
1508
|
-
for (const trial of trials) if (trial.status === "cancelled") cancelledTrials++;
|
|
1509
|
-
else if (trial.status === "completed" && trial.scoring?.passed) passed++;
|
|
1510
|
-
else failed++;
|
|
1511
|
-
return {
|
|
1512
|
-
passed,
|
|
1513
|
-
failed,
|
|
1514
|
-
cancelled: cancelledTrials
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
/** 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. */
|
|
1518
1710
|
function keepWorkspace(policy = "failed", passed) {
|
|
1519
1711
|
return policy === "all" || policy === "failed" && !passed;
|
|
1520
1712
|
}
|
|
1521
1713
|
//#endregion
|
|
1522
|
-
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 };
|