@leostera/evalkit 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -1
- package/README.md +3 -2
- package/dist/cli.mjs +311 -92
- package/dist/dashboard/assets/index-CFJzcw3o.css +2 -0
- package/dist/dashboard/assets/{index-By1SJ1on.js → index-DgoX2x_M.js} +34 -31
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-BbVblaHz.mjs → index-db-DI7xKMSX.mjs} +237 -47
- package/dist/index.d.mts +1141 -220
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +241 -52
- package/dist/runner.d.mts +782 -225
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +2 -2
- package/dist/share-worker.d.mts +1 -1
- package/dist/share-worker.d.mts.map +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.d.mts +9 -6
- package/dist/share.d.mts.map +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +3 -3
- package/dist/skills/evalkit-agents/SKILL.md +3 -3
- package/dist/skills/evalkit-datasets/SKILL.md +1 -1
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +4 -4
- package/dist/skills/evalkit-results/SKILL.md +2 -2
- package/dist/{src-Bn6fnZnF.mjs → src-CXzbHAKN.mjs} +1 -1
- package/dist/{src-BrfIXfO2.mjs → src-D6Ax2t9b.mjs} +636 -220
- package/dist/src-Dc3GXacB.mjs +3637 -0
- package/dist/upgrade-rMsGzUEJ.mjs +230 -0
- package/dist/upgrade-transcript-C8DUykLd.mjs +402 -0
- package/package.json +2 -1
- package/dist/dashboard/assets/index-geXMXCx9.css +0 -2
- package/dist/src-CcdLZqFk.mjs +0 -1714
- package/dist/upgrade-60SIRius.mjs +0 -110
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
//#region ../core/src/usage.ts
|
|
2
|
+
const BUCKETS = [
|
|
3
|
+
"input",
|
|
4
|
+
"output",
|
|
5
|
+
"cacheRead",
|
|
6
|
+
"cacheWrite",
|
|
7
|
+
"reasoning",
|
|
8
|
+
"total"
|
|
9
|
+
];
|
|
10
|
+
/** ATXF usage from what an adapter reported; undefined when it reported nothing. */
|
|
11
|
+
function normalizeUsage(input) {
|
|
12
|
+
if (!input) return void 0;
|
|
13
|
+
for (const key of BUCKETS) {
|
|
14
|
+
const value = input[key];
|
|
15
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new Error(`Usage ${key} must be a non-negative number; received ${String(value)}`);
|
|
16
|
+
}
|
|
17
|
+
if (BUCKETS.every((key) => input[key] === void 0)) return void 0;
|
|
18
|
+
return {
|
|
19
|
+
input: input.input ?? 0,
|
|
20
|
+
output: input.output ?? 0,
|
|
21
|
+
...input.cacheRead === void 0 ? {} : { cacheRead: input.cacheRead },
|
|
22
|
+
...input.cacheWrite === void 0 ? {} : { cacheWrite: input.cacheWrite },
|
|
23
|
+
...input.reasoning === void 0 ? {} : { reasoning: input.reasoning },
|
|
24
|
+
...input.total === void 0 ? {} : { total: input.total }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Tokens the model had to process: uncached input, cache writes and output.
|
|
29
|
+
* Cache reads are left out; see `cachedTokens`. Usage that has only a total
|
|
30
|
+
* (from a provider, or a run saved before 0.11) counts its total less any
|
|
31
|
+
* cache reads.
|
|
32
|
+
*/
|
|
33
|
+
function uncachedTokens(usage) {
|
|
34
|
+
const counted = usage.input + (usage.cacheWrite ?? 0) + usage.output;
|
|
35
|
+
if (counted === 0 && usage.total !== void 0) return Math.max(0, usage.total - (usage.cacheRead ?? 0));
|
|
36
|
+
return counted;
|
|
37
|
+
}
|
|
38
|
+
/** Bucket by bucket; a bucket absent from both stays absent. */
|
|
39
|
+
function addUsage(a, b) {
|
|
40
|
+
if (!a) return b;
|
|
41
|
+
if (!b) return a;
|
|
42
|
+
const sum = {
|
|
43
|
+
input: a.input + b.input,
|
|
44
|
+
output: a.output + b.output
|
|
45
|
+
};
|
|
46
|
+
for (const key of [
|
|
47
|
+
"cacheRead",
|
|
48
|
+
"cacheWrite",
|
|
49
|
+
"reasoning",
|
|
50
|
+
"total"
|
|
51
|
+
]) if (a[key] !== void 0 || b[key] !== void 0) sum[key] = (a[key] ?? 0) + (b[key] ?? 0);
|
|
52
|
+
return sum;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A transcript's usage as ATXF totals it: usage on assistant messages,
|
|
56
|
+
* compactions and branch summaries, and usage records that are not running
|
|
57
|
+
* totals. Every record counts, abandoned branches too, since they were paid
|
|
58
|
+
* for. Undefined when nothing reported usage.
|
|
59
|
+
*/
|
|
60
|
+
function transcriptUsage(transcript) {
|
|
61
|
+
let total;
|
|
62
|
+
for (const record of transcript.records) if (record.type === "message" && record.role === "assistant") total = addUsage(total, record.usage);
|
|
63
|
+
else if (record.type === "compaction" || record.type === "branch_summary") total = addUsage(total, record.usage);
|
|
64
|
+
else if (record.type === "usage" && !record.cumulative) total = addUsage(total, record.usage);
|
|
65
|
+
return total;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The built-in token metrics for the usage a transcript reported, each usage
|
|
69
|
+
* counted on its own and then summed; none when nothing reported usage.
|
|
70
|
+
*/
|
|
71
|
+
function usageMetrics(usages) {
|
|
72
|
+
if (!usages.length) return [];
|
|
73
|
+
const sum = (read) => usages.reduce((total, usage) => total + read(usage), 0);
|
|
74
|
+
const cacheRead = sum((usage) => usage.cacheRead ?? 0);
|
|
75
|
+
const input = sum((usage) => usage.input + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0));
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
name: "tokens",
|
|
79
|
+
value: sum(uncachedTokens),
|
|
80
|
+
unit: "tokens",
|
|
81
|
+
better: "lower",
|
|
82
|
+
builtIn: true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "cached tokens",
|
|
86
|
+
value: cacheRead,
|
|
87
|
+
unit: "tokens",
|
|
88
|
+
builtIn: true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "output tokens",
|
|
92
|
+
value: sum((usage) => usage.output),
|
|
93
|
+
unit: "tokens",
|
|
94
|
+
better: "lower",
|
|
95
|
+
builtIn: true
|
|
96
|
+
},
|
|
97
|
+
...input > 0 ? [{
|
|
98
|
+
name: "cache hit rate",
|
|
99
|
+
value: cacheRead / input,
|
|
100
|
+
better: "higher",
|
|
101
|
+
builtIn: true
|
|
102
|
+
}] : []
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Every usage a transcript reports that ATXF counts toward its total: on
|
|
107
|
+
* assistant messages, compactions and branch summaries, and usage records
|
|
108
|
+
* that are not running totals.
|
|
109
|
+
*/
|
|
110
|
+
function reportedUsages(transcript) {
|
|
111
|
+
const usages = [];
|
|
112
|
+
for (const record of transcript.records) {
|
|
113
|
+
const usage = record.type === "message" && record.role === "assistant" ? record.usage : record.type === "compaction" || record.type === "branch_summary" ? record.usage : record.type === "usage" && !record.cumulative ? record.usage : void 0;
|
|
114
|
+
if (usage) usages.push(usage);
|
|
115
|
+
}
|
|
116
|
+
return usages;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region ../core/src/upgrade.ts
|
|
120
|
+
/** Report schema versions `evalkit upgrade` converts to version 6 first. */
|
|
121
|
+
const UPGRADABLE_SCHEMA_VERSIONS = [
|
|
122
|
+
2,
|
|
123
|
+
3,
|
|
124
|
+
4,
|
|
125
|
+
5
|
|
126
|
+
];
|
|
127
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
/**
|
|
129
|
+
* A version 6 run status from an old trial's status and whether it passed.
|
|
130
|
+
* Old `completed` meant "finished", passed or not; old `failed` meant it
|
|
131
|
+
* could not finish, which version 6 calls `errored`.
|
|
132
|
+
*/
|
|
133
|
+
function upgradeStatus(status, passed) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case "completed": return passed ? "passed" : "failed";
|
|
136
|
+
case "failed": return "errored";
|
|
137
|
+
case "cancelled": return "cancelled";
|
|
138
|
+
default: return "running";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** A trajectory event with the old trial lifecycle events renamed. */
|
|
142
|
+
function upgradeEvent(event) {
|
|
143
|
+
if (!isRecord(event)) return event;
|
|
144
|
+
if (event.kind === "trial-started") return {
|
|
145
|
+
...event,
|
|
146
|
+
kind: "run-started"
|
|
147
|
+
};
|
|
148
|
+
if (event.kind === "trial-completed") return {
|
|
149
|
+
...event,
|
|
150
|
+
kind: "run-completed"
|
|
151
|
+
};
|
|
152
|
+
return event;
|
|
153
|
+
}
|
|
154
|
+
/** A whole `trajectory.jsonl`, line by line; lines that are not JSON stay as they are. */
|
|
155
|
+
function upgradeTrajectory(jsonl) {
|
|
156
|
+
return jsonl.split("\n").map((line) => {
|
|
157
|
+
if (!line.trim()) return line;
|
|
158
|
+
try {
|
|
159
|
+
return JSON.stringify(upgradeEvent(JSON.parse(line)));
|
|
160
|
+
} catch {
|
|
161
|
+
return line;
|
|
162
|
+
}
|
|
163
|
+
}).join("\n");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A catalog (`catalog.json`, or its `catalog` list on its own) with each
|
|
167
|
+
* scenario's `trialCount` and `policy.trials` replaced by `repeat`.
|
|
168
|
+
*/
|
|
169
|
+
function upgradeCatalog(catalog) {
|
|
170
|
+
const entry = (value) => {
|
|
171
|
+
if (!isRecord(value)) return value;
|
|
172
|
+
const { trialCount, policy, ...rest } = value;
|
|
173
|
+
const { trials, ...keptPolicy } = isRecord(policy) ? policy : {};
|
|
174
|
+
const repeat = typeof trialCount === "number" ? trialCount : typeof trials === "number" ? trials : value.repeat;
|
|
175
|
+
return {
|
|
176
|
+
...rest,
|
|
177
|
+
...repeat === void 0 ? {} : { repeat },
|
|
178
|
+
...isRecord(policy) ? { policy: keptPolicy } : {}
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
if (Array.isArray(catalog)) return catalog.map(entry);
|
|
182
|
+
if (isRecord(catalog) && Array.isArray(catalog.catalog)) return {
|
|
183
|
+
...catalog,
|
|
184
|
+
catalog: catalog.catalog.map(entry)
|
|
185
|
+
};
|
|
186
|
+
return catalog;
|
|
187
|
+
}
|
|
188
|
+
/** The fields of an old run manifest and trial manifest that a version 6 manifest keeps. */
|
|
189
|
+
function upgradeManifest(input) {
|
|
190
|
+
const { run, trial = {}, runId, batch, status } = input;
|
|
191
|
+
const pick = (source, keys) => Object.fromEntries(keys.flatMap((key) => source[key] === void 0 ? [] : [[key, source[key]]]));
|
|
192
|
+
return {
|
|
193
|
+
schemaVersion: 6,
|
|
194
|
+
runUri: `evalkit:run:${runId}`,
|
|
195
|
+
...pick(run, [
|
|
196
|
+
"evalId",
|
|
197
|
+
"suiteId",
|
|
198
|
+
"parameters",
|
|
199
|
+
"matrix",
|
|
200
|
+
"aut",
|
|
201
|
+
"startedAt",
|
|
202
|
+
"source",
|
|
203
|
+
"process"
|
|
204
|
+
]),
|
|
205
|
+
...pick(trial, [
|
|
206
|
+
"evalId",
|
|
207
|
+
"parameters",
|
|
208
|
+
"matrix",
|
|
209
|
+
"aut",
|
|
210
|
+
"startedAt",
|
|
211
|
+
"workspaceIgnore",
|
|
212
|
+
"datasets"
|
|
213
|
+
]),
|
|
214
|
+
...batch ? { batch } : {},
|
|
215
|
+
status
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Scenario options from before 0.9 that plain JavaScript scenarios can still
|
|
220
|
+
* carry. Ignoring them would silently change what runs, so both
|
|
221
|
+
* `defineEval` and the runner reject them with the replacement.
|
|
222
|
+
*/
|
|
223
|
+
function assertNoRemovedOptions(definition) {
|
|
224
|
+
if ("fixtures" in definition) throw new Error(`Eval ${String(definition.id)} uses fixtures, which were replaced by datasets: [defineDataset(...)]`);
|
|
225
|
+
if ("judge" in definition) throw new Error(`Eval ${String(definition.id)} sets judge, which moved to each rule: judge(name, { agent, rubric }), or judge in evalkit.config.ts for every judge (EvalKit 0.10)`);
|
|
226
|
+
if (isRecord(definition.policy) && "timeoutMs" in definition.policy) throw new Error(`Eval ${String(definition.id)} sets policy.timeoutMs, which was replaced by limits: { time: ${JSON.stringify(definition.policy.timeoutMs)} }, enforced since EvalKit 0.11`);
|
|
227
|
+
if (isRecord(definition.policy) && "trials" in definition.policy) throw new Error(`Eval ${String(definition.id)} sets policy.trials, which was replaced by repeat: ${JSON.stringify(definition.policy.trials)} (EvalKit 0.9)`);
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { upgradeManifest as a, normalizeUsage as c, uncachedTokens as d, usageMetrics as f, upgradeEvent as i, reportedUsages as l, assertNoRemovedOptions as n, upgradeStatus as o, upgradeCatalog as r, upgradeTrajectory as s, UPGRADABLE_SCHEMA_VERSIONS as t, transcriptUsage as u };
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { d as uncachedTokens, f as usageMetrics, l as reportedUsages } from "./upgrade-rMsGzUEJ.mjs";
|
|
2
|
+
//#region ../core/src/upgrade-transcript.ts
|
|
3
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
/** The `source.format` of a transcript converted from a version 6 trajectory. */
|
|
5
|
+
const V6_TRAJECTORY_FORMAT = "evalkit-trajectory-v6";
|
|
6
|
+
/** Old usage: input counted cached tokens, which were reported as a subset. */
|
|
7
|
+
function upgradeUsage(value) {
|
|
8
|
+
if (!isRecord(value)) return void 0;
|
|
9
|
+
const num = (key) => typeof value[key] === "number" ? value[key] : void 0;
|
|
10
|
+
const input = num("inputTokens");
|
|
11
|
+
const output = num("outputTokens");
|
|
12
|
+
const cached = num("cachedInputTokens");
|
|
13
|
+
const total = num("totalTokens");
|
|
14
|
+
if (input === void 0 && output === void 0) {
|
|
15
|
+
if (total === void 0) return void 0;
|
|
16
|
+
return {
|
|
17
|
+
input: 0,
|
|
18
|
+
output: 0,
|
|
19
|
+
total
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const cacheRead = Math.min(cached ?? 0, input ?? 0);
|
|
23
|
+
return {
|
|
24
|
+
input: (input ?? 0) - cacheRead,
|
|
25
|
+
output: output ?? 0,
|
|
26
|
+
...cacheRead ? { cacheRead } : {},
|
|
27
|
+
...total === void 0 ? {} : { total }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function parseModel(model) {
|
|
31
|
+
if (typeof model !== "string" || !model) return void 0;
|
|
32
|
+
const slash = model.indexOf("/");
|
|
33
|
+
return slash > 0 && slash < model.length - 1 ? {
|
|
34
|
+
provider: model.slice(0, slash),
|
|
35
|
+
id: model.slice(slash + 1)
|
|
36
|
+
} : {
|
|
37
|
+
provider: "unknown",
|
|
38
|
+
id: model
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const text = (value) => typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
42
|
+
/**
|
|
43
|
+
* Splits a version 6 trajectory (its events, parsed) into an ATXF
|
|
44
|
+
* transcript of the agent's events and EvalKit's events placed in it.
|
|
45
|
+
* Every record carries the old events it came from in `native`.
|
|
46
|
+
*/
|
|
47
|
+
function convertTrajectory(events, run) {
|
|
48
|
+
const records = [];
|
|
49
|
+
const runEvents = [];
|
|
50
|
+
const recordOfEvent = /* @__PURE__ */ new Map();
|
|
51
|
+
let lastId = null;
|
|
52
|
+
let next = 0;
|
|
53
|
+
let sawUsage = false;
|
|
54
|
+
let sawTurns = false;
|
|
55
|
+
/** The assistant message the next tool call joins, while nothing came after it. */
|
|
56
|
+
let joinable;
|
|
57
|
+
let openTurn;
|
|
58
|
+
/**
|
|
59
|
+
* 0.10 recorded EvalKit's prompt only on its step event; it becomes the
|
|
60
|
+
* user message, unless the adapter recorded the same message itself.
|
|
61
|
+
*/
|
|
62
|
+
let prompt;
|
|
63
|
+
const native = (...old) => ({
|
|
64
|
+
source: "evalkit",
|
|
65
|
+
format: V6_TRAJECTORY_FORMAT,
|
|
66
|
+
records: old
|
|
67
|
+
});
|
|
68
|
+
const push = (record, index) => {
|
|
69
|
+
const full = {
|
|
70
|
+
...record,
|
|
71
|
+
id: `r${++next}`,
|
|
72
|
+
parentId: lastId
|
|
73
|
+
};
|
|
74
|
+
records.push(full);
|
|
75
|
+
lastId = full.id;
|
|
76
|
+
recordOfEvent.set(index, full.id);
|
|
77
|
+
joinable = void 0;
|
|
78
|
+
return full;
|
|
79
|
+
};
|
|
80
|
+
const flushPrompt = () => {
|
|
81
|
+
if (!prompt) return;
|
|
82
|
+
const { content, time, index, event } = prompt;
|
|
83
|
+
prompt = void 0;
|
|
84
|
+
push({
|
|
85
|
+
type: "message",
|
|
86
|
+
time,
|
|
87
|
+
role: "user",
|
|
88
|
+
content,
|
|
89
|
+
native: native(event)
|
|
90
|
+
}, index);
|
|
91
|
+
};
|
|
92
|
+
events.forEach((event, index) => {
|
|
93
|
+
if (!isRecord(event)) return;
|
|
94
|
+
const time = typeof event.timestamp === "string" ? event.timestamp : run.startedAt;
|
|
95
|
+
if (event.source !== "aut") {
|
|
96
|
+
const { source: _, ...rest } = event;
|
|
97
|
+
if (event.kind !== "transcript-step-started") flushPrompt();
|
|
98
|
+
runEvents.push({
|
|
99
|
+
...rest,
|
|
100
|
+
at: lastId
|
|
101
|
+
});
|
|
102
|
+
if (event.kind === "transcript-step-started" && typeof event.message === "string") prompt = {
|
|
103
|
+
content: event.message,
|
|
104
|
+
time,
|
|
105
|
+
index,
|
|
106
|
+
event
|
|
107
|
+
};
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (prompt && event.kind === "message" && event.role === "user" && event.content === prompt.content) prompt = void 0;
|
|
111
|
+
else flushPrompt();
|
|
112
|
+
switch (event.kind) {
|
|
113
|
+
case "message": {
|
|
114
|
+
const role = event.role;
|
|
115
|
+
const content = event.content;
|
|
116
|
+
const extra = typeof content === "string" ? {} : { content };
|
|
117
|
+
const ext = Object.keys(extra).length ? { ext: { evalkit: extra } } : {};
|
|
118
|
+
if (role === "assistant") joinable = push({
|
|
119
|
+
type: "message",
|
|
120
|
+
time,
|
|
121
|
+
role: "assistant",
|
|
122
|
+
content: [{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: text(content)
|
|
125
|
+
}],
|
|
126
|
+
native: native(event),
|
|
127
|
+
...ext
|
|
128
|
+
}, index);
|
|
129
|
+
else if (role === "tool") push({
|
|
130
|
+
type: "message",
|
|
131
|
+
time,
|
|
132
|
+
role: "tool",
|
|
133
|
+
toolCallId: `unknown-${index}`,
|
|
134
|
+
content: text(content),
|
|
135
|
+
native: native(event),
|
|
136
|
+
...ext
|
|
137
|
+
}, index);
|
|
138
|
+
else push({
|
|
139
|
+
type: "message",
|
|
140
|
+
time,
|
|
141
|
+
role: role === "system" ? "system" : "user",
|
|
142
|
+
content: text(content),
|
|
143
|
+
native: native(event),
|
|
144
|
+
...ext
|
|
145
|
+
}, index);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "tool-call": {
|
|
149
|
+
const block = {
|
|
150
|
+
type: "tool_call",
|
|
151
|
+
id: String(event.id ?? `call-${index}`),
|
|
152
|
+
name: String(event.name ?? "tool"),
|
|
153
|
+
kind: "function",
|
|
154
|
+
arguments: isRecord(event.arguments) ? event.arguments : { value: event.arguments ?? null }
|
|
155
|
+
};
|
|
156
|
+
if (joinable) {
|
|
157
|
+
joinable.content.push(block);
|
|
158
|
+
joinable.native.records.push(event);
|
|
159
|
+
recordOfEvent.set(index, joinable.id);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
joinable = push({
|
|
163
|
+
type: "message",
|
|
164
|
+
time,
|
|
165
|
+
role: "assistant",
|
|
166
|
+
content: [block],
|
|
167
|
+
native: native(event)
|
|
168
|
+
}, index);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
case "tool-result": {
|
|
172
|
+
const result = event.result ?? null;
|
|
173
|
+
push({
|
|
174
|
+
type: "message",
|
|
175
|
+
time,
|
|
176
|
+
role: "tool",
|
|
177
|
+
toolCallId: String(event.id ?? `call-${index}`),
|
|
178
|
+
...typeof event.name === "string" ? { toolName: event.name } : {},
|
|
179
|
+
content: text(result),
|
|
180
|
+
...typeof result === "string" ? {} : { details: { result } },
|
|
181
|
+
native: native(event)
|
|
182
|
+
}, index);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
case "turn-started": {
|
|
186
|
+
sawTurns = true;
|
|
187
|
+
const turnId = `turn-${String(event.turn ?? index)}`;
|
|
188
|
+
push({
|
|
189
|
+
type: "turn",
|
|
190
|
+
time,
|
|
191
|
+
phase: "start",
|
|
192
|
+
turnId,
|
|
193
|
+
native: native(event)
|
|
194
|
+
}, index);
|
|
195
|
+
openTurn = {
|
|
196
|
+
turnId,
|
|
197
|
+
from: records.length
|
|
198
|
+
};
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
case "turn-completed": {
|
|
202
|
+
const usage = upgradeUsage(event.usage);
|
|
203
|
+
const model = parseModel(event.model);
|
|
204
|
+
const from = openTurn?.from ?? 0;
|
|
205
|
+
let holder;
|
|
206
|
+
for (let at = records.length - 1; at >= from; at -= 1) {
|
|
207
|
+
const record = records[at];
|
|
208
|
+
if (record.type === "message" && record.role === "assistant") {
|
|
209
|
+
holder = record;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (usage) sawUsage = true;
|
|
214
|
+
const totalOnly = usage && usage.input === 0 && usage.output === 0 && !usage.cacheRead;
|
|
215
|
+
if (holder && !(usage && holder.usage)) {
|
|
216
|
+
if (usage) holder.usage = usage;
|
|
217
|
+
if (model) holder.model = model;
|
|
218
|
+
if (totalOnly) holder.ext = {
|
|
219
|
+
...holder.ext,
|
|
220
|
+
evalkit: {
|
|
221
|
+
...holder.ext?.evalkit,
|
|
222
|
+
totalOnly: true
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
} else if (usage) push({
|
|
226
|
+
type: "usage",
|
|
227
|
+
time,
|
|
228
|
+
kind: "turn",
|
|
229
|
+
usage,
|
|
230
|
+
...model ? { model } : {},
|
|
231
|
+
...totalOnly ? { ext: { evalkit: { totalOnly: true } } } : {}
|
|
232
|
+
}, index);
|
|
233
|
+
const latency = typeof event.latencyMs === "number" ? { ext: { evalkit: { latencyMs: event.latencyMs } } } : {};
|
|
234
|
+
if (openTurn) {
|
|
235
|
+
push({
|
|
236
|
+
type: "turn",
|
|
237
|
+
time,
|
|
238
|
+
phase: "end",
|
|
239
|
+
turnId: openTurn.turnId,
|
|
240
|
+
outcome: "succeeded",
|
|
241
|
+
native: native(event),
|
|
242
|
+
...latency
|
|
243
|
+
}, index);
|
|
244
|
+
openTurn = void 0;
|
|
245
|
+
} else if (holder) {
|
|
246
|
+
holder.native.records.push(event);
|
|
247
|
+
recordOfEvent.set(index, holder.id);
|
|
248
|
+
} else push({
|
|
249
|
+
type: "custom",
|
|
250
|
+
time,
|
|
251
|
+
namespace: "evalkit",
|
|
252
|
+
name: "turn-completed",
|
|
253
|
+
data: event,
|
|
254
|
+
native: native(event)
|
|
255
|
+
}, index);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
default: {
|
|
259
|
+
const { source: _, kind, timestamp: __, ...data } = event;
|
|
260
|
+
push({
|
|
261
|
+
type: "custom",
|
|
262
|
+
time,
|
|
263
|
+
namespace: "evalkit",
|
|
264
|
+
name: String(kind),
|
|
265
|
+
...Object.keys(data).length ? { data } : {},
|
|
266
|
+
native: native(event)
|
|
267
|
+
}, index);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
flushPrompt();
|
|
272
|
+
const firstTime = records[0]?.time ?? run.startedAt;
|
|
273
|
+
return {
|
|
274
|
+
transcript: {
|
|
275
|
+
header: {
|
|
276
|
+
type: "transcript",
|
|
277
|
+
atxf: "0.1",
|
|
278
|
+
id: run.runId,
|
|
279
|
+
time: firstTime,
|
|
280
|
+
source: {
|
|
281
|
+
agent: run.agent?.id ?? "unknown",
|
|
282
|
+
...run.agent?.version ? { version: run.agent.version } : {},
|
|
283
|
+
format: V6_TRAJECTORY_FORMAT
|
|
284
|
+
},
|
|
285
|
+
fidelity: {
|
|
286
|
+
topology: "linear",
|
|
287
|
+
instructions: "none",
|
|
288
|
+
tools: "none",
|
|
289
|
+
reasoning: "none",
|
|
290
|
+
toolLifecycle: false,
|
|
291
|
+
turns: sawTurns,
|
|
292
|
+
usage: sawUsage ? "per-response" : "none",
|
|
293
|
+
native: "all",
|
|
294
|
+
ids: "synthetic"
|
|
295
|
+
},
|
|
296
|
+
ext: { evalkit: {
|
|
297
|
+
runId: run.runId,
|
|
298
|
+
...run.evalId ? { scenarioId: run.evalId } : {}
|
|
299
|
+
} }
|
|
300
|
+
},
|
|
301
|
+
records
|
|
302
|
+
},
|
|
303
|
+
runEvents,
|
|
304
|
+
recordOfEvent
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* A version 6 `scoring.json` for version 7: each judge's events become its
|
|
309
|
+
* transcript and its usage moves to ATXF's buckets, each checkpoint's
|
|
310
|
+
* matched tool call points at the record holding it, the token metrics
|
|
311
|
+
* are recomputed from the converted transcript, and `steps` is added.
|
|
312
|
+
* Throws when the recomputed tokens (uncached plus cached)
|
|
313
|
+
* do not add up to the saved ones, so a run is never converted wrongly.
|
|
314
|
+
*/
|
|
315
|
+
function upgradeScoring(scoring, run, recordOfEvent, agent) {
|
|
316
|
+
if (!isRecord(scoring)) throw new Error("scoring.json is not an object");
|
|
317
|
+
const judgeUsage = [];
|
|
318
|
+
const verdict = (entry, index) => {
|
|
319
|
+
if (!isRecord(entry)) return entry;
|
|
320
|
+
const result = { ...entry };
|
|
321
|
+
if (isRecord(result.matchedToolCall)) {
|
|
322
|
+
const { eventIndex, id } = result.matchedToolCall;
|
|
323
|
+
const messageId = typeof eventIndex === "number" ? recordOfEvent.get(eventIndex) : void 0;
|
|
324
|
+
if (messageId && typeof id === "string") result.matchedToolCall = {
|
|
325
|
+
messageId,
|
|
326
|
+
id
|
|
327
|
+
};
|
|
328
|
+
else delete result.matchedToolCall;
|
|
329
|
+
}
|
|
330
|
+
if (isRecord(result.judge)) {
|
|
331
|
+
const { events, usage, ...judge } = result.judge;
|
|
332
|
+
const converted = Array.isArray(events) ? convertTrajectory(events.map((event) => isRecord(event) ? {
|
|
333
|
+
...event,
|
|
334
|
+
source: "aut"
|
|
335
|
+
} : event), {
|
|
336
|
+
runId: `${run.runId}-judge-${index}`,
|
|
337
|
+
startedAt: run.startedAt,
|
|
338
|
+
...isRecord(judge.agent) && typeof judge.agent.id === "string" ? { agent: { id: judge.agent.id } } : {}
|
|
339
|
+
}).transcript : void 0;
|
|
340
|
+
const atxfUsage = upgradeUsage(usage);
|
|
341
|
+
if (atxfUsage) judgeUsage.push(atxfUsage);
|
|
342
|
+
result.judge = {
|
|
343
|
+
...judge,
|
|
344
|
+
...atxfUsage ? { usage: atxfUsage } : {},
|
|
345
|
+
...converted ? { transcript: converted } : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
};
|
|
350
|
+
const results = Array.isArray(scoring.results) ? scoring.results.map((entry, index) => verdict(entry, index)) : [];
|
|
351
|
+
const checkpoints = Array.isArray(scoring.checkpoints) ? scoring.checkpoints.map((entry, index) => verdict(entry, results.length + index)) : void 0;
|
|
352
|
+
const metrics = Array.isArray(scoring.metrics) ? upgradeMetrics(scoring.metrics, agent, judgeUsage) : void 0;
|
|
353
|
+
return {
|
|
354
|
+
...scoring,
|
|
355
|
+
results,
|
|
356
|
+
...checkpoints ? { checkpoints } : {},
|
|
357
|
+
...metrics ? { metrics } : {}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Built-in metrics recomputed from the converted transcript. */
|
|
361
|
+
const RECOMPUTED_METRICS = /* @__PURE__ */ new Set([
|
|
362
|
+
"steps",
|
|
363
|
+
"tokens",
|
|
364
|
+
"cached tokens",
|
|
365
|
+
"output tokens",
|
|
366
|
+
"cache hit rate",
|
|
367
|
+
"judge tokens"
|
|
368
|
+
]);
|
|
369
|
+
function upgradeMetrics(metrics, transcript, judges) {
|
|
370
|
+
const agent = reportedUsages(transcript);
|
|
371
|
+
const steps = transcript.records.filter((record) => record.type === "message" && record.role === "assistant").length;
|
|
372
|
+
const saved = (name) => {
|
|
373
|
+
const entry = metrics.find((metric) => isRecord(metric) && metric.name === name);
|
|
374
|
+
return isRecord(entry) && typeof entry.value === "number" ? entry.value : void 0;
|
|
375
|
+
};
|
|
376
|
+
const check = (name, before, usages) => {
|
|
377
|
+
if (before === void 0) return;
|
|
378
|
+
const after = usages.reduce((sum, usage) => sum + uncachedTokens(usage) + (usage.cacheRead ?? 0), 0);
|
|
379
|
+
if (after !== before) throw new Error(`${name} do not add up after converting: ${before} saved, ${after} recomputed`);
|
|
380
|
+
};
|
|
381
|
+
check("tokens", saved("tokens"), agent);
|
|
382
|
+
check("judge tokens", saved("judge tokens"), judges);
|
|
383
|
+
return [
|
|
384
|
+
...metrics.filter((metric) => !(isRecord(metric) && RECOMPUTED_METRICS.has(String(metric.name)))),
|
|
385
|
+
{
|
|
386
|
+
name: "steps",
|
|
387
|
+
value: steps,
|
|
388
|
+
better: "lower",
|
|
389
|
+
builtIn: true
|
|
390
|
+
},
|
|
391
|
+
...usageMetrics(agent).map((metric) => metric),
|
|
392
|
+
...judges.length ? [{
|
|
393
|
+
name: "judge tokens",
|
|
394
|
+
value: judges.reduce((sum, usage) => sum + uncachedTokens(usage), 0),
|
|
395
|
+
unit: "tokens",
|
|
396
|
+
better: "lower",
|
|
397
|
+
builtIn: true
|
|
398
|
+
}] : []
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
//#endregion
|
|
402
|
+
export { upgradeScoring as n, convertTrajectory as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leostera/evalkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Author and run agent evals as code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"evalkit": "dist/cli.mjs"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
+
"@leostera/atxf": "0.1.1",
|
|
49
50
|
"effect": "^3.22.2",
|
|
50
51
|
"hono": "^4.11.6",
|
|
51
52
|
"jiti": "^2.7.0"
|