@leostera/evalkit 0.8.0 → 0.10.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 +93 -2
- package/README.md +5 -4
- package/dist/cli.mjs +510 -379
- package/dist/dashboard/assets/index-DNR4shrM.js +67 -0
- package/dist/dashboard/assets/index-TPYprg75.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-B1FeXzmS.mjs} +333 -51
- package/dist/index.d.mts +529 -183
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +447 -173
- 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 +5 -5
- 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 +8 -6
- package/dist/skills/evalkit-matrices/SKILL.md +2 -2
- package/dist/skills/evalkit-results/SKILL.md +6 -6
- package/dist/{src-LeGSyDD2.mjs → src-B14fXAGp.mjs} +334 -73
- package/dist/{src-CZnDXP1K.mjs → src-Bccv9igQ.mjs} +8 -3
- package/dist/{src-mm8L_IxM.mjs → src-BfSQKOJP.mjs} +676 -321
- package/dist/upgrade-G-uFPJKg.mjs +111 -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 judgeInputNames, B as normalizeAutEvent, G as DEFAULT_ARTIFACT_EXCLUDE, I as RunSummarySchema, K as DEFAULT_ATTACHMENT_LIMITS, L as ScoreResultSchema, M as RunMetadataSchema, N as RunScoringSchema, R as TrajectoryEventSchema, T as JsonValueSchema, U as canonicalParameters, V as turnResultEvents, W as turnTokens, X as answerInstruction, Y as UnreadableAnswer, a as assertJudgesIndependent, at as toJson, et as judgePrompt, gt as resourceUri, i as agentRuntimes, j as RunManifestSchema, lt as relativeDatasetPath, m as recordError, nt as readJudgeAnswer, o as authoringId, ot as toJsonObject, rt as weightedMean, v as parseTrajectoryJsonl, w as CheckpointResultSchema } from "./src-B14fXAGp.mjs";
|
|
2
|
+
import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-G-uFPJKg.mjs";
|
|
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,95 @@ 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
|
+
/**
|
|
109
|
+
* An `attach` that remembers what it saved, so a score or checkpoint can
|
|
110
|
+
* link the files its scorer attached, even when the scorer then failed.
|
|
111
|
+
*/
|
|
112
|
+
function trackAttachments(attach) {
|
|
113
|
+
const attached = [];
|
|
114
|
+
return {
|
|
115
|
+
attached,
|
|
116
|
+
async attach(name, content) {
|
|
117
|
+
await attach(name, content);
|
|
118
|
+
attached.push(name);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** `{ attachments }` when a scorer attached any. */
|
|
123
|
+
const attachedFields = (attached) => attached.length ? { attachments: [...attached] } : {};
|
|
124
|
+
//#endregion
|
|
35
125
|
//#region ../core/src/glob.ts
|
|
36
126
|
/**
|
|
37
127
|
* A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
|
|
@@ -104,9 +194,9 @@ function ignoreRules(patterns) {
|
|
|
104
194
|
};
|
|
105
195
|
}
|
|
106
196
|
/**
|
|
107
|
-
* Copies the candidate workspace's regular files into the
|
|
197
|
+
* Copies the candidate workspace's regular files into the run report,
|
|
108
198
|
* leaving out ignored paths. Symbolic links and special files are skipped,
|
|
109
|
-
* never followed, so a snapshot cannot read outside the
|
|
199
|
+
* never followed, so a snapshot cannot read outside the run. An ignored
|
|
110
200
|
* directory is walked only when a `!` pattern could re-include something in
|
|
111
201
|
* it, and is listed only if it ends up containing a snapshotted file.
|
|
112
202
|
*/
|
|
@@ -159,7 +249,7 @@ async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAUL
|
|
|
159
249
|
const loads = /* @__PURE__ */ new WeakMap();
|
|
160
250
|
/**
|
|
161
251
|
* Loads a dataset at most once per process and project root. A failed load is
|
|
162
|
-
* forgotten, so the next
|
|
252
|
+
* forgotten, so the next run that needs the dataset tries again.
|
|
163
253
|
*/
|
|
164
254
|
function loadDataset(dataset, root, signal) {
|
|
165
255
|
let byRoot = loads.get(dataset);
|
|
@@ -210,12 +300,12 @@ async function readValueFile(id, file) {
|
|
|
210
300
|
}
|
|
211
301
|
throw new Error(`Dataset ${id} valueFile must be a .json or .jsonl file: ${file}`);
|
|
212
302
|
}
|
|
213
|
-
/** Loads (cached), derives this
|
|
214
|
-
async function prepareDatasets(datasets, root,
|
|
303
|
+
/** Loads (cached), derives this run's content with `onRun`, and resolves it. */
|
|
304
|
+
async function prepareDatasets(datasets, root, run) {
|
|
215
305
|
const prepared = [];
|
|
216
306
|
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,
|
|
307
|
+
const loaded = await loadDataset(dataset, root, run.signal);
|
|
308
|
+
const content = dataset.source.kind === "computed" && dataset.source.onRun ? await dataset.source.onRun(loaded, run) : loaded;
|
|
219
309
|
if (typeof content !== "object" || content === null) throw new Error(`Dataset ${dataset.id} onRun must return { files?, value? }`);
|
|
220
310
|
prepared.push(await resolveContent(dataset, content, root));
|
|
221
311
|
}
|
|
@@ -283,7 +373,7 @@ async function digest(files, value) {
|
|
|
283
373
|
if (value !== void 0) hash.update(`value ${canonicalParameters(value)}\n`);
|
|
284
374
|
return `sha256:${hash.digest("hex")}`;
|
|
285
375
|
}
|
|
286
|
-
function
|
|
376
|
+
function runDatasets(prepared) {
|
|
287
377
|
return prepared.map(({ dataset, digest }) => ({
|
|
288
378
|
id: dataset.id,
|
|
289
379
|
visibility: dataset.visibility,
|
|
@@ -332,17 +422,17 @@ async function staticDatasetFile(dataset, root, file) {
|
|
|
332
422
|
}
|
|
333
423
|
//#endregion
|
|
334
424
|
//#region ../runner/src/workspace.ts
|
|
335
|
-
/** Where a
|
|
336
|
-
function
|
|
337
|
-
return path.join(path.resolve(root),
|
|
425
|
+
/** Where a run's persistent workspace lives below `root`. */
|
|
426
|
+
function runWorkspacePath(root, runId) {
|
|
427
|
+
return path.join(path.resolve(root), runId.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
338
428
|
}
|
|
339
429
|
/**
|
|
340
430
|
* Creates isolated candidate and evaluator directories and writes each
|
|
341
431
|
* dataset's files below its mount, on the side its visibility allows.
|
|
342
432
|
* Two datasets writing the same file is an error, never an overwrite.
|
|
343
433
|
*/
|
|
344
|
-
async function
|
|
345
|
-
const root = persistentRoot ?
|
|
434
|
+
async function createRunWorkspace(runId, datasets = [], persistentRoot) {
|
|
435
|
+
const root = persistentRoot ? runWorkspacePath(persistentRoot, runId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-run-"));
|
|
346
436
|
const persistent = persistentRoot !== void 0;
|
|
347
437
|
const artifacts = {
|
|
348
438
|
candidate: { root: path.join(root, "candidate") },
|
|
@@ -394,29 +484,100 @@ function collision(datasetId, file, cause) {
|
|
|
394
484
|
return cause.code === "EEXIST" ? /* @__PURE__ */ new Error(`Dataset ${datasetId} would overwrite ${file}, which another dataset already wrote`) : cause instanceof Error ? cause : new Error(String(cause));
|
|
395
485
|
}
|
|
396
486
|
//#endregion
|
|
487
|
+
//#region ../runner/src/checkpoint-lookup.ts
|
|
488
|
+
/**
|
|
489
|
+
* `checkpoint(rule)` for a scoring context: the latest result of the steps
|
|
490
|
+
* that are this very rule. A rule outside the transcript is a mistake in the
|
|
491
|
+
* scenario, so it throws rather than reading as "not run".
|
|
492
|
+
*/
|
|
493
|
+
function checkpointLookup(transcript, results) {
|
|
494
|
+
return ((rule) => {
|
|
495
|
+
const steps = new Set(transcript.flatMap((step, index) => step === rule ? [index] : []));
|
|
496
|
+
if (!steps.size) throw new Error(`checkpoint(${JSON.stringify(rule.name)}) is not a checkpoint in this scenario's transcript; pass the same rule object the transcript uses`);
|
|
497
|
+
for (let index = results.length - 1; index >= 0; index--) if (steps.has(results[index].step)) return results[index];
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
397
501
|
//#region ../runner/src/score.ts
|
|
502
|
+
const inUnitInterval = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
503
|
+
/**
|
|
504
|
+
* Criteria must be well formed before they can stand for the score. With
|
|
505
|
+
* declared criteria, each takes its declared weight unless it sets one, a
|
|
506
|
+
* declared one left out is not reached, and an undeclared one is an error,
|
|
507
|
+
* so the checks shown before a run are the checks recorded after it.
|
|
508
|
+
*/
|
|
509
|
+
function checkedCriteria(criteria, declared) {
|
|
510
|
+
if (!Array.isArray(criteria) || !criteria.length && !declared?.length) throw new Error("criteria must be a non-empty array");
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
const results = criteria.map((entry, index) => {
|
|
513
|
+
if (typeof entry?.name !== "string" || !entry.name.trim()) throw new Error(`criteria[${index}] needs a name`);
|
|
514
|
+
const { name } = entry;
|
|
515
|
+
if (seen.has(name)) throw new Error(`Criterion ${name} is listed twice`);
|
|
516
|
+
seen.add(name);
|
|
517
|
+
const declaration = declared?.find((criterion) => criterion.name === name);
|
|
518
|
+
if (declared && !declaration) throw new Error(`Criterion ${name} is not declared; declared: ${declared.map((criterion) => criterion.name).join(", ")}`);
|
|
519
|
+
const weight = entry.weight ?? declaration?.weight;
|
|
520
|
+
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) throw new Error(`Criterion ${name} weight must be a positive number; received ${String(weight)}`);
|
|
521
|
+
const explanation = entry.explanation;
|
|
522
|
+
if (explanation !== void 0 && typeof explanation !== "string") throw new Error(`Criterion ${name} explanation must be a string`);
|
|
523
|
+
const common = {
|
|
524
|
+
name,
|
|
525
|
+
...weight !== void 0 ? { weight } : {},
|
|
526
|
+
...explanation?.trim() ? { explanation: explanation.trim() } : {}
|
|
527
|
+
};
|
|
528
|
+
if ("reached" in entry) {
|
|
529
|
+
if (entry.reached !== false) throw new Error(`Criterion ${name}: reached can only be false; leave it out for a check that ran`);
|
|
530
|
+
return {
|
|
531
|
+
...common,
|
|
532
|
+
reached: false
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const value = entry.value;
|
|
536
|
+
if (!inUnitInterval(value)) throw new Error(`Criterion ${name} value must be a number from 0 to 1; received ${String(value)}`);
|
|
537
|
+
const evidence = entry.evidence;
|
|
538
|
+
return {
|
|
539
|
+
...common,
|
|
540
|
+
value,
|
|
541
|
+
...evidence !== void 0 ? { evidence: toJson(evidence, `criteria.${name}.evidence`) } : {}
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
const missing = (declared ?? []).filter((criterion) => !seen.has(criterion.name)).map((criterion) => ({
|
|
545
|
+
name: criterion.name,
|
|
546
|
+
weight: criterion.weight,
|
|
547
|
+
reached: false
|
|
548
|
+
}));
|
|
549
|
+
return [...results, ...missing];
|
|
550
|
+
}
|
|
398
551
|
/**
|
|
399
552
|
* Shared normalization for inline and final predicate and judge rules.
|
|
400
|
-
*
|
|
553
|
+
* Without a `value`, the weighted mean of `criteria` is the value. `passAt`
|
|
554
|
+
* turns the value into a verdict, and is recorded with it; an explicit
|
|
555
|
+
* `passed` wins.
|
|
401
556
|
*/
|
|
402
|
-
function normalizeScore(value, passAt) {
|
|
557
|
+
function normalizeScore(value, passAt, declared) {
|
|
403
558
|
if (typeof value === "object" && value !== null && "skipped" in value && value.skipped === true) return {
|
|
404
559
|
skipped: true,
|
|
405
560
|
explanation: value.reason
|
|
406
561
|
};
|
|
407
562
|
const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
|
|
408
|
-
if (!normalized ||
|
|
409
|
-
|
|
563
|
+
if (!normalized || typeof normalized !== "object") throw new Error(`A scorer must return a boolean, a number from 0 to 1, or a score object; received ${String(value)}`);
|
|
564
|
+
const criteria = normalized.criteria === void 0 ? void 0 : checkedCriteria(normalized.criteria, declared);
|
|
565
|
+
const score = normalized.value ?? (criteria ? weightedMean(criteria) : void 0);
|
|
566
|
+
if (!inUnitInterval(score)) throw new Error(`Score value must be a finite number from 0 to 1; received ${String(score)}`);
|
|
567
|
+
if (normalized.judge?.usage) {
|
|
410
568
|
const { usage } = normalized.judge;
|
|
411
|
-
if (
|
|
569
|
+
if (Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
|
|
412
570
|
}
|
|
413
|
-
const
|
|
571
|
+
const mark = passAt ?? 1;
|
|
414
572
|
return {
|
|
415
|
-
value:
|
|
416
|
-
passed,
|
|
417
|
-
|
|
418
|
-
...
|
|
419
|
-
...
|
|
573
|
+
value: score,
|
|
574
|
+
passed: normalized.passed ?? score >= mark,
|
|
575
|
+
passAt: mark,
|
|
576
|
+
...normalized.explanation ? { explanation: normalized.explanation } : {},
|
|
577
|
+
...normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
|
|
578
|
+
...normalized.choice ? { choice: normalized.choice } : {},
|
|
579
|
+
...criteria ? { criteria } : {},
|
|
580
|
+
...normalized.judge !== void 0 ? { judge: normalized.judge } : {}
|
|
420
581
|
};
|
|
421
582
|
}
|
|
422
583
|
//#endregion
|
|
@@ -426,8 +587,28 @@ const failure$2 = (name, cause) => new RuleExecutionError({
|
|
|
426
587
|
cause,
|
|
427
588
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
428
589
|
});
|
|
429
|
-
/**
|
|
590
|
+
/** A long tool result or reply is cut to this many characters in a judge's conversation. */
|
|
591
|
+
const MAX_PART = 4e3;
|
|
592
|
+
/**
|
|
593
|
+
* Drives one judge agent session for one assessment: builds the prompt from
|
|
594
|
+
* the rule, sends it, reads the answer in the rule's format, and asks once
|
|
595
|
+
* more if it cannot. Its events stay out of the AUT trajectory.
|
|
596
|
+
*/
|
|
430
597
|
function runJudgeAgent(rule, agent, input, placement) {
|
|
598
|
+
return Effect.tryPromise({
|
|
599
|
+
try: () => judgeOnce(rule, agent, input, placement),
|
|
600
|
+
catch: (cause) => cause instanceof RuleExecutionError ? cause : failure$2(rule.name, cause)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async function judgeOnce(rule, agent, input, placement) {
|
|
604
|
+
const inputs = judgeInputNames(rule);
|
|
605
|
+
const values = await inputValues(rule, input, placement);
|
|
606
|
+
const prompt = judgePrompt({
|
|
607
|
+
rubric: rule.rubric,
|
|
608
|
+
answer: rule.answer,
|
|
609
|
+
inputs,
|
|
610
|
+
values
|
|
611
|
+
});
|
|
431
612
|
const events = [];
|
|
432
613
|
const context = {
|
|
433
614
|
...input.context,
|
|
@@ -435,63 +616,117 @@ function runJudgeAgent(rule, agent, input, placement) {
|
|
|
435
616
|
workspace: input.artifacts.evaluator,
|
|
436
617
|
evaluatorWorkspace: input.artifacts.evaluator
|
|
437
618
|
};
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
619
|
+
const session = await agent.start({
|
|
620
|
+
context,
|
|
621
|
+
attach: input.attach,
|
|
622
|
+
onEvent: (event) => {
|
|
623
|
+
const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
|
|
624
|
+
...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
|
|
625
|
+
source: "aut"
|
|
626
|
+
});
|
|
627
|
+
if (observed.source !== "aut") throw new Error("Invalid judge event source");
|
|
628
|
+
const { source: _, ...validated } = observed;
|
|
629
|
+
events.push(validated);
|
|
630
|
+
}
|
|
444
631
|
});
|
|
445
|
-
let
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
catch
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const result = await session.send(prompt);
|
|
464
|
-
if (result !== void 0) events.push(...turnResultEvents(result, 1, () => /* @__PURE__ */ new Date()));
|
|
465
|
-
},
|
|
466
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
467
|
-
}), (session) => Effect.tryPromise({
|
|
468
|
-
try: async () => session.close?.(),
|
|
469
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
470
|
-
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
471
|
-
closeError = error;
|
|
472
|
-
}))));
|
|
473
|
-
if (closeError) return yield* Effect.fail(closeError);
|
|
474
|
-
const completed = events.slice().reverse().find((event) => event.kind === "completed" && event.output !== void 0);
|
|
475
|
-
const assistant = events.slice().reverse().find((event) => event.kind === "message" && event.role === "assistant");
|
|
476
|
-
const output = completed?.kind === "completed" ? completed.output : assistant?.kind === "message" ? assistant.content : void 0;
|
|
477
|
-
if (output === void 0) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge agent did not emit a verdict")));
|
|
478
|
-
const value = yield* Effect.try({
|
|
479
|
-
try: () => typeof output === "string" ? JSON.parse(output) : output,
|
|
480
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
481
|
-
});
|
|
482
|
-
const encodedEvents = yield* Schema.decodeUnknown(JsonValueSchema)(events).pipe(Effect.mapError((cause) => failure$2(rule.name, cause)));
|
|
483
|
-
if (!Array.isArray(encodedEvents)) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge events must be an array")));
|
|
484
|
-
const usage = events.slice().reverse().find((event) => event.kind === "turn-completed" && event.usage);
|
|
485
|
-
return {
|
|
486
|
-
value,
|
|
487
|
-
judge: {
|
|
488
|
-
...agent.identity ? { agent: agent.identity } : {},
|
|
489
|
-
...usage?.kind === "turn-completed" && usage.usage ? { usage: usage.usage } : {},
|
|
490
|
-
events: encodedEvents
|
|
632
|
+
let reading;
|
|
633
|
+
let reply;
|
|
634
|
+
let attempts = 0;
|
|
635
|
+
try {
|
|
636
|
+
let message = prompt;
|
|
637
|
+
let problem = "";
|
|
638
|
+
while (attempts < 2 && !reading) {
|
|
639
|
+
attempts += 1;
|
|
640
|
+
const from = events.length;
|
|
641
|
+
const result = await session.send(message);
|
|
642
|
+
if (result !== void 0) events.push(...turnResultEvents(result, attempts, () => /* @__PURE__ */ new Date()));
|
|
643
|
+
reply = replyIn(events.slice(from));
|
|
644
|
+
if (reply === void 0) problem = "it did not reply";
|
|
645
|
+
else try {
|
|
646
|
+
reading = readJudgeAnswer(rule.answer, reply);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (!(error instanceof UnreadableAnswer)) throw error;
|
|
649
|
+
problem = error.message;
|
|
491
650
|
}
|
|
492
|
-
|
|
651
|
+
message = `Your answer could not be read: ${problem}. Answer again, only in this format.\n\n${answerInstruction(rule.answer)}`;
|
|
652
|
+
}
|
|
653
|
+
if (!reading) throw new Error(`Judge ${rule.name}'s answer could not be read after ${attempts} tries: ${problem}${reply === void 0 ? "" : `. Its last reply: ${preview(reply)}`}`);
|
|
654
|
+
} finally {
|
|
655
|
+
await session.close?.();
|
|
656
|
+
}
|
|
657
|
+
const recorded = Schema.decodeUnknownSync(JsonValueSchema)(events);
|
|
658
|
+
const model = lastOf(events, (event) => event.kind === "turn-completed" && event.model);
|
|
659
|
+
const usage = totalUsage(events);
|
|
660
|
+
const judge = {
|
|
661
|
+
...agent.identity ? { agent: agent.identity } : {},
|
|
662
|
+
...model?.kind === "turn-completed" && model.model ? { model: model.model } : {},
|
|
663
|
+
...usage ? { usage } : {},
|
|
664
|
+
prompt,
|
|
665
|
+
...reply !== void 0 ? { reply } : {},
|
|
666
|
+
...attempts > 1 ? { attempts } : {},
|
|
667
|
+
events: recorded
|
|
668
|
+
};
|
|
669
|
+
return { value: {
|
|
670
|
+
...reading,
|
|
671
|
+
judge
|
|
672
|
+
} };
|
|
673
|
+
}
|
|
674
|
+
/** Each input's text: what the rule's function returned, or the defaults. */
|
|
675
|
+
async function inputValues(rule, input, placement) {
|
|
676
|
+
if (!rule.inputs) return {
|
|
677
|
+
conversation: conversationText(input.trajectory.events, placement),
|
|
678
|
+
reply: text(input.output)
|
|
679
|
+
};
|
|
680
|
+
const values = {};
|
|
681
|
+
for (const [name, read] of Object.entries(rule.inputs)) values[name] = text(await read(input));
|
|
682
|
+
return values;
|
|
683
|
+
}
|
|
684
|
+
/** The run's conversation as plain lines, for a judge to read: this turn at a checkpoint, else all of it. */
|
|
685
|
+
function conversationText(events, placement) {
|
|
686
|
+
let start = 0;
|
|
687
|
+
if (placement === "transcript") events.forEach((event, index) => {
|
|
688
|
+
if (event.kind === "transcript-step-started" && "message" in event) start = index;
|
|
493
689
|
});
|
|
690
|
+
const lines = [];
|
|
691
|
+
for (const event of events.slice(start)) if (event.kind === "transcript-step-started" && "message" in event) lines.push(`User: ${cut(text(event.message))}`);
|
|
692
|
+
else if (event.source !== "aut") continue;
|
|
693
|
+
else if (event.kind === "message") lines.push(`${capitalize(event.role)}: ${cut(text(event.content))}`);
|
|
694
|
+
else if (event.kind === "tool-call") lines.push(`Tool call ${event.name}: ${cut(text(event.arguments))}`);
|
|
695
|
+
else if (event.kind === "tool-result") lines.push(`Tool result ${event.name ?? event.id}: ${cut(text(event.result))}`);
|
|
696
|
+
return lines.join("\n") || "(nothing happened)";
|
|
697
|
+
}
|
|
698
|
+
/** The judge's answer in one attempt's events: its final output, else its last reply. */
|
|
699
|
+
function replyIn(events) {
|
|
700
|
+
const completed = lastOf(events, (event) => event.kind === "completed" && event.output !== void 0);
|
|
701
|
+
if (completed?.kind === "completed") return completed.output;
|
|
702
|
+
const assistant = lastOf(events, (event) => event.kind === "message" && event.role === "assistant");
|
|
703
|
+
return assistant?.kind === "message" ? assistant.content : void 0;
|
|
704
|
+
}
|
|
705
|
+
function totalUsage(events) {
|
|
706
|
+
let total;
|
|
707
|
+
for (const event of events) {
|
|
708
|
+
if (event.kind !== "turn-completed" || !event.usage) continue;
|
|
709
|
+
total ??= {};
|
|
710
|
+
for (const [key, tokens] of Object.entries(event.usage)) if (tokens !== void 0) total[key] = (total[key] ?? 0) + tokens;
|
|
711
|
+
}
|
|
712
|
+
return total;
|
|
713
|
+
}
|
|
714
|
+
function text(value) {
|
|
715
|
+
if (value === void 0) return "(none)";
|
|
716
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
717
|
+
}
|
|
718
|
+
function cut(value) {
|
|
719
|
+
return value.length > MAX_PART ? `${value.slice(0, MAX_PART)}… (${value.length - MAX_PART} more characters)` : value;
|
|
720
|
+
}
|
|
721
|
+
function preview(value) {
|
|
722
|
+
const shown = text(value).replace(/\s+/g, " ");
|
|
723
|
+
return shown.length > 300 ? `${shown.slice(0, 300)}…` : shown;
|
|
494
724
|
}
|
|
725
|
+
/** The last element matching, scanning from the end (no ES2023 lib here). */
|
|
726
|
+
function lastOf(items, match) {
|
|
727
|
+
for (let index = items.length - 1; index >= 0; index -= 1) if (match(items[index])) return items[index];
|
|
728
|
+
}
|
|
729
|
+
const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
495
730
|
//#endregion
|
|
496
731
|
//#region ../runner/src/evaluate-rule.ts
|
|
497
732
|
const failure$1 = (name, cause) => new RuleExecutionError({
|
|
@@ -500,38 +735,30 @@ const failure$1 = (name, cause) => new RuleExecutionError({
|
|
|
500
735
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
501
736
|
});
|
|
502
737
|
/** Both placements call the same rule executor; only input timing and persistence differ. */
|
|
503
|
-
function evaluateRule(rule, input,
|
|
738
|
+
function evaluateRule(rule, input, defaultJudge, placement) {
|
|
504
739
|
return Effect.gen(function* () {
|
|
505
|
-
|
|
506
|
-
|
|
740
|
+
const judgeAgent = rule.kind === "judge" ? rule.agent ?? defaultJudge : void 0;
|
|
741
|
+
if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("This judge has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts")));
|
|
742
|
+
const evaluated = rule.kind === "predicate" ? yield* Effect.tryPromise({
|
|
507
743
|
try: async () => rule.run(input),
|
|
508
744
|
catch: (cause) => failure$1(rule.name, cause)
|
|
509
|
-
})
|
|
745
|
+
}) : (yield* runJudgeAgent(rule, judgeAgent, input, placement)).value;
|
|
510
746
|
const score = yield* Effect.try({
|
|
511
|
-
try: () => normalizeScore(evaluated.
|
|
747
|
+
try: () => normalizeScore(evaluated, rule.passAt, rule.kind === "predicate" ? rule.criteria : void 0),
|
|
512
748
|
catch: (cause) => failure$1(rule.name, cause)
|
|
513
749
|
});
|
|
514
|
-
const withJudge = rule.kind === "judge" && "judge" in evaluated ? {
|
|
515
|
-
...score,
|
|
516
|
-
judge: evaluated.judge
|
|
517
|
-
} : score;
|
|
518
750
|
const validated = yield* Schema.decodeUnknown(ScoreResultSchema)({
|
|
519
751
|
name: rule.name,
|
|
520
752
|
kind: rule.kind,
|
|
521
753
|
durationMs: 0,
|
|
522
|
-
...
|
|
754
|
+
...score
|
|
523
755
|
}).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
|
|
524
756
|
if (validated.skipped) return {
|
|
525
757
|
skipped: true,
|
|
526
758
|
...validated.explanation === void 0 ? {} : { explanation: validated.explanation }
|
|
527
759
|
};
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
passed: validated.passed,
|
|
531
|
-
...validated.explanation === void 0 ? {} : { explanation: validated.explanation },
|
|
532
|
-
...validated.evidence === void 0 ? {} : { evidence: validated.evidence },
|
|
533
|
-
...validated.judge === void 0 ? {} : { judge: validated.judge }
|
|
534
|
-
};
|
|
760
|
+
const { name: _name, kind: _kind, durationMs: _duration, ...result } = validated;
|
|
761
|
+
return result;
|
|
535
762
|
});
|
|
536
763
|
}
|
|
537
764
|
//#endregion
|
|
@@ -583,7 +810,6 @@ function evaluateCheckpoint(step, index, input, now, judgeAgent) {
|
|
|
583
810
|
const annotations = {
|
|
584
811
|
evalId: input.context.evalId,
|
|
585
812
|
runId: input.context.runId,
|
|
586
|
-
trialId: input.context.trialId,
|
|
587
813
|
step: index,
|
|
588
814
|
checkpoint: step.name
|
|
589
815
|
};
|
|
@@ -682,7 +908,7 @@ function userMessage(step, index, { context, randomSeed }) {
|
|
|
682
908
|
parameters: context.parameters ?? {},
|
|
683
909
|
data: context.data,
|
|
684
910
|
seed: randomSeed,
|
|
685
|
-
|
|
911
|
+
...context.batch ? { batch: context.batch } : {}
|
|
686
912
|
});
|
|
687
913
|
if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
|
|
688
914
|
return message;
|
|
@@ -706,6 +932,7 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
706
932
|
name: step.name,
|
|
707
933
|
timestamp: started.toISOString()
|
|
708
934
|
});
|
|
935
|
+
const tracked = trackAttachments(input.attach);
|
|
709
936
|
return runBoundary(evaluateCheckpoint(step, index, {
|
|
710
937
|
context: input.context,
|
|
711
938
|
artifacts: input.artifacts,
|
|
@@ -713,8 +940,13 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
713
940
|
turn: progress.lastTurn,
|
|
714
941
|
...turnOutput(progress.lastTurn),
|
|
715
942
|
data: input.data,
|
|
716
|
-
checkpoints: [...progress.checkpoints]
|
|
717
|
-
|
|
943
|
+
checkpoints: [...progress.checkpoints],
|
|
944
|
+
checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
|
|
945
|
+
attach: tracked.attach
|
|
946
|
+
}, now, input.defaultJudge)).then((result) => ({
|
|
947
|
+
...result,
|
|
948
|
+
...attachedFields(tracked.attached)
|
|
949
|
+
})).catch(async (error) => {
|
|
718
950
|
const recorded = recordError(error);
|
|
719
951
|
progress.checkpoints.push({
|
|
720
952
|
step: index,
|
|
@@ -722,7 +954,8 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
722
954
|
name: step.name,
|
|
723
955
|
status: "error",
|
|
724
956
|
durationMs: now().getTime() - started.getTime(),
|
|
725
|
-
error: recorded
|
|
957
|
+
error: recorded,
|
|
958
|
+
...attachedFields(tracked.attached)
|
|
726
959
|
});
|
|
727
960
|
try {
|
|
728
961
|
await emitRunner({
|
|
@@ -779,7 +1012,9 @@ function scoringContext(input) {
|
|
|
779
1012
|
...input.lastTurn ? { turn: input.lastTurn } : {},
|
|
780
1013
|
...turnOutput(input.lastTurn),
|
|
781
1014
|
data: input.data,
|
|
782
|
-
checkpoints: input.checkpoints
|
|
1015
|
+
checkpoints: input.checkpoints,
|
|
1016
|
+
checkpoint: checkpointLookup(input.transcript, input.checkpoints),
|
|
1017
|
+
attach: input.attach
|
|
783
1018
|
};
|
|
784
1019
|
}
|
|
785
1020
|
/** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
|
|
@@ -797,12 +1032,15 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
797
1032
|
timestamp: started.toISOString()
|
|
798
1033
|
});
|
|
799
1034
|
let result;
|
|
1035
|
+
const tracked = trackAttachments(input.attach);
|
|
800
1036
|
try {
|
|
801
1037
|
const score = await runBoundary(evaluateRule(scorer, scoringContext({
|
|
802
1038
|
...input,
|
|
1039
|
+
attach: tracked.attach,
|
|
803
1040
|
context,
|
|
804
|
-
workspace
|
|
805
|
-
|
|
1041
|
+
workspace,
|
|
1042
|
+
transcript: definition.transcript
|
|
1043
|
+
}), input.defaultJudge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
806
1044
|
cause,
|
|
807
1045
|
message: cause.message
|
|
808
1046
|
}))));
|
|
@@ -810,7 +1048,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
810
1048
|
name: scorer.name,
|
|
811
1049
|
kind: scorer.kind,
|
|
812
1050
|
durationMs: now().getTime() - started.getTime(),
|
|
813
|
-
...score
|
|
1051
|
+
...score,
|
|
1052
|
+
...attachedFields(tracked.attached)
|
|
814
1053
|
};
|
|
815
1054
|
results.push(result);
|
|
816
1055
|
} catch (error) {
|
|
@@ -820,7 +1059,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
820
1059
|
kind: scorer.kind,
|
|
821
1060
|
durationMs: now().getTime() - started.getTime(),
|
|
822
1061
|
error: recorded,
|
|
823
|
-
passed: false
|
|
1062
|
+
passed: false,
|
|
1063
|
+
...attachedFields(tracked.attached)
|
|
824
1064
|
});
|
|
825
1065
|
await emitRunner({
|
|
826
1066
|
kind: "scorer-failed",
|
|
@@ -841,11 +1081,12 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
841
1081
|
//#endregion
|
|
842
1082
|
//#region ../runner/src/metrics.ts
|
|
843
1083
|
/**
|
|
844
|
-
* What EvalKit measures for every
|
|
1084
|
+
* What EvalKit measures for every run: from its trajectory, the agent's time
|
|
845
1085
|
* (first event to the last one before final scoring), tokens when the agent
|
|
846
|
-
* reported usage, and tool calls
|
|
1086
|
+
* reported usage, and tool calls; from its scores and checkpoints, the
|
|
1087
|
+
* tokens its judges used, kept apart from the agent's.
|
|
847
1088
|
*/
|
|
848
|
-
function builtInMetrics(events) {
|
|
1089
|
+
function builtInMetrics(events, verdicts = []) {
|
|
849
1090
|
const times = events.filter((event) => !event.kind.startsWith("scorer-")).map((event) => Date.parse(event.timestamp)).filter((time) => !Number.isNaN(time));
|
|
850
1091
|
const metrics = [];
|
|
851
1092
|
if (times.length) metrics.push({
|
|
@@ -857,8 +1098,7 @@ function builtInMetrics(events) {
|
|
|
857
1098
|
});
|
|
858
1099
|
let tokens;
|
|
859
1100
|
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));
|
|
1101
|
+
const turn = turnTokens(event.usage);
|
|
862
1102
|
if (turn !== void 0) tokens = (tokens ?? 0) + turn;
|
|
863
1103
|
}
|
|
864
1104
|
if (tokens !== void 0) metrics.push({
|
|
@@ -873,6 +1113,18 @@ function builtInMetrics(events) {
|
|
|
873
1113
|
value: events.filter((event) => event.source === "aut" && event.kind === "tool-call").length,
|
|
874
1114
|
builtIn: true
|
|
875
1115
|
});
|
|
1116
|
+
let judgeTokens;
|
|
1117
|
+
for (const verdict of verdicts) {
|
|
1118
|
+
const used = verdict.judge?.usage && turnTokens(verdict.judge.usage);
|
|
1119
|
+
if (used !== void 0) judgeTokens = (judgeTokens ?? 0) + used;
|
|
1120
|
+
}
|
|
1121
|
+
if (judgeTokens !== void 0) metrics.push({
|
|
1122
|
+
name: "judge tokens",
|
|
1123
|
+
value: judgeTokens,
|
|
1124
|
+
unit: "tokens",
|
|
1125
|
+
better: "lower",
|
|
1126
|
+
builtIn: true
|
|
1127
|
+
});
|
|
876
1128
|
return metrics;
|
|
877
1129
|
}
|
|
878
1130
|
/** Runs the scenario's metrics; a failing metric records its error and the rest still run. */
|
|
@@ -903,7 +1155,7 @@ async function evaluateMetrics(definition, context) {
|
|
|
903
1155
|
//#endregion
|
|
904
1156
|
//#region ../runner/src/local-report-store.ts
|
|
905
1157
|
const MANIFEST_FILE = "manifest.json";
|
|
906
|
-
/** The project's evals and suites when the run started
|
|
1158
|
+
/** The project's evals and suites when the run started. */
|
|
907
1159
|
const CATALOG_FILE = "catalog.json";
|
|
908
1160
|
async function writeJson(filePath, value) {
|
|
909
1161
|
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -922,10 +1174,12 @@ function artifactPath(root, value) {
|
|
|
922
1174
|
if (normalized === ".." || normalized.startsWith(`..${path.sep}`)) throw new Error(`Artifact path escapes its root: ${value}`);
|
|
923
1175
|
return path.join(root, normalized);
|
|
924
1176
|
}
|
|
925
|
-
var
|
|
1177
|
+
var LocalRunWriter = class {
|
|
1178
|
+
location;
|
|
926
1179
|
directory;
|
|
927
1180
|
metadata;
|
|
928
|
-
constructor(directory, metadata) {
|
|
1181
|
+
constructor(location, directory, metadata) {
|
|
1182
|
+
this.location = location;
|
|
929
1183
|
this.directory = directory;
|
|
930
1184
|
this.metadata = metadata;
|
|
931
1185
|
}
|
|
@@ -933,40 +1187,13 @@ var LocalTrialWriter = class {
|
|
|
933
1187
|
return appendFile(path.join(this.directory, "trajectory.jsonl"), `${JSON.stringify(Schema.encodeSync(TrajectoryEventSchema)(event))}\n`);
|
|
934
1188
|
}
|
|
935
1189
|
writeScores(scoring) {
|
|
936
|
-
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(
|
|
1190
|
+
return writeJson(path.join(this.directory, "scoring.json"), Schema.encodeSync(RunScoringSchema)(scoring));
|
|
937
1191
|
}
|
|
938
1192
|
async writeArtifact(relativePath, data) {
|
|
939
1193
|
const target = artifactPath(path.join(this.directory, "artifacts"), relativePath);
|
|
940
1194
|
await mkdir(path.dirname(target), { recursive: true });
|
|
941
1195
|
await writeFile(target, data);
|
|
942
1196
|
}
|
|
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
1197
|
async finalize(summary) {
|
|
971
1198
|
await writeJson(path.join(this.directory, "summary.json"), Schema.encodeSync(RunSummarySchema)(summary));
|
|
972
1199
|
await replaceJson(path.join(this.directory, MANIFEST_FILE), {
|
|
@@ -991,17 +1218,15 @@ function localReportStore(rootDirectory) {
|
|
|
991
1218
|
}
|
|
992
1219
|
//#endregion
|
|
993
1220
|
//#region ../runner/src/local-report-reader.ts
|
|
994
|
-
/**
|
|
1221
|
+
/** The local report boundary for CLI, dashboard, and CI consumers (schema version 6). */
|
|
995
1222
|
async function decode(file, schema) {
|
|
996
1223
|
return Schema.decodeUnknownSync(schema)(JSON.parse(await readFile(file, "utf8")));
|
|
997
1224
|
}
|
|
998
1225
|
const readRunManifest = (root, runId) => decode(join(root, runId, "manifest.json"), RunManifestSchema);
|
|
999
1226
|
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"));
|
|
1227
|
+
const readRunScoring = (root, runId) => decode(join(root, runId, "scoring.json"), RunScoringSchema);
|
|
1228
|
+
async function readRunEvents(root, runId) {
|
|
1229
|
+
return parseTrajectoryJsonl(await readFile(join(root, runId, "trajectory.jsonl"), "utf8"));
|
|
1005
1230
|
}
|
|
1006
1231
|
/**
|
|
1007
1232
|
* True when a run's owning process is known to be gone: it ran on this host
|
|
@@ -1028,6 +1253,7 @@ function runMatrix(matrix, options) {
|
|
|
1028
1253
|
const iterator = matrix.cells(options.selection);
|
|
1029
1254
|
const summary = {
|
|
1030
1255
|
cells: 0,
|
|
1256
|
+
runs: 0,
|
|
1031
1257
|
passed: 0,
|
|
1032
1258
|
failed: 0,
|
|
1033
1259
|
cancelled: 0
|
|
@@ -1043,7 +1269,7 @@ function runMatrix(matrix, options) {
|
|
|
1043
1269
|
"sandbox"
|
|
1044
1270
|
].find((name) => agentRuntimes(cell.eval.agent)[name]);
|
|
1045
1271
|
const suiteId = options.suiteId ?? options.suiteFor?.(cell);
|
|
1046
|
-
const
|
|
1272
|
+
const runs = yield* runEval(cell.eval, {
|
|
1047
1273
|
...options,
|
|
1048
1274
|
...suiteId ? { suiteId } : {},
|
|
1049
1275
|
concurrency: 1,
|
|
@@ -1055,18 +1281,232 @@ function runMatrix(matrix, options) {
|
|
|
1055
1281
|
}
|
|
1056
1282
|
});
|
|
1057
1283
|
summary.cells++;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1284
|
+
for (const run of runs) {
|
|
1285
|
+
summary.runs++;
|
|
1286
|
+
if (run.status === "cancelled") summary.cancelled++;
|
|
1287
|
+
else if (run.status === "passed") summary.passed++;
|
|
1288
|
+
else summary.failed++;
|
|
1289
|
+
}
|
|
1290
|
+
if (options.onResult) yield* Effect.tryPromise(async () => options.onResult(cell, runs));
|
|
1062
1291
|
}
|
|
1063
1292
|
})), { concurrency });
|
|
1064
1293
|
return summary;
|
|
1065
1294
|
});
|
|
1066
1295
|
}
|
|
1067
1296
|
//#endregion
|
|
1297
|
+
//#region ../runner/src/upgrade.ts
|
|
1298
|
+
/** A new run being written. */
|
|
1299
|
+
const TEMPORARY = ".upgrade-";
|
|
1300
|
+
/** An old run set aside while its replacement, with the same ID, moves in. */
|
|
1301
|
+
const RETIRED = ".upgraded-";
|
|
1302
|
+
const PER_RUN_FILES = ["archived.json"];
|
|
1303
|
+
/**
|
|
1304
|
+
* Rewrites results from report schema versions 2 to 5 into version 6, in
|
|
1305
|
+
* place (RFD0008). An old run of one trial (most of them) becomes a run with
|
|
1306
|
+
* the same ID, so links to it keep working. An old run of several trials
|
|
1307
|
+
* becomes runs with the trials' IDs, in a batch with the old run's ID.
|
|
1308
|
+
*
|
|
1309
|
+
* Each new run is written under a temporary name and renamed into place;
|
|
1310
|
+
* artifacts are moved, not copied, as the last step before that rename. An
|
|
1311
|
+
* old run is removed only after its new runs exist, and one replaced under
|
|
1312
|
+
* the same ID is set aside first, so a crash at any point leaves the old run
|
|
1313
|
+
* or complete new ones, and re-running finishes the job. Version 6 runs are
|
|
1314
|
+
* left alone.
|
|
1315
|
+
*/
|
|
1316
|
+
async function upgradeReports(root, options = {}) {
|
|
1317
|
+
const report = {
|
|
1318
|
+
upgraded: 0,
|
|
1319
|
+
runs: 0,
|
|
1320
|
+
batches: 0,
|
|
1321
|
+
running: []
|
|
1322
|
+
};
|
|
1323
|
+
let entries;
|
|
1324
|
+
try {
|
|
1325
|
+
entries = await readdir(root);
|
|
1326
|
+
} catch {
|
|
1327
|
+
return report;
|
|
1328
|
+
}
|
|
1329
|
+
if (!options.dryRun) await finishReplacements(root, entries);
|
|
1330
|
+
for (const entry of entries) {
|
|
1331
|
+
if (entry.startsWith(".")) continue;
|
|
1332
|
+
const directory = join(root, entry);
|
|
1333
|
+
const manifest = await readJson(join(directory, "manifest.json"));
|
|
1334
|
+
if (!manifest || !UPGRADABLE_SCHEMA_VERSIONS.includes(manifest.schemaVersion)) continue;
|
|
1335
|
+
const summary = await readJson(join(directory, "summary.json"));
|
|
1336
|
+
if (!summary && !ownerIsGone(manifest.process)) {
|
|
1337
|
+
report.running.push(entry);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const trials = await trialIds(directory);
|
|
1341
|
+
report.upgraded += 1;
|
|
1342
|
+
report.runs += Math.max(trials.length, 1);
|
|
1343
|
+
if (trials.length > 1) report.batches += 1;
|
|
1344
|
+
if (options.dryRun) continue;
|
|
1345
|
+
await upgradeRun(root, entry, manifest, summary, trials);
|
|
1346
|
+
}
|
|
1347
|
+
if (!options.dryRun) await removeLeftovers(root);
|
|
1348
|
+
return report;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Finishes same-ID replacements a crash interrupted: the old run was set
|
|
1352
|
+
* aside, and its replacement may or may not have moved in.
|
|
1353
|
+
*/
|
|
1354
|
+
async function finishReplacements(root, entries) {
|
|
1355
|
+
for (const entry of entries) {
|
|
1356
|
+
if (!entry.startsWith(RETIRED)) continue;
|
|
1357
|
+
const id = entry.slice(10);
|
|
1358
|
+
const retired = join(root, entry);
|
|
1359
|
+
const directory = join(root, id);
|
|
1360
|
+
const replacement = join(root, `${TEMPORARY}${id}`);
|
|
1361
|
+
if (!await exists(directory)) await rename(await exists(replacement) ? replacement : retired, directory);
|
|
1362
|
+
await rm(retired, {
|
|
1363
|
+
recursive: true,
|
|
1364
|
+
force: true
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Temporary folders nothing will finish. One holding artifacts is kept: it
|
|
1370
|
+
* could be the only copy, and an old run would have moved them back.
|
|
1371
|
+
*/
|
|
1372
|
+
async function removeLeftovers(root) {
|
|
1373
|
+
for (const entry of await readdir(root)) if (entry.startsWith(TEMPORARY) && !await exists(join(root, entry, "artifacts"))) await rm(join(root, entry), {
|
|
1374
|
+
recursive: true,
|
|
1375
|
+
force: true
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
/** Swaps a fully written replacement in for the old run of the same ID. */
|
|
1379
|
+
async function replaceInPlace(root, id, replacement) {
|
|
1380
|
+
const retired = join(root, `${RETIRED}${id}`);
|
|
1381
|
+
await rename(join(root, id), retired);
|
|
1382
|
+
await rename(replacement, join(root, id));
|
|
1383
|
+
await rm(retired, {
|
|
1384
|
+
recursive: true,
|
|
1385
|
+
force: true
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async function upgradeRun(root, oldRunId, runManifest, runSummary, trials) {
|
|
1389
|
+
const directory = join(root, oldRunId);
|
|
1390
|
+
const catalog = await readJson(join(directory, "catalog.json"));
|
|
1391
|
+
const extras = await presentFiles(directory, PER_RUN_FILES);
|
|
1392
|
+
if (!trials.length) {
|
|
1393
|
+
const target = join(root, `${TEMPORARY}${oldRunId}`);
|
|
1394
|
+
await mkdir(target, { recursive: true });
|
|
1395
|
+
const status = runSummary ? "cancelled" : "running";
|
|
1396
|
+
await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
|
|
1397
|
+
run: runManifest,
|
|
1398
|
+
runId: oldRunId,
|
|
1399
|
+
status
|
|
1400
|
+
}));
|
|
1401
|
+
if (runSummary) await writeJsonFile(join(target, "summary.json"), {
|
|
1402
|
+
status,
|
|
1403
|
+
endedAt: runSummary.endedAt,
|
|
1404
|
+
...runSummary.durationMs === void 0 ? {} : { durationMs: runSummary.durationMs },
|
|
1405
|
+
...runSummary.error ? { error: runSummary.error } : {}
|
|
1406
|
+
});
|
|
1407
|
+
await copyShared(directory, target, catalog, extras);
|
|
1408
|
+
await replaceInPlace(root, oldRunId, target);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
const inPlace = trials.length === 1;
|
|
1412
|
+
for (const trialId of trials) {
|
|
1413
|
+
const runId = inPlace ? oldRunId : trialId;
|
|
1414
|
+
const destination = join(root, runId);
|
|
1415
|
+
if (!inPlace && await exists(join(destination, "manifest.json"))) continue;
|
|
1416
|
+
const source = join(directory, "trials", trialId);
|
|
1417
|
+
const trialManifest = await readJson(join(source, "manifest.json")) ?? {};
|
|
1418
|
+
const trialSummary = await readJson(join(source, "summary.json"));
|
|
1419
|
+
const passed = (trialSummary?.scoring)?.passed;
|
|
1420
|
+
const status = upgradeStatus(trialSummary?.status ?? trialManifest.status, passed);
|
|
1421
|
+
const index = typeof trialManifest.trialIndex === "number" ? trialManifest.trialIndex : trials.indexOf(trialId);
|
|
1422
|
+
const target = join(root, `${TEMPORARY}${runId}`);
|
|
1423
|
+
if (!await exists(join(source, "artifacts")) && await exists(join(target, "artifacts"))) await rename(join(target, "artifacts"), join(source, "artifacts"));
|
|
1424
|
+
await rm(target, {
|
|
1425
|
+
recursive: true,
|
|
1426
|
+
force: true
|
|
1427
|
+
});
|
|
1428
|
+
await mkdir(target, { recursive: true });
|
|
1429
|
+
await writeJsonFile(join(target, "manifest.json"), upgradeManifest({
|
|
1430
|
+
run: runManifest,
|
|
1431
|
+
trial: trialManifest,
|
|
1432
|
+
runId,
|
|
1433
|
+
...!inPlace ? { batch: {
|
|
1434
|
+
id: oldRunId,
|
|
1435
|
+
index,
|
|
1436
|
+
size: trials.length
|
|
1437
|
+
} } : {},
|
|
1438
|
+
status
|
|
1439
|
+
}));
|
|
1440
|
+
if (trialSummary) await writeJsonFile(join(target, "summary.json"), {
|
|
1441
|
+
...trialSummary,
|
|
1442
|
+
status
|
|
1443
|
+
});
|
|
1444
|
+
const scoring = await readText(join(source, "scoring.json"));
|
|
1445
|
+
if (scoring !== void 0) await writeFile(join(target, "scoring.json"), scoring);
|
|
1446
|
+
const trajectory = await readText(join(source, "trajectory.jsonl"));
|
|
1447
|
+
if (trajectory !== void 0) await writeFile(join(target, "trajectory.jsonl"), upgradeTrajectory(trajectory));
|
|
1448
|
+
await copyShared(directory, target, catalog, extras);
|
|
1449
|
+
if (await exists(join(source, "artifacts"))) await rename(join(source, "artifacts"), join(target, "artifacts"));
|
|
1450
|
+
if (inPlace) await replaceInPlace(root, oldRunId, target);
|
|
1451
|
+
else await rename(target, destination);
|
|
1452
|
+
}
|
|
1453
|
+
if (!inPlace) await rm(directory, {
|
|
1454
|
+
recursive: true,
|
|
1455
|
+
force: true
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
async function copyShared(from, to, catalog, extras) {
|
|
1459
|
+
if (catalog) await writeJsonFile(join(to, "catalog.json"), upgradeCatalog(catalog));
|
|
1460
|
+
for (const file of extras) await cp(join(from, file), join(to, file));
|
|
1461
|
+
}
|
|
1462
|
+
async function trialIds(directory) {
|
|
1463
|
+
try {
|
|
1464
|
+
return (await readdir(join(directory, "trials"), { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1465
|
+
} catch {
|
|
1466
|
+
return [];
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function presentFiles(directory, names) {
|
|
1470
|
+
const present = [];
|
|
1471
|
+
for (const name of names) if (await exists(join(directory, name))) present.push(name);
|
|
1472
|
+
return present;
|
|
1473
|
+
}
|
|
1474
|
+
async function exists(path) {
|
|
1475
|
+
try {
|
|
1476
|
+
await stat(path);
|
|
1477
|
+
return true;
|
|
1478
|
+
} catch {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
async function readText(path) {
|
|
1483
|
+
try {
|
|
1484
|
+
return await readFile(path, "utf8");
|
|
1485
|
+
} catch {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function readJson(path) {
|
|
1490
|
+
const text = await readText(path);
|
|
1491
|
+
if (text === void 0) return void 0;
|
|
1492
|
+
let value;
|
|
1493
|
+
try {
|
|
1494
|
+
value = JSON.parse(text);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1497
|
+
}
|
|
1498
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1499
|
+
}
|
|
1500
|
+
async function writeJsonFile(path, value) {
|
|
1501
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
1502
|
+
}
|
|
1503
|
+
//#endregion
|
|
1068
1504
|
//#region ../runner/src/index.ts
|
|
1069
|
-
/**
|
|
1505
|
+
/**
|
|
1506
|
+
* Runs a scenario: once, or `repeat` times as one batch. Resolves to one
|
|
1507
|
+
* result per run started; runs never started because of cancellation are
|
|
1508
|
+
* left out.
|
|
1509
|
+
*/
|
|
1070
1510
|
function runEval(definition, options) {
|
|
1071
1511
|
return Effect.gen(function* () {
|
|
1072
1512
|
yield* Effect.logInfo("eval run started").pipe(Effect.annotateLogs({
|
|
@@ -1074,19 +1514,19 @@ function runEval(definition, options) {
|
|
|
1074
1514
|
...options.runId ? { runId: options.runId } : {},
|
|
1075
1515
|
...options.suiteId ? { suiteId: options.suiteId } : {}
|
|
1076
1516
|
}));
|
|
1077
|
-
const
|
|
1078
|
-
try: () =>
|
|
1517
|
+
const runs = yield* Effect.tryPromise({
|
|
1518
|
+
try: () => executeBatch(definition, options),
|
|
1079
1519
|
catch: (error) => error
|
|
1080
1520
|
});
|
|
1081
1521
|
yield* Effect.logInfo("eval run finished").pipe(Effect.annotateLogs({
|
|
1082
1522
|
evalId: authoringId(definition),
|
|
1083
|
-
|
|
1084
|
-
|
|
1523
|
+
runs: runs.length,
|
|
1524
|
+
passed: runs.filter((run) => run.status === "passed").length
|
|
1085
1525
|
}));
|
|
1086
|
-
return
|
|
1526
|
+
return runs;
|
|
1087
1527
|
});
|
|
1088
1528
|
}
|
|
1089
|
-
function createId(
|
|
1529
|
+
function createId() {
|
|
1090
1530
|
return crypto.randomUUID();
|
|
1091
1531
|
}
|
|
1092
1532
|
function canonicalId(kind, id) {
|
|
@@ -1095,9 +1535,9 @@ function canonicalId(kind, id) {
|
|
|
1095
1535
|
function assertUuid(value, label) {
|
|
1096
1536
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) throw new Error(`${label} must be a UUID`);
|
|
1097
1537
|
}
|
|
1098
|
-
function runMetadata(definition, options, runId, startedAt) {
|
|
1538
|
+
function runMetadata(definition, options, runId, startedAt, extra) {
|
|
1099
1539
|
return {
|
|
1100
|
-
schemaVersion:
|
|
1540
|
+
schemaVersion: 6,
|
|
1101
1541
|
runId,
|
|
1102
1542
|
runUri: canonicalId("run", runId),
|
|
1103
1543
|
evalId: definition.id,
|
|
@@ -1110,7 +1550,9 @@ function runMetadata(definition, options, runId, startedAt) {
|
|
|
1110
1550
|
...definition.agent.identity ? { aut: definition.agent.identity } : {},
|
|
1111
1551
|
parameters: options.parameters,
|
|
1112
1552
|
matrix: options.matrix,
|
|
1113
|
-
startedAt
|
|
1553
|
+
startedAt,
|
|
1554
|
+
...options.batch ? { batch: options.batch } : {},
|
|
1555
|
+
...extra
|
|
1114
1556
|
};
|
|
1115
1557
|
}
|
|
1116
1558
|
/** Recording a secondary error must never mask the original failure or block cleanup. */
|
|
@@ -1123,104 +1565,53 @@ async function tryEmitError(error, emitRunner, now) {
|
|
|
1123
1565
|
});
|
|
1124
1566
|
} catch {}
|
|
1125
1567
|
}
|
|
1126
|
-
/**
|
|
1127
|
-
async function
|
|
1568
|
+
/** Starts `repeat` runs of a scenario, as one batch when there are several. */
|
|
1569
|
+
async function executeBatch(definition, options) {
|
|
1128
1570
|
let hasUser = false;
|
|
1129
1571
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
1130
1572
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
1131
|
-
|
|
1573
|
+
assertNoRemovedOptions(definition);
|
|
1574
|
+
assertJudgesIndependent(definition, options.judge);
|
|
1575
|
+
for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && !rule.agent && !options.judge) throw new Error(`Judge ${rule.name} in ${definition.id} has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts`);
|
|
1132
1576
|
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 });
|
|
1577
|
+
const repeat = options.repeat ?? definition.repeat ?? 1;
|
|
1578
|
+
if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
|
|
1579
|
+
if (repeat === 1) return [await runOnce(definition, options)];
|
|
1580
|
+
if (options.runId) throw new Error("runId names one run; it cannot be used with repeat");
|
|
1581
|
+
const batchId = createId();
|
|
1141
1582
|
const signal = options.signal;
|
|
1142
|
-
const
|
|
1143
|
-
const
|
|
1144
|
-
try: async () => signal?.aborted ? void 0 :
|
|
1583
|
+
const runs = Array.from({ length: repeat }, (_, index) => {
|
|
1584
|
+
const run = Effect.tryPromise({
|
|
1585
|
+
try: async () => signal?.aborted ? void 0 : runOnce(definition, {
|
|
1145
1586
|
...options,
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1587
|
+
batch: {
|
|
1588
|
+
id: batchId,
|
|
1589
|
+
index,
|
|
1590
|
+
size: repeat
|
|
1591
|
+
}
|
|
1151
1592
|
}),
|
|
1152
1593
|
catch: (error) => error
|
|
1153
1594
|
});
|
|
1154
|
-
return options.semaphore ? options.semaphore.withPermits(1)(
|
|
1155
|
-
});
|
|
1156
|
-
const trials = (await Effect.runPromise(Effect.all(trialEffects, { concurrency: options.concurrency ?? 32 }))).filter((trial) => trial !== void 0);
|
|
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 } : {}
|
|
1595
|
+
return options.semaphore ? options.semaphore.withPermits(1)(run) : run;
|
|
1168
1596
|
});
|
|
1169
|
-
|
|
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
|
-
};
|
|
1597
|
+
return (await Effect.runPromise(Effect.all(runs, { concurrency: options.concurrency ?? 32 }))).filter((run) => run !== void 0);
|
|
1206
1598
|
}
|
|
1207
|
-
|
|
1599
|
+
/** One execution: a session in its own sandbox, scored and reported. */
|
|
1600
|
+
async function runOnce(definition, options) {
|
|
1208
1601
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1209
|
-
const runId = options.runId ?? createId(
|
|
1210
|
-
const trialId = options.trialId ?? createId("trial");
|
|
1602
|
+
const runId = options.runId ?? createId();
|
|
1211
1603
|
const startedAt = now().toISOString();
|
|
1212
1604
|
const selectedRuntime = options.runtime;
|
|
1213
1605
|
const runtime = selectedRuntime ? agentRuntimes(definition.agent)[selectedRuntime] : void 0;
|
|
1214
1606
|
if (selectedRuntime && !runtime) throw new Error(`AUT does not declare the requested ${selectedRuntime} runtime`);
|
|
1215
1607
|
const randomSeed = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(1))[0];
|
|
1216
|
-
const
|
|
1608
|
+
const batch = options.batch;
|
|
1217
1609
|
const signal = options.signal ?? new AbortController().signal;
|
|
1218
1610
|
const root = options.root ?? process.cwd();
|
|
1219
1611
|
const baseContext = {
|
|
1220
1612
|
runId,
|
|
1221
1613
|
evalId: authoringId(definition),
|
|
1222
|
-
|
|
1223
|
-
trialIndex,
|
|
1614
|
+
...batch ? { batch } : {},
|
|
1224
1615
|
metadata: {
|
|
1225
1616
|
...toJsonObject(definition.metadata ?? {}, `${definition.id}.metadata`),
|
|
1226
1617
|
randomSeed
|
|
@@ -1234,8 +1625,7 @@ async function runTrial(definition, options) {
|
|
|
1234
1625
|
prepared = await prepareDatasets(definition.datasets ?? [], root, {
|
|
1235
1626
|
evalId: baseContext.evalId,
|
|
1236
1627
|
runId,
|
|
1237
|
-
|
|
1238
|
-
trialIndex,
|
|
1628
|
+
...batch ? { batch } : {},
|
|
1239
1629
|
seed: randomSeed,
|
|
1240
1630
|
parameters: options.parameters ?? {},
|
|
1241
1631
|
signal
|
|
@@ -1247,40 +1637,28 @@ async function runTrial(definition, options) {
|
|
|
1247
1637
|
const scoringData = datasetValues(prepared, "all");
|
|
1248
1638
|
const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
|
|
1249
1639
|
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,
|
|
1640
|
+
const runWriter = await options.report.startRun(runMetadata(definition, options, runId, startedAt, {
|
|
1263
1641
|
workspaceIgnore,
|
|
1264
|
-
...prepared.length ? { datasets:
|
|
1265
|
-
});
|
|
1642
|
+
...prepared.length ? { datasets: runDatasets(prepared) } : {}
|
|
1643
|
+
}), { catalog: options.catalog });
|
|
1644
|
+
const attachments = attachmentStore(runWriter, options.attachmentLimits);
|
|
1266
1645
|
let writeChain = Promise.resolve();
|
|
1267
1646
|
let reportingFailed = false;
|
|
1268
|
-
const
|
|
1647
|
+
const runRef = {
|
|
1269
1648
|
runId,
|
|
1270
|
-
|
|
1271
|
-
trialIndex,
|
|
1649
|
+
...batch ? { batch } : {},
|
|
1272
1650
|
evalId: definition.id,
|
|
1273
1651
|
...options.suiteId ? { suiteId: options.suiteId } : {},
|
|
1274
1652
|
...options.parameters ? { parameters: options.parameters } : {}
|
|
1275
1653
|
};
|
|
1276
1654
|
const observe = (event) => {
|
|
1277
1655
|
try {
|
|
1278
|
-
options.onEvent?.(
|
|
1656
|
+
options.onEvent?.(runRef, event);
|
|
1279
1657
|
} catch {}
|
|
1280
1658
|
};
|
|
1281
1659
|
const append = async (event) => {
|
|
1282
1660
|
events.push(event);
|
|
1283
|
-
writeChain = writeChain.then(() =>
|
|
1661
|
+
writeChain = writeChain.then(() => runWriter.appendEvent(event));
|
|
1284
1662
|
try {
|
|
1285
1663
|
await writeChain;
|
|
1286
1664
|
observe(event);
|
|
@@ -1316,7 +1694,7 @@ async function runTrial(definition, options) {
|
|
|
1316
1694
|
const workspaceScope = await Effect.runPromise(Scope.make());
|
|
1317
1695
|
try {
|
|
1318
1696
|
await emitRunner({
|
|
1319
|
-
kind: "
|
|
1697
|
+
kind: "run-started",
|
|
1320
1698
|
timestamp: now().toISOString()
|
|
1321
1699
|
});
|
|
1322
1700
|
if (signal.aborted) throw cancelled();
|
|
@@ -1325,7 +1703,7 @@ async function runTrial(definition, options) {
|
|
|
1325
1703
|
message: datasetsError instanceof Error ? datasetsError.message : String(datasetsError)
|
|
1326
1704
|
});
|
|
1327
1705
|
workspace = await runBoundary(Effect.acquireRelease(Effect.tryPromise({
|
|
1328
|
-
try: () =>
|
|
1706
|
+
try: () => createRunWorkspace(runId, prepared, options.workspaceRoot),
|
|
1329
1707
|
catch: (cause) => new DatasetError({
|
|
1330
1708
|
cause,
|
|
1331
1709
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -1338,20 +1716,21 @@ async function runTrial(definition, options) {
|
|
|
1338
1716
|
})
|
|
1339
1717
|
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
1340
1718
|
cleanupError = error;
|
|
1341
|
-
}).pipe(Effect.tap(() => Effect.logError("
|
|
1342
|
-
const
|
|
1343
|
-
const
|
|
1719
|
+
}).pipe(Effect.tap(() => Effect.logError("run workspace cleanup failed")))))).pipe(Effect.provideService(Scope.Scope, workspaceScope)));
|
|
1720
|
+
const runWorkspace = workspace;
|
|
1721
|
+
const runContext = {
|
|
1344
1722
|
...baseContext,
|
|
1345
1723
|
data: candidateData,
|
|
1346
|
-
workspace:
|
|
1347
|
-
evaluatorWorkspace:
|
|
1724
|
+
workspace: runWorkspace.artifacts.candidate,
|
|
1725
|
+
evaluatorWorkspace: runWorkspace.artifacts.evaluator
|
|
1348
1726
|
};
|
|
1349
|
-
context =
|
|
1727
|
+
context = runContext;
|
|
1350
1728
|
await runBoundary(Effect.acquireUseRelease(Effect.tryPromise({
|
|
1351
1729
|
try: () => definition.agent.start({
|
|
1352
|
-
context:
|
|
1730
|
+
context: runContext,
|
|
1353
1731
|
...runtime ? { runtime } : {},
|
|
1354
|
-
onEvent: onAutEvent
|
|
1732
|
+
onEvent: onAutEvent,
|
|
1733
|
+
attach: attachments.attach
|
|
1355
1734
|
}),
|
|
1356
1735
|
catch: (cause) => new AutExecutionError({
|
|
1357
1736
|
cause,
|
|
@@ -1359,8 +1738,8 @@ async function runTrial(definition, options) {
|
|
|
1359
1738
|
})
|
|
1360
1739
|
}), (session) => Effect.tryPromise({
|
|
1361
1740
|
try: () => executeTranscript(definition, session, {
|
|
1362
|
-
context:
|
|
1363
|
-
artifacts:
|
|
1741
|
+
context: runContext,
|
|
1742
|
+
artifacts: runWorkspace.artifacts,
|
|
1364
1743
|
events,
|
|
1365
1744
|
now,
|
|
1366
1745
|
randomSeed,
|
|
@@ -1368,7 +1747,9 @@ async function runTrial(definition, options) {
|
|
|
1368
1747
|
emitAut,
|
|
1369
1748
|
data: scoringData,
|
|
1370
1749
|
signal,
|
|
1371
|
-
progress
|
|
1750
|
+
progress,
|
|
1751
|
+
attach: attachments.attach,
|
|
1752
|
+
defaultJudge: options.judge
|
|
1372
1753
|
}),
|
|
1373
1754
|
catch: (cause) => cause
|
|
1374
1755
|
}), (opened) => Effect.tryPromise({
|
|
@@ -1381,7 +1762,6 @@ async function runTrial(definition, options) {
|
|
|
1381
1762
|
closeError = error;
|
|
1382
1763
|
}).pipe(Effect.tap(() => Effect.logError("AUT session close failed")))))).pipe(Effect.annotateLogs({
|
|
1383
1764
|
runId,
|
|
1384
|
-
trialId,
|
|
1385
1765
|
evalId: definition.id
|
|
1386
1766
|
})));
|
|
1387
1767
|
} catch (error) {
|
|
@@ -1411,22 +1791,26 @@ async function runTrial(definition, options) {
|
|
|
1411
1791
|
skippedScorers,
|
|
1412
1792
|
data: scoringData,
|
|
1413
1793
|
checkpoints,
|
|
1794
|
+
attach: attachments.attach,
|
|
1795
|
+
defaultJudge: options.judge,
|
|
1414
1796
|
now,
|
|
1415
1797
|
emitRunner
|
|
1416
1798
|
});
|
|
1417
|
-
metrics.push(...builtInMetrics(events));
|
|
1799
|
+
metrics.push(...builtInMetrics(events, [...scoreResults, ...checkpoints]));
|
|
1418
1800
|
if (context && workspace && !wasCancelled) metrics.push(...await evaluateMetrics(definition, scoringContext({
|
|
1419
1801
|
context,
|
|
1420
1802
|
workspace,
|
|
1421
1803
|
events,
|
|
1422
1804
|
lastTurn,
|
|
1423
1805
|
data: scoringData,
|
|
1424
|
-
checkpoints
|
|
1806
|
+
checkpoints,
|
|
1807
|
+
transcript: definition.transcript,
|
|
1808
|
+
attach: attachments.attach
|
|
1425
1809
|
})));
|
|
1426
1810
|
scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1427
|
-
await
|
|
1811
|
+
await runWriter.writeScores(scoring);
|
|
1428
1812
|
if (workspace) try {
|
|
1429
|
-
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root,
|
|
1813
|
+
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, runWriter, workspaceIgnore);
|
|
1430
1814
|
} catch (captureError) {
|
|
1431
1815
|
if (!primaryError) primaryError = captureError;
|
|
1432
1816
|
await tryEmitError(captureError, emitRunner, now);
|
|
@@ -1444,49 +1828,34 @@ async function runTrial(definition, options) {
|
|
|
1444
1828
|
scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1445
1829
|
if (!primaryError) try {
|
|
1446
1830
|
await emitRunner({
|
|
1447
|
-
kind: "
|
|
1831
|
+
kind: "run-completed",
|
|
1448
1832
|
timestamp: now().toISOString()
|
|
1449
1833
|
});
|
|
1450
1834
|
} catch (error) {
|
|
1451
1835
|
primaryError = error;
|
|
1452
1836
|
}
|
|
1453
|
-
status = wasCancelled ? "cancelled" : primaryError ? "
|
|
1837
|
+
status = wasCancelled ? "cancelled" : primaryError ? "errored" : scoring.passed ? "passed" : "failed";
|
|
1454
1838
|
const error = primaryError ? recordError(primaryError) : void 0;
|
|
1839
|
+
const attached = await attachments.close();
|
|
1455
1840
|
const endedAt = now().toISOString();
|
|
1456
|
-
|
|
1841
|
+
const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
1842
|
+
await runWriter.finalize({
|
|
1457
1843
|
status,
|
|
1458
1844
|
endedAt,
|
|
1459
|
-
durationMs
|
|
1845
|
+
durationMs,
|
|
1460
1846
|
scoring,
|
|
1461
1847
|
...artifacts ? { artifacts } : {},
|
|
1848
|
+
...attached.length ? { attachments: attached } : {},
|
|
1462
1849
|
...error ? { error } : {}
|
|
1463
1850
|
});
|
|
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
1851
|
return {
|
|
1481
1852
|
runId,
|
|
1482
1853
|
runUri: canonicalId("run", runId),
|
|
1483
|
-
trialId,
|
|
1484
|
-
trialUri: canonicalId("trial", trialId),
|
|
1485
|
-
trialIndex: options.trialIndex ?? 0,
|
|
1486
1854
|
evalId: definition.id,
|
|
1487
1855
|
status,
|
|
1856
|
+
...batch ? { batch } : {},
|
|
1488
1857
|
reportLocation: runWriter.location,
|
|
1489
|
-
durationMs
|
|
1858
|
+
durationMs,
|
|
1490
1859
|
scoring,
|
|
1491
1860
|
...error ? { error } : {}
|
|
1492
1861
|
};
|
|
@@ -1500,23 +1869,9 @@ function datasetValues(prepared, audience) {
|
|
|
1500
1869
|
}
|
|
1501
1870
|
return values;
|
|
1502
1871
|
}
|
|
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. */
|
|
1872
|
+
/** Persistent workspaces of runs that did not pass are kept for debugging by default. */
|
|
1518
1873
|
function keepWorkspace(policy = "failed", passed) {
|
|
1519
1874
|
return policy === "all" || policy === "failed" && !passed;
|
|
1520
1875
|
}
|
|
1521
1876
|
//#endregion
|
|
1522
|
-
export {
|
|
1877
|
+
export { DatasetError as _, readRunEvents as a, readRunSummary as c, staticDatasetFile as d, staticDatasetFiles as f, CheckpointExecutionError as g, CancelledError as h, ownerIsGone as i, localReportStore as l, AutExecutionError as m, upgradeReports as n, readRunManifest as o, globMatcher as p, runMatrix as r, readRunScoring as s, runEval as t, runWorkspacePath as u, ReportError as v, ScoringError as y };
|