@leostera/evalkit 0.6.1 → 0.8.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 +48 -1
- package/dist/cli.mjs +5186 -10301
- package/dist/dashboard/assets/index-C12IeG2y.css +2 -0
- package/dist/dashboard/assets/{index-CTJpd8q8.js → index-xlVsW6ZR.js} +14 -11
- package/dist/dashboard/index.html +2 -2
- package/dist/index.d.mts +130 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -8
- package/dist/runner.d.mts +104 -3
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +3 -2
- package/dist/skills/evalkit-results/SKILL.md +1 -1
- package/dist/{src-CZPufWN_.mjs → src-LeGSyDD2.mjs} +56 -3
- package/dist/{src-CKqOzeBi.mjs → src-mm8L_IxM.mjs} +227 -36
- package/package.json +1 -1
- package/dist/dashboard/assets/index-YAabTN3X.css +0 -2
|
@@ -283,10 +283,10 @@ function catalogScorer(rule, at) {
|
|
|
283
283
|
}
|
|
284
284
|
function catalogStep(step, at) {
|
|
285
285
|
switch (step.kind) {
|
|
286
|
-
case "user": return {
|
|
286
|
+
case "user": return typeof step.message === "string" ? {
|
|
287
287
|
kind: "user",
|
|
288
288
|
message: step.message
|
|
289
|
-
};
|
|
289
|
+
} : { kind: "user" };
|
|
290
290
|
case "agent": return {
|
|
291
291
|
kind: "agent",
|
|
292
292
|
expectation: step.expectation
|
|
@@ -316,6 +316,22 @@ function shapeOf(value) {
|
|
|
316
316
|
return { type: typeof json };
|
|
317
317
|
}
|
|
318
318
|
//#endregion
|
|
319
|
+
//#region ../core/src/config.ts
|
|
320
|
+
/** Left out of candidate snapshots unless `artifacts.exclude` says otherwise. */
|
|
321
|
+
const DEFAULT_ARTIFACT_EXCLUDE = [
|
|
322
|
+
".git",
|
|
323
|
+
"node_modules",
|
|
324
|
+
".venv",
|
|
325
|
+
"__pycache__",
|
|
326
|
+
"_build",
|
|
327
|
+
"_opam",
|
|
328
|
+
"target"
|
|
329
|
+
];
|
|
330
|
+
/** Pure, typed project configuration. Loading/discovery belongs to the CLI. */
|
|
331
|
+
function defineConfig(configuration) {
|
|
332
|
+
return configuration;
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
319
335
|
//#region ../core/src/canonical-json.ts
|
|
320
336
|
/** Canonical JSON: keys sorted recursively, array order preserved. Rejects non-JSON inputs. */
|
|
321
337
|
function canonicalParameters(value) {
|
|
@@ -603,6 +619,8 @@ const TrialMetadataSchema = Schema.Struct({
|
|
|
603
619
|
matrix: Schema.optional(MatrixCellSchema),
|
|
604
620
|
aut: Schema.optional(AutIdentitySchema),
|
|
605
621
|
startedAt: Schema.String,
|
|
622
|
+
/** The effective snapshot ignore patterns: the project's, then the scenario's. */
|
|
623
|
+
workspaceIgnore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
|
606
624
|
datasets: Schema.optional(Schema.mutable(Schema.Array(Schema.Struct({
|
|
607
625
|
id: Schema.String,
|
|
608
626
|
visibility: Schema.Literal("candidate", "evaluator"),
|
|
@@ -663,8 +681,17 @@ const CheckpointResultSchema = Schema.Struct({
|
|
|
663
681
|
id: Schema.String
|
|
664
682
|
}))
|
|
665
683
|
});
|
|
684
|
+
const MetricResultSchema = Schema.Struct({
|
|
685
|
+
name: Schema.String,
|
|
686
|
+
value: Schema.optional(Schema.Number),
|
|
687
|
+
unit: Schema.optional(Schema.String),
|
|
688
|
+
better: Schema.optional(Schema.Literal("lower", "higher")),
|
|
689
|
+
builtIn: Schema.optional(Schema.Literal(true)),
|
|
690
|
+
error: Schema.optional(RecordedErrorSchema)
|
|
691
|
+
});
|
|
666
692
|
const TrialScoringSchema = Schema.Struct({
|
|
667
693
|
results: Schema.mutable(Schema.Array(ScoreResultSchema)),
|
|
694
|
+
metrics: Schema.optional(Schema.mutable(Schema.Array(MetricResultSchema))),
|
|
668
695
|
checkpoints: Schema.optional(Schema.mutable(Schema.Array(CheckpointResultSchema))),
|
|
669
696
|
skippedScorers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
|
670
697
|
overall: Schema.optional(Schema.Number),
|
|
@@ -874,6 +901,10 @@ function expectToolCall(expected) {
|
|
|
874
901
|
expected
|
|
875
902
|
};
|
|
876
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* A user message. Pass a function to compute it for each trial, e.g. from a
|
|
906
|
+
* matrix axis: `user(({ parameters }) => `Set up OCaml with ${parameters.toolchain}.`)`.
|
|
907
|
+
*/
|
|
877
908
|
function user(message) {
|
|
878
909
|
return {
|
|
879
910
|
kind: "user",
|
|
@@ -907,6 +938,15 @@ function predicate(name, run, options = {}) {
|
|
|
907
938
|
...options
|
|
908
939
|
});
|
|
909
940
|
}
|
|
941
|
+
function metric(name, run, options = {}) {
|
|
942
|
+
if (!name.trim()) throw new Error("Metric name must not be empty");
|
|
943
|
+
return markDefinitionSite({
|
|
944
|
+
kind: "metric",
|
|
945
|
+
name,
|
|
946
|
+
run,
|
|
947
|
+
...options
|
|
948
|
+
});
|
|
949
|
+
}
|
|
910
950
|
function judge(name, options) {
|
|
911
951
|
if (!name.trim() || !options.rubric.trim()) throw new Error("Judge name and rubric must not be empty");
|
|
912
952
|
return markDefinitionSite({
|
|
@@ -919,12 +959,19 @@ function defineEval(definition) {
|
|
|
919
959
|
authoringId(definition);
|
|
920
960
|
if ("fixtures" in definition) throw new Error(`Eval ${definition.id} uses fixtures, which were replaced by datasets: [defineDataset(...)]`);
|
|
921
961
|
if (definition.metadata !== void 0) toJsonObject(definition.metadata, `${definition.id}.metadata`);
|
|
962
|
+
for (const [index, pattern] of (definition.workspace?.ignore ?? []).entries()) if (typeof pattern !== "string" || pattern.replace(/^!/, "").trim() === "") throw new Error(`Eval ${definition.id} workspace.ignore[${index}] must be a non-empty glob, got ${JSON.stringify(pattern)}`);
|
|
922
963
|
const datasetIds = /* @__PURE__ */ new Set();
|
|
923
964
|
for (const [index, dataset] of (definition.datasets ?? []).entries()) {
|
|
924
965
|
if (!isDataset(dataset)) throw new Error(`Eval ${definition.id} datasets[${index}] is not a dataset; create it with defineDataset(...)`);
|
|
925
966
|
if (datasetIds.has(dataset.id)) throw new Error(`Eval ${definition.id} uses dataset ${dataset.id} more than once`);
|
|
926
967
|
datasetIds.add(dataset.id);
|
|
927
968
|
}
|
|
969
|
+
const metricNames = new Set(BUILT_IN_METRICS);
|
|
970
|
+
for (const [index, entry] of (definition.metrics ?? []).entries()) {
|
|
971
|
+
if (entry?.kind !== "metric") throw new Error(`Eval ${definition.id} metrics[${index}] is not a metric; create it with metric(...)`);
|
|
972
|
+
if (metricNames.has(entry.name)) throw new Error(`Eval ${definition.id} has more than one metric named ${entry.name} (built-in: ${BUILT_IN_METRICS.join(", ")})`);
|
|
973
|
+
metricNames.add(entry.name);
|
|
974
|
+
}
|
|
928
975
|
let hasUser = false;
|
|
929
976
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
930
977
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
@@ -1001,6 +1048,12 @@ function registerEvals(registrations) {
|
|
|
1001
1048
|
catalog: (options) => evals.map((evaluation) => catalogEntry(evaluation, suiteIdByEvalId.get(authoringId(evaluation)), options))
|
|
1002
1049
|
};
|
|
1003
1050
|
}
|
|
1051
|
+
/** Metrics EvalKit records for every trial; scenario metrics may not reuse these names. */
|
|
1052
|
+
const BUILT_IN_METRICS = [
|
|
1053
|
+
"agent time",
|
|
1054
|
+
"tokens",
|
|
1055
|
+
"tool calls"
|
|
1056
|
+
];
|
|
1004
1057
|
function recordError(error) {
|
|
1005
1058
|
if (error instanceof Error) return {
|
|
1006
1059
|
name: error.name,
|
|
@@ -1013,4 +1066,4 @@ function recordError(error) {
|
|
|
1013
1066
|
};
|
|
1014
1067
|
}
|
|
1015
1068
|
//#endregion
|
|
1016
|
-
export {
|
|
1069
|
+
export { ResourceUriSchema as $, RunSummarySchema as A, defineEvalMatrix as B, JudgeRunInfoSchema as C, RunMetadataSchema as D, RunManifestSchema as E, TrialScoringSchema as F, NotJsonError as G, DEFAULT_ARTIFACT_EXCLUDE as H, TrialSummarySchema as I, defineDataset as J, toJson as K, UsageSchema as L, TrajectoryEventSchema as M, TrialManifestSchema as N, RunSourceSchema as O, TrialMetadataSchema as P, ResourceKindSchema as Q, normalizeAutEvent as R, JsonValueSchema as S, RecordedErrorSchema as T, defineConfig as U, canonicalParameters as V, catalogEntry as W, relativeDatasetPath as X, isDataset as Y, definitionSite as Z, parseTrajectoryJsonl as _, authoringId as a, resourceUuid as at, AutIdentitySchema as b, defineSuite as c, metric as d, UuidSchema as et, predicate as f, user as g, skip as h, agentRuntimes as i, resourceUriSchema as it, ScoreResultSchema as j, RunStatusSchema as k, expectToolCall as l, registerEvals as m, DEFAULT_AGENT_RUNTIMES as n, parseResourceUri as nt, defineAgent as o, recordError as p, toJsonObject as q, agent as r, resourceUri as rt, defineEval as s, BUILT_IN_METRICS as t, authorId as tt, judge as u, ApiErrorSchema as v, MetricResultSchema as w, CheckpointResultSchema as x, ArtifactEntrySchema as y, turnResultEvents as z };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as RunSummarySchema, D as RunMetadataSchema, E as RunManifestSchema, F as TrialScoringSchema, H as DEFAULT_ARTIFACT_EXCLUDE, I as TrialSummarySchema, K as toJson, M as TrajectoryEventSchema, N as TrialManifestSchema, P as TrialMetadataSchema, R as normalizeAutEvent, S as JsonValueSchema, V as canonicalParameters, X as relativeDatasetPath, _ as parseTrajectoryJsonl, a as authoringId, i as agentRuntimes, j as ScoreResultSchema, p as recordError, q as toJsonObject, rt as resourceUri, x as CheckpointResultSchema, z as turnResultEvents } from "./src-LeGSyDD2.mjs";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import { Data, Effect, Either, Exit, Scope } from "effect";
|
|
4
4
|
import os from "node:os";
|
|
@@ -32,30 +32,113 @@ async function unlessAborted(work, signal) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region ../core/src/glob.ts
|
|
36
|
+
/**
|
|
37
|
+
* A glob matcher for discovery: `*` and `?` stay within a path segment, `**`
|
|
38
|
+
* spans segments, and `{a,b}` is an alternation.
|
|
39
|
+
*/
|
|
40
|
+
function globMatcher(pattern) {
|
|
41
|
+
let source = "";
|
|
42
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
43
|
+
const char = pattern[index];
|
|
44
|
+
if (char === "*" && pattern[index + 1] === "*") {
|
|
45
|
+
index++;
|
|
46
|
+
if (pattern[index + 1] === "/") {
|
|
47
|
+
index++;
|
|
48
|
+
source += "(?:.*/)?";
|
|
49
|
+
} else source += ".*";
|
|
50
|
+
} else if (char === "*") source += "[^/]*";
|
|
51
|
+
else if (char === "?") source += "[^/]";
|
|
52
|
+
else if (char === "{") {
|
|
53
|
+
const end = pattern.indexOf("}", index);
|
|
54
|
+
if (end === -1) source += "\\{";
|
|
55
|
+
else {
|
|
56
|
+
source += `(?:${pattern.slice(index + 1, end).split(",").map((part) => part.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join("|")})`;
|
|
57
|
+
index = end;
|
|
58
|
+
}
|
|
59
|
+
} else source += char.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
60
|
+
}
|
|
61
|
+
const expression = new RegExp(`^${source}$`);
|
|
62
|
+
return (path) => expression.test(path);
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
35
65
|
//#region ../runner/src/snapshot.ts
|
|
66
|
+
const GLOB_CHARS = /[*?{]/;
|
|
67
|
+
function ignoreRule(pattern) {
|
|
68
|
+
const negated = pattern.startsWith("!");
|
|
69
|
+
const trimmed = (negated ? pattern.slice(1) : pattern).replace(/^\.\//, "").replace(/\/$/, "");
|
|
70
|
+
const match = globMatcher(trimmed);
|
|
71
|
+
if (!trimmed.includes("/")) return {
|
|
72
|
+
negated,
|
|
73
|
+
match: (relativePath) => match(path.posix.basename(relativePath)),
|
|
74
|
+
mayMatchBelow: () => true
|
|
75
|
+
};
|
|
76
|
+
const segments = trimmed.split("/");
|
|
77
|
+
const firstGlob = segments.findIndex((segment) => GLOB_CHARS.test(segment));
|
|
78
|
+
const prefix = segments.slice(0, firstGlob === -1 ? segments.length : firstGlob).join("/");
|
|
79
|
+
return {
|
|
80
|
+
negated,
|
|
81
|
+
match,
|
|
82
|
+
mayMatchBelow: (directory) => prefix === "" || prefix.startsWith(`${directory}/`) || prefix === directory || directory.startsWith(`${prefix}/`)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
36
85
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
86
|
+
* Ignore rules for candidate snapshots, read like `.gitignore`: globs relative
|
|
87
|
+
* to the workspace root, where a pattern without `/` matches that name at any
|
|
88
|
+
* depth, and later patterns win. `!pattern` re-includes a path; unlike Git, it
|
|
89
|
+
* also works below an ignored directory (`_build` then `!_build/doc/**`).
|
|
39
90
|
*/
|
|
40
|
-
|
|
91
|
+
function ignoreRules(patterns) {
|
|
92
|
+
const rules = patterns.map(ignoreRule);
|
|
93
|
+
return {
|
|
94
|
+
/** Whether a path is ignored, given whether its directory was. */
|
|
95
|
+
ignored(relativePath, parentIgnored = false) {
|
|
96
|
+
let ignored = parentIgnored;
|
|
97
|
+
for (const rule of rules) if (rule.match(relativePath)) ignored = !rule.negated;
|
|
98
|
+
return ignored;
|
|
99
|
+
},
|
|
100
|
+
/** Whether an ignored directory must still be walked for re-included paths. */
|
|
101
|
+
mayReinclude(directory) {
|
|
102
|
+
return rules.some((rule) => rule.negated && rule.mayMatchBelow(directory));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Copies the candidate workspace's regular files into the trial report,
|
|
108
|
+
* leaving out ignored paths. Symbolic links and special files are skipped,
|
|
109
|
+
* never followed, so a snapshot cannot read outside the trial. An ignored
|
|
110
|
+
* directory is walked only when a `!` pattern could re-include something in
|
|
111
|
+
* it, and is listed only if it ends up containing a snapshotted file.
|
|
112
|
+
*/
|
|
113
|
+
async function snapshotCandidateWorkspace(candidateRoot, writer, ignore = DEFAULT_ARTIFACT_EXCLUDE) {
|
|
41
114
|
const artifacts = [];
|
|
42
|
-
|
|
115
|
+
const rules = ignoreRules(ignore);
|
|
116
|
+
/** Returns whether anything below `directory` was snapshotted. */
|
|
117
|
+
async function visit(directory, relativeDirectory, directoryIgnored) {
|
|
43
118
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
44
119
|
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
120
|
+
let kept = false;
|
|
45
121
|
for (const entry of entries) {
|
|
46
122
|
const absolutePath = path.join(directory, entry.name);
|
|
47
123
|
const relativePath = path.join(relativeDirectory, entry.name);
|
|
124
|
+
const posixPath = relativePath.split(path.sep).join("/");
|
|
125
|
+
const ignored = rules.ignored(posixPath, directoryIgnored);
|
|
48
126
|
const stat = await lstat(absolutePath);
|
|
49
|
-
if (stat.isSymbolicLink())
|
|
127
|
+
if (stat.isSymbolicLink()) continue;
|
|
50
128
|
if (stat.isDirectory()) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
if (ignored && !rules.mayReinclude(posixPath)) continue;
|
|
130
|
+
const entryIndex = artifacts.length;
|
|
131
|
+
const keptBelow = await visit(absolutePath, relativePath, ignored);
|
|
132
|
+
if (!ignored || keptBelow) {
|
|
133
|
+
artifacts.splice(entryIndex, 0, {
|
|
134
|
+
path: path.join("candidate", relativePath),
|
|
135
|
+
kind: "directory"
|
|
136
|
+
});
|
|
137
|
+
kept = true;
|
|
138
|
+
}
|
|
56
139
|
continue;
|
|
57
140
|
}
|
|
58
|
-
if (!stat.isFile())
|
|
141
|
+
if (ignored || !stat.isFile()) continue;
|
|
59
142
|
const data = await readFile(absolutePath);
|
|
60
143
|
const artifactPath = path.join("candidate", relativePath);
|
|
61
144
|
await writer.writeArtifact(artifactPath, data);
|
|
@@ -64,9 +147,11 @@ async function snapshotCandidateWorkspace(candidateRoot, writer) {
|
|
|
64
147
|
kind: "file",
|
|
65
148
|
size: data.byteLength
|
|
66
149
|
});
|
|
150
|
+
kept = true;
|
|
67
151
|
}
|
|
152
|
+
return kept;
|
|
68
153
|
}
|
|
69
|
-
await visit(candidateRoot, "");
|
|
154
|
+
await visit(candidateRoot, "", false);
|
|
70
155
|
return artifacts;
|
|
71
156
|
}
|
|
72
157
|
//#endregion
|
|
@@ -247,8 +332,9 @@ async function staticDatasetFile(dataset, root, file) {
|
|
|
247
332
|
}
|
|
248
333
|
//#endregion
|
|
249
334
|
//#region ../runner/src/workspace.ts
|
|
250
|
-
|
|
251
|
-
|
|
335
|
+
/** Where a trial's persistent workspace lives below `root`. */
|
|
336
|
+
function trialWorkspacePath(root, trialId) {
|
|
337
|
+
return path.join(path.resolve(root), trialId.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
252
338
|
}
|
|
253
339
|
/**
|
|
254
340
|
* Creates isolated candidate and evaluator directories and writes each
|
|
@@ -256,7 +342,7 @@ function trialDirectoryName(trialId) {
|
|
|
256
342
|
* Two datasets writing the same file is an error, never an overwrite.
|
|
257
343
|
*/
|
|
258
344
|
async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
|
|
259
|
-
const root = persistentRoot ?
|
|
345
|
+
const root = persistentRoot ? trialWorkspacePath(persistentRoot, trialId) : await mkdtemp(path.join(os.tmpdir(), "evalkit-trial-"));
|
|
260
346
|
const persistent = persistentRoot !== void 0;
|
|
261
347
|
const artifacts = {
|
|
262
348
|
candidate: { root: path.join(root, "candidate") },
|
|
@@ -270,10 +356,13 @@ async function createTrialWorkspace(trialId, datasets = [], persistentRoot) {
|
|
|
270
356
|
}
|
|
271
357
|
return {
|
|
272
358
|
artifacts,
|
|
273
|
-
cleanup:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
359
|
+
cleanup: async (keep = false) => {
|
|
360
|
+
if (persistent && keep) return;
|
|
361
|
+
await rm(root, {
|
|
362
|
+
recursive: true,
|
|
363
|
+
force: true
|
|
364
|
+
});
|
|
365
|
+
}
|
|
277
366
|
};
|
|
278
367
|
} catch (error) {
|
|
279
368
|
await rm(root, {
|
|
@@ -545,7 +634,7 @@ async function executeTranscript(definition, session, input) {
|
|
|
545
634
|
let turn = 0;
|
|
546
635
|
for (const [index, step] of definition.transcript.entries()) {
|
|
547
636
|
if (input.signal.aborted) throw cancelled();
|
|
548
|
-
const message = step.kind === "user" ? step
|
|
637
|
+
const message = step.kind === "user" ? userMessage(step, index, input) : void 0;
|
|
549
638
|
await emitRunner({
|
|
550
639
|
kind: "transcript-step-started",
|
|
551
640
|
step: index,
|
|
@@ -586,6 +675,18 @@ async function executeTranscript(definition, session, input) {
|
|
|
586
675
|
});
|
|
587
676
|
}
|
|
588
677
|
}
|
|
678
|
+
/** A computed message sees the parameters and candidate data the agent also sees. */
|
|
679
|
+
function userMessage(step, index, { context, randomSeed }) {
|
|
680
|
+
if (typeof step.message === "string") return step.message;
|
|
681
|
+
const message = step.message({
|
|
682
|
+
parameters: context.parameters ?? {},
|
|
683
|
+
data: context.data,
|
|
684
|
+
seed: randomSeed,
|
|
685
|
+
trialIndex: context.trialIndex
|
|
686
|
+
});
|
|
687
|
+
if (typeof message !== "string" || !message.trim()) throw new Error(`The user message at step ${index + 1} must compute a non-empty string`);
|
|
688
|
+
return message;
|
|
689
|
+
}
|
|
589
690
|
async function sendUserTurn(session, message, signal) {
|
|
590
691
|
return runBoundary(Effect.tryPromise({
|
|
591
692
|
try: () => unlessAborted(() => session.send(message), signal),
|
|
@@ -611,7 +712,8 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
611
712
|
trajectory: { events: input.events },
|
|
612
713
|
turn: progress.lastTurn,
|
|
613
714
|
...turnOutput(progress.lastTurn),
|
|
614
|
-
data: input.data
|
|
715
|
+
data: input.data,
|
|
716
|
+
checkpoints: [...progress.checkpoints]
|
|
615
717
|
}, now, definition.judge)).catch(async (error) => {
|
|
616
718
|
const recorded = recordError(error);
|
|
617
719
|
progress.checkpoints.push({
|
|
@@ -656,20 +758,33 @@ function turnOutput(turn) {
|
|
|
656
758
|
}
|
|
657
759
|
//#endregion
|
|
658
760
|
//#region ../runner/src/final-scoring.ts
|
|
659
|
-
function scoreSummary(results, checkpoints, skippedScorers) {
|
|
761
|
+
function scoreSummary(results, checkpoints, skippedScorers, metrics = []) {
|
|
660
762
|
const valid = results.filter((result) => result.value !== void 0 && !result.skipped);
|
|
661
763
|
const hasError = results.some((result) => result.error !== void 0);
|
|
662
764
|
return {
|
|
663
765
|
results,
|
|
766
|
+
...metrics.length ? { metrics } : {},
|
|
664
767
|
...checkpoints.length ? { checkpoints } : {},
|
|
665
768
|
...skippedScorers.length ? { skippedScorers } : {},
|
|
666
769
|
...valid.length === 0 ? {} : { overall: valid.reduce((sum, result) => sum + (result.value ?? 0), 0) / valid.length },
|
|
667
770
|
passed: !hasError && results.every((result) => result.skipped || result.passed === true) && checkpoints.every((result) => result.status === "passed" || result.status === "skipped") && !skippedScorers.length
|
|
668
771
|
};
|
|
669
772
|
}
|
|
773
|
+
/** What final scorers and metrics receive. */
|
|
774
|
+
function scoringContext(input) {
|
|
775
|
+
return {
|
|
776
|
+
context: input.context,
|
|
777
|
+
artifacts: input.workspace.artifacts,
|
|
778
|
+
trajectory: { events: input.events },
|
|
779
|
+
...input.lastTurn ? { turn: input.lastTurn } : {},
|
|
780
|
+
...turnOutput(input.lastTurn),
|
|
781
|
+
data: input.data,
|
|
782
|
+
checkpoints: input.checkpoints
|
|
783
|
+
};
|
|
784
|
+
}
|
|
670
785
|
/** Append completed scores to the supplied arrays, even when a later scorer or report write fails. */
|
|
671
786
|
async function evaluateFinalScorers(definition, input) {
|
|
672
|
-
const { context, workspace,
|
|
787
|
+
const { context, workspace, results, skippedScorers, now, emitRunner } = input;
|
|
673
788
|
for (const scorer of definition.scoring) {
|
|
674
789
|
if (input.cancelled || input.reportingFailed || !context || !workspace || (input.primaryError || input.stoppedEarly) && (!("supportsPartial" in scorer) || !scorer.supportsPartial)) {
|
|
675
790
|
skippedScorers.push(scorer.name);
|
|
@@ -683,14 +798,11 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
683
798
|
});
|
|
684
799
|
let result;
|
|
685
800
|
try {
|
|
686
|
-
const score = await runBoundary(evaluateRule(scorer, {
|
|
801
|
+
const score = await runBoundary(evaluateRule(scorer, scoringContext({
|
|
802
|
+
...input,
|
|
687
803
|
context,
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
...lastTurn ? { turn: lastTurn } : {},
|
|
691
|
-
...turnOutput(lastTurn),
|
|
692
|
-
data: input.data
|
|
693
|
-
}, definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
804
|
+
workspace
|
|
805
|
+
}), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
694
806
|
cause,
|
|
695
807
|
message: cause.message
|
|
696
808
|
}))));
|
|
@@ -727,6 +839,68 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
727
839
|
}
|
|
728
840
|
}
|
|
729
841
|
//#endregion
|
|
842
|
+
//#region ../runner/src/metrics.ts
|
|
843
|
+
/**
|
|
844
|
+
* What EvalKit measures for every trial from its trajectory: the agent's time
|
|
845
|
+
* (first event to the last one before final scoring), tokens when the agent
|
|
846
|
+
* reported usage, and tool calls.
|
|
847
|
+
*/
|
|
848
|
+
function builtInMetrics(events) {
|
|
849
|
+
const times = events.filter((event) => !event.kind.startsWith("scorer-")).map((event) => Date.parse(event.timestamp)).filter((time) => !Number.isNaN(time));
|
|
850
|
+
const metrics = [];
|
|
851
|
+
if (times.length) metrics.push({
|
|
852
|
+
name: "agent time",
|
|
853
|
+
value: Math.max(...times) - Math.min(...times),
|
|
854
|
+
unit: "ms",
|
|
855
|
+
better: "lower",
|
|
856
|
+
builtIn: true
|
|
857
|
+
});
|
|
858
|
+
let tokens;
|
|
859
|
+
for (const event of events) if (event.source === "aut" && event.kind === "turn-completed" && event.usage) {
|
|
860
|
+
const { totalTokens, inputTokens, outputTokens } = event.usage;
|
|
861
|
+
const turn = totalTokens ?? (inputTokens === void 0 && outputTokens === void 0 ? void 0 : (inputTokens ?? 0) + (outputTokens ?? 0));
|
|
862
|
+
if (turn !== void 0) tokens = (tokens ?? 0) + turn;
|
|
863
|
+
}
|
|
864
|
+
if (tokens !== void 0) metrics.push({
|
|
865
|
+
name: "tokens",
|
|
866
|
+
value: tokens,
|
|
867
|
+
unit: "tokens",
|
|
868
|
+
better: "lower",
|
|
869
|
+
builtIn: true
|
|
870
|
+
});
|
|
871
|
+
metrics.push({
|
|
872
|
+
name: "tool calls",
|
|
873
|
+
value: events.filter((event) => event.source === "aut" && event.kind === "tool-call").length,
|
|
874
|
+
builtIn: true
|
|
875
|
+
});
|
|
876
|
+
return metrics;
|
|
877
|
+
}
|
|
878
|
+
/** Runs the scenario's metrics; a failing metric records its error and the rest still run. */
|
|
879
|
+
async function evaluateMetrics(definition, context) {
|
|
880
|
+
const results = [];
|
|
881
|
+
for (const entry of definition.metrics ?? []) {
|
|
882
|
+
const shape = {
|
|
883
|
+
name: entry.name,
|
|
884
|
+
...entry.unit ? { unit: entry.unit } : {},
|
|
885
|
+
...entry.better ? { better: entry.better } : {}
|
|
886
|
+
};
|
|
887
|
+
try {
|
|
888
|
+
const value = await entry.run(context);
|
|
889
|
+
if (value !== void 0 && !Number.isFinite(value)) throw new Error(`Metric ${entry.name} must return a finite number or undefined; got ${String(value)}`);
|
|
890
|
+
results.push({
|
|
891
|
+
...shape,
|
|
892
|
+
...value === void 0 ? {} : { value }
|
|
893
|
+
});
|
|
894
|
+
} catch (error) {
|
|
895
|
+
results.push({
|
|
896
|
+
...shape,
|
|
897
|
+
error: recordError(error)
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return results;
|
|
902
|
+
}
|
|
903
|
+
//#endregion
|
|
730
904
|
//#region ../runner/src/local-report-store.ts
|
|
731
905
|
const MANIFEST_FILE = "manifest.json";
|
|
732
906
|
/** The project's evals and suites when the run started (schema version 4+). */
|
|
@@ -1071,6 +1245,7 @@ async function runTrial(definition, options) {
|
|
|
1071
1245
|
}
|
|
1072
1246
|
const candidateData = datasetValues(prepared, "candidate");
|
|
1073
1247
|
const scoringData = datasetValues(prepared, "all");
|
|
1248
|
+
const workspaceIgnore = [...options.artifactExclude ?? DEFAULT_ARTIFACT_EXCLUDE, ...definition.workspace?.ignore ?? []];
|
|
1074
1249
|
const events = [];
|
|
1075
1250
|
const runWriter = options.runWriter ?? await options.report.startRun(runMetadata(definition, options, runId, startedAt), { catalog: options.catalog });
|
|
1076
1251
|
const trialWriter = await runWriter.startTrial({
|
|
@@ -1085,6 +1260,7 @@ async function runTrial(definition, options) {
|
|
|
1085
1260
|
parameters: options.parameters,
|
|
1086
1261
|
matrix: options.matrix,
|
|
1087
1262
|
startedAt,
|
|
1263
|
+
workspaceIgnore,
|
|
1088
1264
|
...prepared.length ? { datasets: trialDatasets(prepared) } : {}
|
|
1089
1265
|
});
|
|
1090
1266
|
let writeChain = Promise.resolve();
|
|
@@ -1155,7 +1331,7 @@ async function runTrial(definition, options) {
|
|
|
1155
1331
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
1156
1332
|
})
|
|
1157
1333
|
}), (created) => Effect.tryPromise({
|
|
1158
|
-
try: () => created.cleanup(),
|
|
1334
|
+
try: () => created.cleanup(keepWorkspace(options.keepWorkspaces, scoring?.passed === true)),
|
|
1159
1335
|
catch: (cause) => new DatasetError({
|
|
1160
1336
|
cause,
|
|
1161
1337
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
@@ -1220,6 +1396,7 @@ async function runTrial(definition, options) {
|
|
|
1220
1396
|
const { checkpoints, stoppedEarly, lastTurn } = progress;
|
|
1221
1397
|
const scoreResults = [];
|
|
1222
1398
|
const skippedScorers = [];
|
|
1399
|
+
const metrics = [];
|
|
1223
1400
|
try {
|
|
1224
1401
|
await evaluateFinalScorers(definition, {
|
|
1225
1402
|
context,
|
|
@@ -1233,13 +1410,23 @@ async function runTrial(definition, options) {
|
|
|
1233
1410
|
results: scoreResults,
|
|
1234
1411
|
skippedScorers,
|
|
1235
1412
|
data: scoringData,
|
|
1413
|
+
checkpoints,
|
|
1236
1414
|
now,
|
|
1237
1415
|
emitRunner
|
|
1238
1416
|
});
|
|
1239
|
-
|
|
1417
|
+
metrics.push(...builtInMetrics(events));
|
|
1418
|
+
if (context && workspace && !wasCancelled) metrics.push(...await evaluateMetrics(definition, scoringContext({
|
|
1419
|
+
context,
|
|
1420
|
+
workspace,
|
|
1421
|
+
events,
|
|
1422
|
+
lastTurn,
|
|
1423
|
+
data: scoringData,
|
|
1424
|
+
checkpoints
|
|
1425
|
+
})));
|
|
1426
|
+
scoring = scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1240
1427
|
await trialWriter.writeScores(scoring);
|
|
1241
1428
|
if (workspace) try {
|
|
1242
|
-
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter);
|
|
1429
|
+
artifacts = await snapshotCandidateWorkspace(workspace.artifacts.candidate.root, trialWriter, workspaceIgnore);
|
|
1243
1430
|
} catch (captureError) {
|
|
1244
1431
|
if (!primaryError) primaryError = captureError;
|
|
1245
1432
|
await tryEmitError(captureError, emitRunner, now);
|
|
@@ -1254,7 +1441,7 @@ async function runTrial(definition, options) {
|
|
|
1254
1441
|
await tryEmitError(cleanupError, emitRunner, now);
|
|
1255
1442
|
}
|
|
1256
1443
|
}
|
|
1257
|
-
scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers);
|
|
1444
|
+
scoring ??= scoreSummary(scoreResults, checkpoints, skippedScorers, metrics);
|
|
1258
1445
|
if (!primaryError) try {
|
|
1259
1446
|
await emitRunner({
|
|
1260
1447
|
kind: "trial-completed",
|
|
@@ -1327,5 +1514,9 @@ function trialCounts(trials) {
|
|
|
1327
1514
|
cancelled: cancelledTrials
|
|
1328
1515
|
};
|
|
1329
1516
|
}
|
|
1517
|
+
/** Persistent workspaces of trials that did not pass are kept for debugging by default. */
|
|
1518
|
+
function keepWorkspace(policy = "failed", passed) {
|
|
1519
|
+
return policy === "all" || policy === "failed" && !passed;
|
|
1520
|
+
}
|
|
1330
1521
|
//#endregion
|
|
1331
|
-
export {
|
|
1522
|
+
export { CheckpointExecutionError as _, readRunSummary as a, ScoringError as b, readTrialScoring as c, trialWorkspacePath as d, staticDatasetFile as f, CancelledError as g, AutExecutionError as h, readRunManifest as i, readTrialSummary as l, globMatcher as m, runMatrix as n, readTrialEvents as o, staticDatasetFiles as p, ownerIsGone as r, readTrialManifest as s, runEval as t, localReportStore as u, DatasetError as v, ReportError as y };
|