@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,6 +1,7 @@
|
|
|
1
|
+
import { n as assertNoRemovedOptions } from "./upgrade-G-uFPJKg.mjs";
|
|
1
2
|
import * as Schema from "effect/Schema";
|
|
2
3
|
//#region ../core/src/identity.ts
|
|
3
|
-
const ResourceKindSchema = Schema.Literal("run", "
|
|
4
|
+
const ResourceKindSchema = Schema.Literal("run", "artifact");
|
|
4
5
|
/** Human-authored project-local identifier; never a generated resource URI. */
|
|
5
6
|
function authorId(value) {
|
|
6
7
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) throw new Error(`ID must be lowercase kebab-case: ${value}`);
|
|
@@ -8,7 +9,7 @@ function authorId(value) {
|
|
|
8
9
|
}
|
|
9
10
|
/** RFC 4122 textual UUID. EvalKit accepts v4/v7 and other valid UUID versions. */
|
|
10
11
|
const UuidSchema = Schema.String.pipe(Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i));
|
|
11
|
-
const ResourceUriSchema = Schema.String.pipe(Schema.pattern(/^evalkit:(run|
|
|
12
|
+
const ResourceUriSchema = Schema.String.pipe(Schema.pattern(/^evalkit:(run|artifact):[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i));
|
|
12
13
|
function resourceUriSchema(kind) {
|
|
13
14
|
return Schema.String.pipe(Schema.pattern(new RegExp(`^evalkit:${kind}:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`, "i")));
|
|
14
15
|
}
|
|
@@ -16,7 +17,7 @@ function resourceUri(kind, uuid) {
|
|
|
16
17
|
return `evalkit:${kind}:${uuid}`;
|
|
17
18
|
}
|
|
18
19
|
function parseResourceUri(value, expectedKind) {
|
|
19
|
-
const match = /^evalkit:(run|
|
|
20
|
+
const match = /^evalkit:(run|artifact):(.+)$/.exec(value);
|
|
20
21
|
if (!match) throw new Error(`Invalid EvalKit resource URI: ${value}`);
|
|
21
22
|
const kind = match[1];
|
|
22
23
|
if (expectedKind && kind !== expectedKind) throw new Error(`Expected an EvalKit ${expectedKind} URI; received ${value}`);
|
|
@@ -101,10 +102,10 @@ function relativeDatasetPath(value, label) {
|
|
|
101
102
|
return parts.length ? parts.join("/") : ".";
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
|
-
* Declares named data that scenarios use: files placed in each
|
|
105
|
+
* Declares named data that scenarios use: files placed in each run's
|
|
105
106
|
* workspace, a value for the agent and scorers, or both. Static datasets give
|
|
106
107
|
* `files`, `value` or `valueFile`; computed datasets give `onLoad` (cached per
|
|
107
|
-
* process) and optionally `onRun` (per
|
|
108
|
+
* process) and optionally `onRun` (per run).
|
|
108
109
|
*/
|
|
109
110
|
function defineDataset(definition) {
|
|
110
111
|
return markDefinitionSite(checkedDataset(definition));
|
|
@@ -205,11 +206,194 @@ function toJsonObject(value, path) {
|
|
|
205
206
|
return converted;
|
|
206
207
|
}
|
|
207
208
|
//#endregion
|
|
209
|
+
//#region ../core/src/judge.ts
|
|
210
|
+
const KEY = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
211
|
+
function unitInterval(value) {
|
|
212
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A judge that answers with one of these choices, in this order:
|
|
216
|
+
* `choices({ A: ['Accurate and helpful', 1], B: ['Partly right', 0.5], C: ['Wrong', 0] })`.
|
|
217
|
+
*/
|
|
218
|
+
function choices(options) {
|
|
219
|
+
const entries = Object.entries(options);
|
|
220
|
+
if (entries.length < 2) throw new Error("choices(...) needs at least two choices");
|
|
221
|
+
const seen = /* @__PURE__ */ new Set();
|
|
222
|
+
return {
|
|
223
|
+
kind: "choices",
|
|
224
|
+
choices: entries.map(([key, [label, value]]) => {
|
|
225
|
+
if (!KEY.test(key)) throw new Error(`Choice ${JSON.stringify(key)} must be a short word of letters, digits, "-" or "_"`);
|
|
226
|
+
if (seen.has(key.toUpperCase())) throw new Error(`Choice ${key} is listed twice`);
|
|
227
|
+
seen.add(key.toUpperCase());
|
|
228
|
+
if (!label.trim()) throw new Error(`Choice ${key} needs a label`);
|
|
229
|
+
if (!unitInterval(value)) throw new Error(`Choice ${key} must be worth a number from 0 to 1; received ${String(value)}`);
|
|
230
|
+
return {
|
|
231
|
+
key,
|
|
232
|
+
label,
|
|
233
|
+
value
|
|
234
|
+
};
|
|
235
|
+
})
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* A judge that scores each criterion from 0 to 1; the score is the weighted
|
|
240
|
+
* mean. A number is the weight; `{ weight, description }` also says what the
|
|
241
|
+
* criterion means. Weights default to 1.
|
|
242
|
+
*/
|
|
243
|
+
function criteria(options) {
|
|
244
|
+
return {
|
|
245
|
+
kind: "criteria",
|
|
246
|
+
criteria: declaredCriteria(options)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Checked criteria declarations, for `criteria()` and `predicate(..., { criteria })`. */
|
|
250
|
+
function declaredCriteria(options) {
|
|
251
|
+
const entries = Object.entries(options);
|
|
252
|
+
if (!entries.length) throw new Error("criteria(...) needs a criterion");
|
|
253
|
+
return entries.map(([name, option]) => {
|
|
254
|
+
if (!name.trim()) throw new Error("A criterion needs a name");
|
|
255
|
+
const weight = typeof option === "number" ? option : option.weight ?? 1;
|
|
256
|
+
if (!Number.isFinite(weight) || weight <= 0) throw new Error(`Criterion ${name} must have a positive weight; received ${String(weight)}`);
|
|
257
|
+
const description = typeof option === "number" ? void 0 : option.description;
|
|
258
|
+
return {
|
|
259
|
+
name,
|
|
260
|
+
weight,
|
|
261
|
+
...description ? { description } : {}
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** What a criterion counts for in the weighted mean. */
|
|
266
|
+
const criterionValue = (result) => "value" in result ? result.value : 0;
|
|
267
|
+
/** The weighted mean of criteria; weights default to 1, a criterion not reached counts 0. */
|
|
268
|
+
function weightedMean(results) {
|
|
269
|
+
let total = 0;
|
|
270
|
+
let weights = 0;
|
|
271
|
+
for (const result of results) {
|
|
272
|
+
const weight = result.weight ?? 1;
|
|
273
|
+
total += weight * criterionValue(result);
|
|
274
|
+
weights += weight;
|
|
275
|
+
}
|
|
276
|
+
if (!weights) throw new Error("Criteria need a positive total weight");
|
|
277
|
+
return total / weights;
|
|
278
|
+
}
|
|
279
|
+
/** The inputs a judge sees when the rule names none. */
|
|
280
|
+
const DEFAULT_JUDGE_INPUTS = ["conversation", "reply"];
|
|
281
|
+
/** The names of the inputs a judge sees, in prompt order. */
|
|
282
|
+
function judgeInputNames(rule) {
|
|
283
|
+
return rule.inputs ? Object.keys(rule.inputs) : [...DEFAULT_JUDGE_INPUTS];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The prompt a judge agent receives. `values` holds each input's text;
|
|
287
|
+
* an input without one shows as a `{placeholder}`, which is how the
|
|
288
|
+
* dashboard shows the prompt before any run.
|
|
289
|
+
*/
|
|
290
|
+
function judgePrompt(options) {
|
|
291
|
+
const inputs = options.inputs.map((name) => `${name}:\n${options.values?.[name] ?? `{${name}}`}`).join("\n\n");
|
|
292
|
+
return [
|
|
293
|
+
"You are judging one run of an AI agent. Judge only by the rubric, using only the inputs below.",
|
|
294
|
+
`Rubric:\n${options.rubric.trim()}`,
|
|
295
|
+
inputs,
|
|
296
|
+
answerInstruction(options.answer)
|
|
297
|
+
].join("\n\n");
|
|
298
|
+
}
|
|
299
|
+
function answerInstruction(answer) {
|
|
300
|
+
switch (answer.kind) {
|
|
301
|
+
case "verdict": return "Answer with only a JSON object: {\"value\": <a number from 0 to 1>, \"explanation\": \"<one or two sentences why>\"}";
|
|
302
|
+
case "choices": return [`Answer with one of ${answer.choices.map((choice) => choice.key).join(", ")} on the first line, then one sentence why on the next.`, ...answer.choices.map((choice) => `${choice.key}) ${choice.label}`)].join("\n");
|
|
303
|
+
case "criteria": return [
|
|
304
|
+
"Score each criterion from 0 to 1:",
|
|
305
|
+
...answer.criteria.map((criterion) => `- ${criterion.name}${criterion.description ? `: ${criterion.description}` : ""}`),
|
|
306
|
+
`Answer with only a JSON object: {"criteria": {${answer.criteria.map((criterion) => `${JSON.stringify(criterion.name)}: {"value": <0 to 1>, "explanation": "<why>"}`).join(", ")}}, "explanation": "<one sentence overall>"}`
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Thrown when a judge's reply is not in the format asked for; the message says why. */
|
|
311
|
+
var UnreadableAnswer = class extends Error {};
|
|
312
|
+
/**
|
|
313
|
+
* Reads a judge's reply in the format its rule asked for. Throws
|
|
314
|
+
* `UnreadableAnswer` with a reason the judge can be told, so it can answer again.
|
|
315
|
+
*/
|
|
316
|
+
function readJudgeAnswer(answer, reply) {
|
|
317
|
+
if (answer.kind === "choices") return readChoice(answer.choices, reply);
|
|
318
|
+
const object = jsonObject(reply);
|
|
319
|
+
if (answer.kind === "verdict") {
|
|
320
|
+
if (!unitInterval(object.value)) throw new UnreadableAnswer("\"value\" must be a number from 0 to 1");
|
|
321
|
+
return {
|
|
322
|
+
value: object.value,
|
|
323
|
+
...typeof object.explanation === "string" && object.explanation.trim() ? { explanation: object.explanation.trim() } : {},
|
|
324
|
+
...object.evidence !== void 0 ? { evidence: object.evidence } : {}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const scored = object.criteria;
|
|
328
|
+
if (typeof scored !== "object" || scored === null || Array.isArray(scored)) throw new UnreadableAnswer("\"criteria\" must be an object with one entry per criterion");
|
|
329
|
+
const results = answer.criteria.map((criterion) => {
|
|
330
|
+
const entry = scored[criterion.name];
|
|
331
|
+
const value = typeof entry === "number" ? entry : typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry.value : void 0;
|
|
332
|
+
if (!unitInterval(value)) throw new UnreadableAnswer(`criterion ${JSON.stringify(criterion.name)} needs a "value" from 0 to 1`);
|
|
333
|
+
const explanation = typeof entry === "object" && entry !== null && !Array.isArray(entry) && typeof entry.explanation === "string" && entry.explanation.trim() ? entry.explanation.trim() : void 0;
|
|
334
|
+
return {
|
|
335
|
+
name: criterion.name,
|
|
336
|
+
value,
|
|
337
|
+
weight: criterion.weight,
|
|
338
|
+
...explanation ? { explanation } : {}
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
return {
|
|
342
|
+
value: weightedMean(results),
|
|
343
|
+
criteria: results,
|
|
344
|
+
...typeof object.explanation === "string" && object.explanation.trim() ? { explanation: object.explanation.trim() } : {}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function readChoice(options, reply) {
|
|
348
|
+
if (typeof reply !== "string") throw new UnreadableAnswer("answer with plain text, not JSON");
|
|
349
|
+
const lines = reply.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
350
|
+
const first = lines[0] ?? "";
|
|
351
|
+
const match = /^(?:answer\s*[:-]?\s*)?[*_("'[\s]*([A-Za-z0-9][A-Za-z0-9_-]*)/i.exec(first);
|
|
352
|
+
const said = match?.[1]?.toUpperCase();
|
|
353
|
+
const choice = options.find((option) => option.key.toUpperCase() === said);
|
|
354
|
+
if (!choice) throw new UnreadableAnswer(`the first line must be one of ${options.map((option) => option.key).join(", ")}`);
|
|
355
|
+
const explanation = [first.slice(match[0].length).replace(/^[*_)"'\].:\s-]+/, "").trim(), ...lines.slice(1)].filter(Boolean).join(" ");
|
|
356
|
+
return {
|
|
357
|
+
value: choice.value,
|
|
358
|
+
choice: choice.key,
|
|
359
|
+
...explanation ? { explanation } : {}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/** The JSON object in a reply: the reply itself, or the first `{…}` in its text (code fences allowed). */
|
|
363
|
+
function jsonObject(reply) {
|
|
364
|
+
if (typeof reply === "object" && reply !== null && !Array.isArray(reply)) return reply;
|
|
365
|
+
if (typeof reply !== "string") throw new UnreadableAnswer("answer with a JSON object");
|
|
366
|
+
const start = reply.indexOf("{");
|
|
367
|
+
const end = reply.lastIndexOf("}");
|
|
368
|
+
if (start === -1 || end < start) throw new UnreadableAnswer("answer with only a JSON object");
|
|
369
|
+
let parsed;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(reply.slice(start, end + 1));
|
|
372
|
+
} catch {
|
|
373
|
+
throw new UnreadableAnswer("the JSON object could not be parsed");
|
|
374
|
+
}
|
|
375
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new UnreadableAnswer("answer with a JSON object");
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The pass mark in words: `only 1`, `≥ 0.5`, or for choices the ones that
|
|
380
|
+
* pass, such as `A or B`.
|
|
381
|
+
*/
|
|
382
|
+
function passMark(passAt, answer) {
|
|
383
|
+
const at = passAt ?? 1;
|
|
384
|
+
if (answer?.kind === "choices") {
|
|
385
|
+
const passing = answer.choices.filter((choice) => choice.value >= at).map((choice) => choice.key);
|
|
386
|
+
if (!passing.length) return "no choice passes";
|
|
387
|
+
if (passing.length === answer.choices.length) return "any choice";
|
|
388
|
+
return passing.length === 1 ? `only ${passing[0]}` : `${passing.slice(0, -1).join(", ")} or ${passing.at(-1)}`;
|
|
389
|
+
}
|
|
390
|
+
return at === 1 ? "only 1" : `≥ ${at}`;
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
208
393
|
//#region ../core/src/catalog.ts
|
|
209
394
|
function catalogEntry(evaluation, suite, options = {}) {
|
|
210
395
|
const at = (value) => relativeSite(definitionSite(value), options.root);
|
|
211
396
|
const definedAt = at(evaluation);
|
|
212
|
-
const judgeAgent = evaluation.judge ? catalogAgent(evaluation.judge, at) : void 0;
|
|
213
397
|
const policy = evaluation.policy ?? {};
|
|
214
398
|
return {
|
|
215
399
|
id: evaluation.id,
|
|
@@ -217,16 +401,14 @@ function catalogEntry(evaluation, suite, options = {}) {
|
|
|
217
401
|
...evaluation.name ? { name: evaluation.name } : {},
|
|
218
402
|
...suite ? { suiteId: authoringId(suite) } : {},
|
|
219
403
|
agent: catalogAgent(evaluation.agent, at),
|
|
220
|
-
...judgeAgent ? { judgeAgent } : {},
|
|
221
404
|
datasets: (evaluation.datasets ?? []).map((dataset) => catalogDataset(dataset, at)),
|
|
222
|
-
|
|
405
|
+
repeat: evaluation.repeat ?? 1,
|
|
223
406
|
policy: {
|
|
224
|
-
trials: policy.trials ?? 1,
|
|
225
407
|
...policy.timeoutMs !== void 0 ? { timeoutMs: policy.timeoutMs } : {},
|
|
226
408
|
...policy.failfast !== void 0 ? { failfast: policy.failfast } : {}
|
|
227
409
|
},
|
|
228
|
-
transcript: evaluation.transcript.map((step) => catalogStep(step, at)),
|
|
229
|
-
scorers: evaluation.scoring.map((rule) => catalogScorer(rule, at)),
|
|
410
|
+
transcript: evaluation.transcript.map((step) => catalogStep(step, at, options.judge)),
|
|
411
|
+
scorers: evaluation.scoring.map((rule) => catalogScorer(rule, at, options.judge)),
|
|
230
412
|
...evaluation.metadata !== void 0 ? { metadata: toJson(evaluation.metadata, `${evaluation.id}.metadata`) } : {},
|
|
231
413
|
...definedAt ? { definedAt } : {}
|
|
232
414
|
};
|
|
@@ -270,18 +452,37 @@ function catalogDataset(dataset, at) {
|
|
|
270
452
|
...source.valueFile !== void 0 ? { value: { file: source.valueFile } } : source.value !== void 0 ? { value: { shape: shapeOf(source.value) } } : {}
|
|
271
453
|
};
|
|
272
454
|
}
|
|
273
|
-
function catalogScorer(rule, at) {
|
|
455
|
+
function catalogScorer(rule, at, defaultJudge) {
|
|
274
456
|
const definedAt = at(rule);
|
|
275
457
|
return {
|
|
276
458
|
name: rule.name,
|
|
277
459
|
kind: rule.kind,
|
|
278
|
-
...rule.
|
|
460
|
+
...rule.description ? { description: rule.description } : {},
|
|
461
|
+
...rule.passAt !== void 0 ? { passAt: rule.passAt } : {},
|
|
279
462
|
...rule.supportsPartial !== void 0 ? { supportsPartial: rule.supportsPartial } : {},
|
|
280
|
-
...rule.kind === "judge" ?
|
|
463
|
+
...rule.kind === "judge" ? catalogJudge(rule, at, defaultJudge) : rule.criteria ? { answer: {
|
|
464
|
+
kind: "criteria",
|
|
465
|
+
criteria: rule.criteria
|
|
466
|
+
} } : {},
|
|
281
467
|
...definedAt ? { definedAt } : {}
|
|
282
468
|
};
|
|
283
469
|
}
|
|
284
|
-
function
|
|
470
|
+
function catalogJudge(rule, at, defaultJudge) {
|
|
471
|
+
const agent = rule.agent ?? defaultJudge;
|
|
472
|
+
const inputs = judgeInputNames(rule);
|
|
473
|
+
return {
|
|
474
|
+
rubric: rule.rubric,
|
|
475
|
+
...agent ? { agent: catalogAgent(agent, at) } : {},
|
|
476
|
+
inputs,
|
|
477
|
+
answer: rule.answer,
|
|
478
|
+
prompt: judgePrompt({
|
|
479
|
+
rubric: rule.rubric,
|
|
480
|
+
answer: rule.answer,
|
|
481
|
+
inputs
|
|
482
|
+
})
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
function catalogStep(step, at, defaultJudge) {
|
|
285
486
|
switch (step.kind) {
|
|
286
487
|
case "user": return typeof step.message === "string" ? {
|
|
287
488
|
kind: "user",
|
|
@@ -298,7 +499,7 @@ function catalogStep(step, at) {
|
|
|
298
499
|
};
|
|
299
500
|
default: return {
|
|
300
501
|
kind: "checkpoint",
|
|
301
|
-
scorer: catalogScorer(step, at)
|
|
502
|
+
scorer: catalogScorer(step, at, defaultJudge)
|
|
302
503
|
};
|
|
303
504
|
}
|
|
304
505
|
}
|
|
@@ -317,6 +518,10 @@ function shapeOf(value) {
|
|
|
317
518
|
}
|
|
318
519
|
//#endregion
|
|
319
520
|
//#region ../core/src/config.ts
|
|
521
|
+
const DEFAULT_ATTACHMENT_LIMITS = {
|
|
522
|
+
maxFileBytes: 5e6,
|
|
523
|
+
maxRunBytes: 2e7
|
|
524
|
+
};
|
|
320
525
|
/** Left out of candidate snapshots unless `artifacts.exclude` says otherwise. */
|
|
321
526
|
const DEFAULT_ARTIFACT_EXCLUDE = [
|
|
322
527
|
".git",
|
|
@@ -332,6 +537,18 @@ function defineConfig(configuration) {
|
|
|
332
537
|
return configuration;
|
|
333
538
|
}
|
|
334
539
|
//#endregion
|
|
540
|
+
//#region ../core/src/usage.ts
|
|
541
|
+
/**
|
|
542
|
+
* The tokens a turn counts toward the `tokens` metric: `inputTokens +
|
|
543
|
+
* outputTokens` (cached input included), or `totalTokens` when neither is
|
|
544
|
+
* reported. Undefined when the turn reported no usage.
|
|
545
|
+
*/
|
|
546
|
+
function turnTokens(usage) {
|
|
547
|
+
if (!usage) return void 0;
|
|
548
|
+
if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0) return (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
549
|
+
return usage.totalTokens;
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
335
552
|
//#region ../core/src/canonical-json.ts
|
|
336
553
|
/** Canonical JSON: keys sorted recursively, array order preserved. Rejects non-JSON inputs. */
|
|
337
554
|
function canonicalParameters(value) {
|
|
@@ -546,17 +763,20 @@ function turnResultEvents(result, turn, now) {
|
|
|
546
763
|
role: "assistant",
|
|
547
764
|
content: result.reply
|
|
548
765
|
}, now));
|
|
549
|
-
if (result.usage) events.push(normalizeAutEvent({
|
|
766
|
+
if (result.usage || result.model) events.push(normalizeAutEvent({
|
|
550
767
|
kind: "turn-completed",
|
|
551
768
|
turn,
|
|
552
|
-
usage: result.usage
|
|
769
|
+
...result.usage ? { usage: result.usage } : {},
|
|
770
|
+
...result.model ? { model: result.model } : {}
|
|
553
771
|
}, now));
|
|
554
772
|
return events;
|
|
555
773
|
}
|
|
556
774
|
//#endregion
|
|
557
775
|
//#region ../core/src/schema.ts
|
|
558
776
|
/** Runtime schemas for serialized EvalKit report and API boundaries. */
|
|
559
|
-
const RunStatusSchema = Schema.Literal("running", "
|
|
777
|
+
const RunStatusSchema = Schema.Literal("running", "passed", "failed", "errored", "cancelled");
|
|
778
|
+
/** The report schema version this EvalKit writes and reads. */
|
|
779
|
+
const REPORT_SCHEMA_VERSION = 6;
|
|
560
780
|
const RecordedErrorSchema = Schema.Struct({
|
|
561
781
|
name: Schema.String,
|
|
562
782
|
message: Schema.String,
|
|
@@ -594,8 +814,19 @@ const RunSourceSchema = Schema.Struct({
|
|
|
594
814
|
url: Schema.optional(Schema.String)
|
|
595
815
|
}))
|
|
596
816
|
});
|
|
817
|
+
const BatchSchema = Schema.Struct({
|
|
818
|
+
id: Schema.String,
|
|
819
|
+
index: Schema.Number,
|
|
820
|
+
size: Schema.Number
|
|
821
|
+
});
|
|
822
|
+
const RunDatasetSchema = Schema.Struct({
|
|
823
|
+
id: Schema.String,
|
|
824
|
+
visibility: Schema.Literal("candidate", "evaluator"),
|
|
825
|
+
source: Schema.Literal("static", "computed"),
|
|
826
|
+
digest: Schema.String
|
|
827
|
+
});
|
|
597
828
|
const RunMetadataSchema = Schema.Struct({
|
|
598
|
-
schemaVersion: Schema.Literal(
|
|
829
|
+
schemaVersion: Schema.Literal(6),
|
|
599
830
|
runUri: resourceUriSchema("run"),
|
|
600
831
|
evalId: Schema.String,
|
|
601
832
|
suiteId: Schema.optional(Schema.String),
|
|
@@ -607,39 +838,21 @@ const RunMetadataSchema = Schema.Struct({
|
|
|
607
838
|
process: Schema.optional(Schema.Struct({
|
|
608
839
|
pid: Schema.Number,
|
|
609
840
|
hostname: Schema.String
|
|
610
|
-
}))
|
|
611
|
-
|
|
612
|
-
const TrialMetadataSchema = Schema.Struct({
|
|
613
|
-
schemaVersion: Schema.Literal(2, 3, 4, 5),
|
|
614
|
-
runUri: resourceUriSchema("run"),
|
|
615
|
-
trialUri: resourceUriSchema("trial"),
|
|
616
|
-
trialIndex: Schema.Number,
|
|
617
|
-
evalId: Schema.String,
|
|
618
|
-
parameters: Schema.optional(ParametersSchema),
|
|
619
|
-
matrix: Schema.optional(MatrixCellSchema),
|
|
620
|
-
aut: Schema.optional(AutIdentitySchema),
|
|
621
|
-
startedAt: Schema.String,
|
|
841
|
+
})),
|
|
842
|
+
batch: Schema.optional(BatchSchema),
|
|
622
843
|
/** The effective snapshot ignore patterns: the project's, then the scenario's. */
|
|
623
844
|
workspaceIgnore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
|
624
|
-
datasets: Schema.optional(Schema.mutable(Schema.Array(
|
|
625
|
-
id: Schema.String,
|
|
626
|
-
visibility: Schema.Literal("candidate", "evaluator"),
|
|
627
|
-
source: Schema.Literal("static", "computed"),
|
|
628
|
-
digest: Schema.String
|
|
629
|
-
}))))
|
|
845
|
+
datasets: Schema.optional(Schema.mutable(Schema.Array(RunDatasetSchema)))
|
|
630
846
|
});
|
|
631
|
-
/** On-disk manifests include a lifecycle status
|
|
847
|
+
/** On-disk manifests include a lifecycle status. */
|
|
632
848
|
const RunManifestSchema = Schema.Struct({
|
|
633
849
|
...RunMetadataSchema.fields,
|
|
634
850
|
status: Schema.optional(RunStatusSchema)
|
|
635
851
|
});
|
|
636
|
-
const TrialManifestSchema = Schema.Struct({
|
|
637
|
-
...TrialMetadataSchema.fields,
|
|
638
|
-
status: Schema.optional(RunStatusSchema)
|
|
639
|
-
});
|
|
640
852
|
const UsageSchema = Schema.Struct({
|
|
641
853
|
inputTokens: Schema.optional(Schema.Number),
|
|
642
854
|
outputTokens: Schema.optional(Schema.Number),
|
|
855
|
+
cachedInputTokens: Schema.optional(Schema.Number),
|
|
643
856
|
totalTokens: Schema.optional(Schema.Number)
|
|
644
857
|
});
|
|
645
858
|
const JudgeRunInfoSchema = Schema.Struct({
|
|
@@ -649,9 +862,32 @@ const JudgeRunInfoSchema = Schema.Struct({
|
|
|
649
862
|
name: Schema.optional(Schema.String),
|
|
650
863
|
version: Schema.optional(Schema.String)
|
|
651
864
|
})),
|
|
865
|
+
model: Schema.optional(Schema.String),
|
|
652
866
|
usage: Schema.optional(UsageSchema),
|
|
867
|
+
prompt: Schema.optional(Schema.String),
|
|
868
|
+
reply: Schema.optional(JsonValueSchema),
|
|
869
|
+
attempts: Schema.optional(Schema.Number),
|
|
653
870
|
events: Schema.optional(Schema.mutable(Schema.Array(JsonValueSchema)))
|
|
654
871
|
});
|
|
872
|
+
const CriterionResultSchema = Schema.Union(Schema.Struct({
|
|
873
|
+
name: Schema.String,
|
|
874
|
+
value: Schema.Number,
|
|
875
|
+
weight: Schema.optional(Schema.Number),
|
|
876
|
+
explanation: Schema.optional(Schema.String),
|
|
877
|
+
evidence: Schema.optional(JsonValueSchema)
|
|
878
|
+
}), Schema.Struct({
|
|
879
|
+
name: Schema.String,
|
|
880
|
+
reached: Schema.Literal(false),
|
|
881
|
+
weight: Schema.optional(Schema.Number),
|
|
882
|
+
explanation: Schema.optional(Schema.String)
|
|
883
|
+
}));
|
|
884
|
+
/** What every score and checkpoint result can carry about its verdict. */
|
|
885
|
+
const verdictFields = {
|
|
886
|
+
passAt: Schema.optional(Schema.Number),
|
|
887
|
+
choice: Schema.optional(Schema.String),
|
|
888
|
+
criteria: Schema.optional(Schema.mutable(Schema.Array(CriterionResultSchema))),
|
|
889
|
+
attachments: Schema.optional(Schema.mutable(Schema.Array(Schema.String)))
|
|
890
|
+
};
|
|
655
891
|
const ScoreResultSchema = Schema.Struct({
|
|
656
892
|
name: Schema.String,
|
|
657
893
|
kind: Schema.Literal("predicate", "judge"),
|
|
@@ -659,6 +895,7 @@ const ScoreResultSchema = Schema.Struct({
|
|
|
659
895
|
passed: Schema.optional(Schema.Boolean),
|
|
660
896
|
explanation: Schema.optional(Schema.String),
|
|
661
897
|
evidence: Schema.optional(JsonValueSchema),
|
|
898
|
+
...verdictFields,
|
|
662
899
|
judge: Schema.optional(JudgeRunInfoSchema),
|
|
663
900
|
durationMs: Schema.Number,
|
|
664
901
|
error: Schema.optional(RecordedErrorSchema),
|
|
@@ -673,6 +910,7 @@ const CheckpointResultSchema = Schema.Struct({
|
|
|
673
910
|
passed: Schema.optional(Schema.Boolean),
|
|
674
911
|
explanation: Schema.optional(Schema.String),
|
|
675
912
|
evidence: Schema.optional(JsonValueSchema),
|
|
913
|
+
...verdictFields,
|
|
676
914
|
judge: Schema.optional(JudgeRunInfoSchema),
|
|
677
915
|
durationMs: Schema.optional(Schema.Number),
|
|
678
916
|
error: Schema.optional(RecordedErrorSchema),
|
|
@@ -689,7 +927,7 @@ const MetricResultSchema = Schema.Struct({
|
|
|
689
927
|
builtIn: Schema.optional(Schema.Literal(true)),
|
|
690
928
|
error: Schema.optional(RecordedErrorSchema)
|
|
691
929
|
});
|
|
692
|
-
const
|
|
930
|
+
const RunScoringSchema = Schema.Struct({
|
|
693
931
|
results: Schema.mutable(Schema.Array(ScoreResultSchema)),
|
|
694
932
|
metrics: Schema.optional(Schema.mutable(Schema.Array(MetricResultSchema))),
|
|
695
933
|
checkpoints: Schema.optional(Schema.mutable(Schema.Array(CheckpointResultSchema))),
|
|
@@ -702,22 +940,18 @@ const ArtifactEntrySchema = Schema.Struct({
|
|
|
702
940
|
kind: Schema.Literal("file", "directory"),
|
|
703
941
|
size: Schema.optional(Schema.Number)
|
|
704
942
|
});
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
scoring: Schema.optional(TrialScoringSchema),
|
|
710
|
-
artifacts: Schema.optional(Schema.mutable(Schema.Array(ArtifactEntrySchema))),
|
|
711
|
-
error: Schema.optional(RecordedErrorSchema)
|
|
943
|
+
const AttachmentEntrySchema = Schema.Struct({
|
|
944
|
+
name: Schema.String,
|
|
945
|
+
size: Schema.Number,
|
|
946
|
+
truncatedFrom: Schema.optional(Schema.Number)
|
|
712
947
|
});
|
|
713
948
|
const RunSummarySchema = Schema.Struct({
|
|
714
949
|
status: RunStatusSchema,
|
|
715
950
|
endedAt: Schema.String,
|
|
716
951
|
durationMs: Schema.optional(Schema.Number),
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
cancelled: Schema.optional(Schema.Number),
|
|
952
|
+
scoring: Schema.optional(RunScoringSchema),
|
|
953
|
+
artifacts: Schema.optional(Schema.mutable(Schema.Array(ArtifactEntrySchema))),
|
|
954
|
+
attachments: Schema.optional(Schema.mutable(Schema.Array(AttachmentEntrySchema))),
|
|
721
955
|
error: Schema.optional(RecordedErrorSchema)
|
|
722
956
|
});
|
|
723
957
|
const autEvent = {
|
|
@@ -761,7 +995,8 @@ const autEvent = {
|
|
|
761
995
|
turn: Schema.Number,
|
|
762
996
|
timestamp: Schema.String,
|
|
763
997
|
latencyMs: Schema.optional(Schema.Number),
|
|
764
|
-
usage: Schema.optional(UsageSchema)
|
|
998
|
+
usage: Schema.optional(UsageSchema),
|
|
999
|
+
model: Schema.optional(Schema.String)
|
|
765
1000
|
}),
|
|
766
1001
|
completed: Schema.Struct({
|
|
767
1002
|
source: Schema.Literal("aut"),
|
|
@@ -777,9 +1012,9 @@ const autEvent = {
|
|
|
777
1012
|
})
|
|
778
1013
|
};
|
|
779
1014
|
const runnerEvent = {
|
|
780
|
-
|
|
1015
|
+
runStarted: Schema.Struct({
|
|
781
1016
|
source: Schema.Literal("runner"),
|
|
782
|
-
kind: Schema.Literal("
|
|
1017
|
+
kind: Schema.Literal("run-started"),
|
|
783
1018
|
timestamp: Schema.String
|
|
784
1019
|
}),
|
|
785
1020
|
stepStarted: Schema.Struct({
|
|
@@ -843,9 +1078,9 @@ const runnerEvent = {
|
|
|
843
1078
|
error: RecordedErrorSchema,
|
|
844
1079
|
timestamp: Schema.String
|
|
845
1080
|
}),
|
|
846
|
-
|
|
1081
|
+
runCompleted: Schema.Struct({
|
|
847
1082
|
source: Schema.Literal("runner"),
|
|
848
|
-
kind: Schema.Literal("
|
|
1083
|
+
kind: Schema.Literal("run-completed"),
|
|
849
1084
|
timestamp: Schema.String
|
|
850
1085
|
}),
|
|
851
1086
|
error: Schema.Struct({
|
|
@@ -902,7 +1137,7 @@ function expectToolCall(expected) {
|
|
|
902
1137
|
};
|
|
903
1138
|
}
|
|
904
1139
|
/**
|
|
905
|
-
* A user message. Pass a function to compute it for each
|
|
1140
|
+
* A user message. Pass a function to compute it for each run, e.g. from a
|
|
906
1141
|
* matrix axis: `user(({ parameters }) => `Set up OCaml with ${parameters.toolchain}.`)`.
|
|
907
1142
|
*/
|
|
908
1143
|
function user(message) {
|
|
@@ -918,8 +1153,8 @@ function agent(expectation) {
|
|
|
918
1153
|
};
|
|
919
1154
|
}
|
|
920
1155
|
/**
|
|
921
|
-
* Marks a scorer as not applicable to this
|
|
922
|
-
* passes nor fails the
|
|
1156
|
+
* Marks a scorer as not applicable to this run. A skipped result neither
|
|
1157
|
+
* passes nor fails the run and is left out of score averages.
|
|
923
1158
|
*/
|
|
924
1159
|
function skip(reason) {
|
|
925
1160
|
if (!reason.trim()) throw new Error("skip() needs a reason");
|
|
@@ -928,16 +1163,20 @@ function skip(reason) {
|
|
|
928
1163
|
reason
|
|
929
1164
|
};
|
|
930
1165
|
}
|
|
1166
|
+
/** A scorer in code. Its `evidence` type carries through to `checkpoint(rule)`. */
|
|
931
1167
|
function predicate(name, run, options = {}) {
|
|
932
1168
|
if (!name.trim()) throw new Error("Predicate name must not be empty");
|
|
933
|
-
|
|
1169
|
+
assertPassAt(`Predicate ${name}`, options.passAt);
|
|
1170
|
+
const { criteria: declared, ...rest } = options;
|
|
934
1171
|
return markDefinitionSite({
|
|
935
1172
|
kind: "predicate",
|
|
936
1173
|
name,
|
|
937
1174
|
run,
|
|
938
|
-
...
|
|
1175
|
+
...rest,
|
|
1176
|
+
...declared ? { criteria: declaredCriteria(Object.fromEntries(Object.entries(declared).map(([key, option]) => [key, typeof option === "string" ? { description: option } : option]))) } : {}
|
|
939
1177
|
});
|
|
940
1178
|
}
|
|
1179
|
+
/** A built-in metric's name is a type error here, as well as at definition time. */
|
|
941
1180
|
function metric(name, run, options = {}) {
|
|
942
1181
|
if (!name.trim()) throw new Error("Metric name must not be empty");
|
|
943
1182
|
return markDefinitionSite({
|
|
@@ -947,17 +1186,26 @@ function metric(name, run, options = {}) {
|
|
|
947
1186
|
...options
|
|
948
1187
|
});
|
|
949
1188
|
}
|
|
1189
|
+
const INPUT_NAME = /^[a-z][a-zA-Z0-9_-]{0,31}$/;
|
|
950
1190
|
function judge(name, options) {
|
|
951
|
-
if (!name.trim() || !options.rubric
|
|
1191
|
+
if (!name.trim() || !options.rubric?.trim()) throw new Error("Judge name and rubric must not be empty");
|
|
1192
|
+
assertPassAt(`Judge ${name}`, options.passAt);
|
|
1193
|
+
for (const input of Object.keys(options.inputs ?? {})) if (!INPUT_NAME.test(input)) throw new Error(`Judge ${name} input ${JSON.stringify(input)} must start with a lowercase letter and use letters, digits, "-" or "_"`);
|
|
1194
|
+
if (options.inputs && !Object.keys(options.inputs).length) throw new Error(`Judge ${name} inputs must name at least one input`);
|
|
1195
|
+
if (options.agent && typeof options.agent.start !== "function") throw new Error(`Judge ${name} agent must be made with defineAgent()`);
|
|
952
1196
|
return markDefinitionSite({
|
|
953
1197
|
kind: "judge",
|
|
954
1198
|
name,
|
|
955
|
-
...options
|
|
1199
|
+
...options,
|
|
1200
|
+
answer: options.answer ?? { kind: "verdict" }
|
|
956
1201
|
});
|
|
957
1202
|
}
|
|
1203
|
+
function assertPassAt(owner, passAt) {
|
|
1204
|
+
if (passAt !== void 0 && !(Number.isFinite(passAt) && passAt >= 0 && passAt <= 1)) throw new Error(`${owner} passAt must be a number from 0 to 1; received ${passAt}`);
|
|
1205
|
+
}
|
|
958
1206
|
function defineEval(definition) {
|
|
959
1207
|
authoringId(definition);
|
|
960
|
-
|
|
1208
|
+
assertNoRemovedOptions(definition);
|
|
961
1209
|
if (definition.metadata !== void 0) toJsonObject(definition.metadata, `${definition.id}.metadata`);
|
|
962
1210
|
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)}`);
|
|
963
1211
|
const datasetIds = /* @__PURE__ */ new Set();
|
|
@@ -975,10 +1223,13 @@ function defineEval(definition) {
|
|
|
975
1223
|
let hasUser = false;
|
|
976
1224
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
977
1225
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
978
|
-
|
|
979
|
-
if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
|
|
1226
|
+
assertJudgesIndependent(definition);
|
|
980
1227
|
return markDefinitionSite(definition);
|
|
981
1228
|
}
|
|
1229
|
+
/** A judge never grades with the agent it is judging. */
|
|
1230
|
+
function assertJudgesIndependent(definition, defaultJudge) {
|
|
1231
|
+
for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && (rule.agent ?? defaultJudge) === definition.agent) throw new Error(`Judge ${rule.name} in ${definition.id} uses the agent under test; a judge needs its own agent`);
|
|
1232
|
+
}
|
|
982
1233
|
function defineSuite(suite) {
|
|
983
1234
|
authoringId(suite);
|
|
984
1235
|
return suite;
|
|
@@ -1048,11 +1299,21 @@ function registerEvals(registrations) {
|
|
|
1048
1299
|
catalog: (options) => evals.map((evaluation) => catalogEntry(evaluation, suiteIdByEvalId.get(authoringId(evaluation)), options))
|
|
1049
1300
|
};
|
|
1050
1301
|
}
|
|
1051
|
-
/**
|
|
1302
|
+
/**
|
|
1303
|
+
* Metrics EvalKit records for every run; scenario metrics may not reuse
|
|
1304
|
+
* these names.
|
|
1305
|
+
*
|
|
1306
|
+
* - `agent time` (ms): from the run's first event to its last one before
|
|
1307
|
+
* final scoring.
|
|
1308
|
+
* - `tokens`: the sum of `turnTokens(usage)` over the turns that reported
|
|
1309
|
+
* usage, cached input included. Absent when no turn reported any.
|
|
1310
|
+
* - `tool calls`: the agent's `tool-call` events.
|
|
1311
|
+
*/
|
|
1052
1312
|
const BUILT_IN_METRICS = [
|
|
1053
1313
|
"agent time",
|
|
1054
1314
|
"tokens",
|
|
1055
|
-
"tool calls"
|
|
1315
|
+
"tool calls",
|
|
1316
|
+
"judge tokens"
|
|
1056
1317
|
];
|
|
1057
1318
|
function recordError(error) {
|
|
1058
1319
|
if (error instanceof Error) return {
|
|
@@ -1066,4 +1327,4 @@ function recordError(error) {
|
|
|
1066
1327
|
};
|
|
1067
1328
|
}
|
|
1068
1329
|
//#endregion
|
|
1069
|
-
export {
|
|
1330
|
+
export { judgeInputNames as $, RunDatasetSchema as A, normalizeAutEvent as B, BatchSchema as C, MetricResultSchema as D, JudgeRunInfoSchema as E, RunStatusSchema as F, DEFAULT_ARTIFACT_EXCLUDE as G, defineEvalMatrix as H, RunSummarySchema as I, catalogEntry as J, DEFAULT_ATTACHMENT_LIMITS as K, ScoreResultSchema as L, RunMetadataSchema as M, RunScoringSchema as N, REPORT_SCHEMA_VERSION as O, RunSourceSchema as P, criteria as Q, TrajectoryEventSchema as R, AutIdentitySchema as S, JsonValueSchema as T, canonicalParameters as U, turnResultEvents as V, turnTokens as W, answerInstruction as X, UnreadableAnswer as Y, choices as Z, user as _, resourceUriSchema as _t, assertJudgesIndependent as a, toJson as at, ArtifactEntrySchema as b, defineEval as c, isDataset as ct, judge as d, ResourceKindSchema as dt, judgePrompt as et, metric as f, ResourceUriSchema as ft, skip as g, resourceUri as gt, registerEvals as h, parseResourceUri as ht, agentRuntimes as i, NotJsonError as it, RunManifestSchema as j, RecordedErrorSchema as k, defineSuite as l, relativeDatasetPath as lt, recordError as m, authorId as mt, DEFAULT_AGENT_RUNTIMES as n, readJudgeAnswer as nt, authoringId as o, toJsonObject as ot, predicate as p, UuidSchema as pt, defineConfig as q, agent as r, weightedMean as rt, defineAgent as s, defineDataset as st, BUILT_IN_METRICS as t, passMark as tt, expectToolCall as u, definitionSite as ut, parseTrajectoryJsonl as v, resourceUuid as vt, CheckpointResultSchema as w, AttachmentEntrySchema as x, ApiErrorSchema as y, UsageSchema as z };
|