@leostera/evalkit 0.9.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 +48 -1
- package/README.md +3 -2
- package/dist/cli.mjs +139 -29
- package/dist/dashboard/assets/{index-By1SJ1on.js → index-DNR4shrM.js} +32 -31
- package/dist/dashboard/assets/index-TPYprg75.css +2 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/{index-db-BbVblaHz.mjs → index-db-B1FeXzmS.mjs} +1 -1
- package/dist/index.d.mts +313 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/runner.d.mts +222 -6
- package/dist/runner.d.mts.map +1 -1
- package/dist/runner.mjs +1 -1
- package/dist/share-worker.mjs +1 -1
- package/dist/share.mjs +2 -2
- package/dist/skills/evalkit/SKILL.md +2 -2
- package/dist/skills/evalkit-evals/SKILL.md +1 -1
- package/dist/skills/evalkit-judges/SKILL.md +3 -3
- package/dist/skills/evalkit-results/SKILL.md +1 -1
- package/dist/{src-BrfIXfO2.mjs → src-B14fXAGp.mjs} +264 -21
- package/dist/{src-Bn6fnZnF.mjs → src-Bccv9igQ.mjs} +1 -1
- package/dist/{src-CcdLZqFk.mjs → src-BfSQKOJP.mjs} +260 -97
- package/dist/{upgrade-60SIRius.mjs → upgrade-G-uFPJKg.mjs} +1 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-geXMXCx9.css +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-
|
|
1
|
+
import { $ as judgeInputNames, B as normalizeAutEvent, G as DEFAULT_ARTIFACT_EXCLUDE, I as RunSummarySchema, K as DEFAULT_ATTACHMENT_LIMITS, L as ScoreResultSchema, M as RunMetadataSchema, N as RunScoringSchema, R as TrajectoryEventSchema, T as JsonValueSchema, U as canonicalParameters, V as turnResultEvents, W as turnTokens, X as answerInstruction, Y as UnreadableAnswer, a as assertJudgesIndependent, at as toJson, et as judgePrompt, gt as resourceUri, i as agentRuntimes, j as RunManifestSchema, lt as relativeDatasetPath, m as recordError, nt as readJudgeAnswer, o as authoringId, ot as toJsonObject, rt as weightedMean, v as parseTrajectoryJsonl, w as CheckpointResultSchema } from "./src-B14fXAGp.mjs";
|
|
2
|
+
import { a as upgradeManifest, n as assertNoRemovedOptions, o as upgradeStatus, r as upgradeCatalog, s as upgradeTrajectory, t as UPGRADABLE_SCHEMA_VERSIONS } from "./upgrade-G-uFPJKg.mjs";
|
|
3
3
|
import * as Schema from "effect/Schema";
|
|
4
4
|
import { Data, Effect, Either, Exit, Scope } from "effect";
|
|
5
5
|
import os from "node:os";
|
|
@@ -105,6 +105,22 @@ function attachmentStore(writer, limits = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* An `attach` that remembers what it saved, so a score or checkpoint can
|
|
110
|
+
* link the files its scorer attached, even when the scorer then failed.
|
|
111
|
+
*/
|
|
112
|
+
function trackAttachments(attach) {
|
|
113
|
+
const attached = [];
|
|
114
|
+
return {
|
|
115
|
+
attached,
|
|
116
|
+
async attach(name, content) {
|
|
117
|
+
await attach(name, content);
|
|
118
|
+
attached.push(name);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** `{ attachments }` when a scorer attached any. */
|
|
123
|
+
const attachedFields = (attached) => attached.length ? { attachments: [...attached] } : {};
|
|
108
124
|
//#endregion
|
|
109
125
|
//#region ../core/src/glob.ts
|
|
110
126
|
/**
|
|
@@ -483,28 +499,85 @@ function checkpointLookup(transcript, results) {
|
|
|
483
499
|
}
|
|
484
500
|
//#endregion
|
|
485
501
|
//#region ../runner/src/score.ts
|
|
502
|
+
const inUnitInterval = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
503
|
+
/**
|
|
504
|
+
* Criteria must be well formed before they can stand for the score. With
|
|
505
|
+
* declared criteria, each takes its declared weight unless it sets one, a
|
|
506
|
+
* declared one left out is not reached, and an undeclared one is an error,
|
|
507
|
+
* so the checks shown before a run are the checks recorded after it.
|
|
508
|
+
*/
|
|
509
|
+
function checkedCriteria(criteria, declared) {
|
|
510
|
+
if (!Array.isArray(criteria) || !criteria.length && !declared?.length) throw new Error("criteria must be a non-empty array");
|
|
511
|
+
const seen = /* @__PURE__ */ new Set();
|
|
512
|
+
const results = criteria.map((entry, index) => {
|
|
513
|
+
if (typeof entry?.name !== "string" || !entry.name.trim()) throw new Error(`criteria[${index}] needs a name`);
|
|
514
|
+
const { name } = entry;
|
|
515
|
+
if (seen.has(name)) throw new Error(`Criterion ${name} is listed twice`);
|
|
516
|
+
seen.add(name);
|
|
517
|
+
const declaration = declared?.find((criterion) => criterion.name === name);
|
|
518
|
+
if (declared && !declaration) throw new Error(`Criterion ${name} is not declared; declared: ${declared.map((criterion) => criterion.name).join(", ")}`);
|
|
519
|
+
const weight = entry.weight ?? declaration?.weight;
|
|
520
|
+
if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) throw new Error(`Criterion ${name} weight must be a positive number; received ${String(weight)}`);
|
|
521
|
+
const explanation = entry.explanation;
|
|
522
|
+
if (explanation !== void 0 && typeof explanation !== "string") throw new Error(`Criterion ${name} explanation must be a string`);
|
|
523
|
+
const common = {
|
|
524
|
+
name,
|
|
525
|
+
...weight !== void 0 ? { weight } : {},
|
|
526
|
+
...explanation?.trim() ? { explanation: explanation.trim() } : {}
|
|
527
|
+
};
|
|
528
|
+
if ("reached" in entry) {
|
|
529
|
+
if (entry.reached !== false) throw new Error(`Criterion ${name}: reached can only be false; leave it out for a check that ran`);
|
|
530
|
+
return {
|
|
531
|
+
...common,
|
|
532
|
+
reached: false
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const value = entry.value;
|
|
536
|
+
if (!inUnitInterval(value)) throw new Error(`Criterion ${name} value must be a number from 0 to 1; received ${String(value)}`);
|
|
537
|
+
const evidence = entry.evidence;
|
|
538
|
+
return {
|
|
539
|
+
...common,
|
|
540
|
+
value,
|
|
541
|
+
...evidence !== void 0 ? { evidence: toJson(evidence, `criteria.${name}.evidence`) } : {}
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
const missing = (declared ?? []).filter((criterion) => !seen.has(criterion.name)).map((criterion) => ({
|
|
545
|
+
name: criterion.name,
|
|
546
|
+
weight: criterion.weight,
|
|
547
|
+
reached: false
|
|
548
|
+
}));
|
|
549
|
+
return [...results, ...missing];
|
|
550
|
+
}
|
|
486
551
|
/**
|
|
487
552
|
* Shared normalization for inline and final predicate and judge rules.
|
|
488
|
-
*
|
|
553
|
+
* Without a `value`, the weighted mean of `criteria` is the value. `passAt`
|
|
554
|
+
* turns the value into a verdict, and is recorded with it; an explicit
|
|
555
|
+
* `passed` wins.
|
|
489
556
|
*/
|
|
490
|
-
function normalizeScore(value, passAt) {
|
|
557
|
+
function normalizeScore(value, passAt, declared) {
|
|
491
558
|
if (typeof value === "object" && value !== null && "skipped" in value && value.skipped === true) return {
|
|
492
559
|
skipped: true,
|
|
493
560
|
explanation: value.reason
|
|
494
561
|
};
|
|
495
562
|
const normalized = typeof value === "boolean" ? { value: Number(value) } : typeof value === "number" ? { value } : value;
|
|
496
|
-
if (!normalized ||
|
|
497
|
-
|
|
563
|
+
if (!normalized || typeof normalized !== "object") throw new Error(`A scorer must return a boolean, a number from 0 to 1, or a score object; received ${String(value)}`);
|
|
564
|
+
const criteria = normalized.criteria === void 0 ? void 0 : checkedCriteria(normalized.criteria, declared);
|
|
565
|
+
const score = normalized.value ?? (criteria ? weightedMean(criteria) : void 0);
|
|
566
|
+
if (!inUnitInterval(score)) throw new Error(`Score value must be a finite number from 0 to 1; received ${String(score)}`);
|
|
567
|
+
if (normalized.judge?.usage) {
|
|
498
568
|
const { usage } = normalized.judge;
|
|
499
|
-
if (
|
|
569
|
+
if (Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
|
|
500
570
|
}
|
|
501
|
-
const
|
|
571
|
+
const mark = passAt ?? 1;
|
|
502
572
|
return {
|
|
503
|
-
value:
|
|
504
|
-
passed,
|
|
505
|
-
|
|
506
|
-
...
|
|
507
|
-
...
|
|
573
|
+
value: score,
|
|
574
|
+
passed: normalized.passed ?? score >= mark,
|
|
575
|
+
passAt: mark,
|
|
576
|
+
...normalized.explanation ? { explanation: normalized.explanation } : {},
|
|
577
|
+
...normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
|
|
578
|
+
...normalized.choice ? { choice: normalized.choice } : {},
|
|
579
|
+
...criteria ? { criteria } : {},
|
|
580
|
+
...normalized.judge !== void 0 ? { judge: normalized.judge } : {}
|
|
508
581
|
};
|
|
509
582
|
}
|
|
510
583
|
//#endregion
|
|
@@ -514,8 +587,28 @@ const failure$2 = (name, cause) => new RuleExecutionError({
|
|
|
514
587
|
cause,
|
|
515
588
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
516
589
|
});
|
|
517
|
-
/**
|
|
590
|
+
/** A long tool result or reply is cut to this many characters in a judge's conversation. */
|
|
591
|
+
const MAX_PART = 4e3;
|
|
592
|
+
/**
|
|
593
|
+
* Drives one judge agent session for one assessment: builds the prompt from
|
|
594
|
+
* the rule, sends it, reads the answer in the rule's format, and asks once
|
|
595
|
+
* more if it cannot. Its events stay out of the AUT trajectory.
|
|
596
|
+
*/
|
|
518
597
|
function runJudgeAgent(rule, agent, input, placement) {
|
|
598
|
+
return Effect.tryPromise({
|
|
599
|
+
try: () => judgeOnce(rule, agent, input, placement),
|
|
600
|
+
catch: (cause) => cause instanceof RuleExecutionError ? cause : failure$2(rule.name, cause)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
async function judgeOnce(rule, agent, input, placement) {
|
|
604
|
+
const inputs = judgeInputNames(rule);
|
|
605
|
+
const values = await inputValues(rule, input, placement);
|
|
606
|
+
const prompt = judgePrompt({
|
|
607
|
+
rubric: rule.rubric,
|
|
608
|
+
answer: rule.answer,
|
|
609
|
+
inputs,
|
|
610
|
+
values
|
|
611
|
+
});
|
|
519
612
|
const events = [];
|
|
520
613
|
const context = {
|
|
521
614
|
...input.context,
|
|
@@ -523,64 +616,117 @@ function runJudgeAgent(rule, agent, input, placement) {
|
|
|
523
616
|
workspace: input.artifacts.evaluator,
|
|
524
617
|
evaluatorWorkspace: input.artifacts.evaluator
|
|
525
618
|
};
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
619
|
+
const session = await agent.start({
|
|
620
|
+
context,
|
|
621
|
+
attach: input.attach,
|
|
622
|
+
onEvent: (event) => {
|
|
623
|
+
const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
|
|
624
|
+
...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
|
|
625
|
+
source: "aut"
|
|
626
|
+
});
|
|
627
|
+
if (observed.source !== "aut") throw new Error("Invalid judge event source");
|
|
628
|
+
const { source: _, ...validated } = observed;
|
|
629
|
+
events.push(validated);
|
|
630
|
+
}
|
|
532
631
|
});
|
|
533
|
-
let
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
try: async () => {
|
|
552
|
-
const result = await session.send(prompt);
|
|
553
|
-
if (result !== void 0) events.push(...turnResultEvents(result, 1, () => /* @__PURE__ */ new Date()));
|
|
554
|
-
},
|
|
555
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
556
|
-
}), (session) => Effect.tryPromise({
|
|
557
|
-
try: async () => session.close?.(),
|
|
558
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
559
|
-
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
560
|
-
closeError = error;
|
|
561
|
-
}))));
|
|
562
|
-
if (closeError) return yield* Effect.fail(closeError);
|
|
563
|
-
const completed = events.slice().reverse().find((event) => event.kind === "completed" && event.output !== void 0);
|
|
564
|
-
const assistant = events.slice().reverse().find((event) => event.kind === "message" && event.role === "assistant");
|
|
565
|
-
const output = completed?.kind === "completed" ? completed.output : assistant?.kind === "message" ? assistant.content : void 0;
|
|
566
|
-
if (output === void 0) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge agent did not emit a verdict")));
|
|
567
|
-
const value = yield* Effect.try({
|
|
568
|
-
try: () => typeof output === "string" ? JSON.parse(output) : output,
|
|
569
|
-
catch: (cause) => failure$2(rule.name, cause)
|
|
570
|
-
});
|
|
571
|
-
const encodedEvents = yield* Schema.decodeUnknown(JsonValueSchema)(events).pipe(Effect.mapError((cause) => failure$2(rule.name, cause)));
|
|
572
|
-
if (!Array.isArray(encodedEvents)) return yield* Effect.fail(failure$2(rule.name, /* @__PURE__ */ new Error("Judge events must be an array")));
|
|
573
|
-
const usage = events.slice().reverse().find((event) => event.kind === "turn-completed" && event.usage);
|
|
574
|
-
return {
|
|
575
|
-
value,
|
|
576
|
-
judge: {
|
|
577
|
-
...agent.identity ? { agent: agent.identity } : {},
|
|
578
|
-
...usage?.kind === "turn-completed" && usage.usage ? { usage: usage.usage } : {},
|
|
579
|
-
events: encodedEvents
|
|
632
|
+
let reading;
|
|
633
|
+
let reply;
|
|
634
|
+
let attempts = 0;
|
|
635
|
+
try {
|
|
636
|
+
let message = prompt;
|
|
637
|
+
let problem = "";
|
|
638
|
+
while (attempts < 2 && !reading) {
|
|
639
|
+
attempts += 1;
|
|
640
|
+
const from = events.length;
|
|
641
|
+
const result = await session.send(message);
|
|
642
|
+
if (result !== void 0) events.push(...turnResultEvents(result, attempts, () => /* @__PURE__ */ new Date()));
|
|
643
|
+
reply = replyIn(events.slice(from));
|
|
644
|
+
if (reply === void 0) problem = "it did not reply";
|
|
645
|
+
else try {
|
|
646
|
+
reading = readJudgeAnswer(rule.answer, reply);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (!(error instanceof UnreadableAnswer)) throw error;
|
|
649
|
+
problem = error.message;
|
|
580
650
|
}
|
|
581
|
-
|
|
651
|
+
message = `Your answer could not be read: ${problem}. Answer again, only in this format.\n\n${answerInstruction(rule.answer)}`;
|
|
652
|
+
}
|
|
653
|
+
if (!reading) throw new Error(`Judge ${rule.name}'s answer could not be read after ${attempts} tries: ${problem}${reply === void 0 ? "" : `. Its last reply: ${preview(reply)}`}`);
|
|
654
|
+
} finally {
|
|
655
|
+
await session.close?.();
|
|
656
|
+
}
|
|
657
|
+
const recorded = Schema.decodeUnknownSync(JsonValueSchema)(events);
|
|
658
|
+
const model = lastOf(events, (event) => event.kind === "turn-completed" && event.model);
|
|
659
|
+
const usage = totalUsage(events);
|
|
660
|
+
const judge = {
|
|
661
|
+
...agent.identity ? { agent: agent.identity } : {},
|
|
662
|
+
...model?.kind === "turn-completed" && model.model ? { model: model.model } : {},
|
|
663
|
+
...usage ? { usage } : {},
|
|
664
|
+
prompt,
|
|
665
|
+
...reply !== void 0 ? { reply } : {},
|
|
666
|
+
...attempts > 1 ? { attempts } : {},
|
|
667
|
+
events: recorded
|
|
668
|
+
};
|
|
669
|
+
return { value: {
|
|
670
|
+
...reading,
|
|
671
|
+
judge
|
|
672
|
+
} };
|
|
673
|
+
}
|
|
674
|
+
/** Each input's text: what the rule's function returned, or the defaults. */
|
|
675
|
+
async function inputValues(rule, input, placement) {
|
|
676
|
+
if (!rule.inputs) return {
|
|
677
|
+
conversation: conversationText(input.trajectory.events, placement),
|
|
678
|
+
reply: text(input.output)
|
|
679
|
+
};
|
|
680
|
+
const values = {};
|
|
681
|
+
for (const [name, read] of Object.entries(rule.inputs)) values[name] = text(await read(input));
|
|
682
|
+
return values;
|
|
683
|
+
}
|
|
684
|
+
/** The run's conversation as plain lines, for a judge to read: this turn at a checkpoint, else all of it. */
|
|
685
|
+
function conversationText(events, placement) {
|
|
686
|
+
let start = 0;
|
|
687
|
+
if (placement === "transcript") events.forEach((event, index) => {
|
|
688
|
+
if (event.kind === "transcript-step-started" && "message" in event) start = index;
|
|
582
689
|
});
|
|
690
|
+
const lines = [];
|
|
691
|
+
for (const event of events.slice(start)) if (event.kind === "transcript-step-started" && "message" in event) lines.push(`User: ${cut(text(event.message))}`);
|
|
692
|
+
else if (event.source !== "aut") continue;
|
|
693
|
+
else if (event.kind === "message") lines.push(`${capitalize(event.role)}: ${cut(text(event.content))}`);
|
|
694
|
+
else if (event.kind === "tool-call") lines.push(`Tool call ${event.name}: ${cut(text(event.arguments))}`);
|
|
695
|
+
else if (event.kind === "tool-result") lines.push(`Tool result ${event.name ?? event.id}: ${cut(text(event.result))}`);
|
|
696
|
+
return lines.join("\n") || "(nothing happened)";
|
|
697
|
+
}
|
|
698
|
+
/** The judge's answer in one attempt's events: its final output, else its last reply. */
|
|
699
|
+
function replyIn(events) {
|
|
700
|
+
const completed = lastOf(events, (event) => event.kind === "completed" && event.output !== void 0);
|
|
701
|
+
if (completed?.kind === "completed") return completed.output;
|
|
702
|
+
const assistant = lastOf(events, (event) => event.kind === "message" && event.role === "assistant");
|
|
703
|
+
return assistant?.kind === "message" ? assistant.content : void 0;
|
|
704
|
+
}
|
|
705
|
+
function totalUsage(events) {
|
|
706
|
+
let total;
|
|
707
|
+
for (const event of events) {
|
|
708
|
+
if (event.kind !== "turn-completed" || !event.usage) continue;
|
|
709
|
+
total ??= {};
|
|
710
|
+
for (const [key, tokens] of Object.entries(event.usage)) if (tokens !== void 0) total[key] = (total[key] ?? 0) + tokens;
|
|
711
|
+
}
|
|
712
|
+
return total;
|
|
713
|
+
}
|
|
714
|
+
function text(value) {
|
|
715
|
+
if (value === void 0) return "(none)";
|
|
716
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
583
717
|
}
|
|
718
|
+
function cut(value) {
|
|
719
|
+
return value.length > MAX_PART ? `${value.slice(0, MAX_PART)}… (${value.length - MAX_PART} more characters)` : value;
|
|
720
|
+
}
|
|
721
|
+
function preview(value) {
|
|
722
|
+
const shown = text(value).replace(/\s+/g, " ");
|
|
723
|
+
return shown.length > 300 ? `${shown.slice(0, 300)}…` : shown;
|
|
724
|
+
}
|
|
725
|
+
/** The last element matching, scanning from the end (no ES2023 lib here). */
|
|
726
|
+
function lastOf(items, match) {
|
|
727
|
+
for (let index = items.length - 1; index >= 0; index -= 1) if (match(items[index])) return items[index];
|
|
728
|
+
}
|
|
729
|
+
const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
584
730
|
//#endregion
|
|
585
731
|
//#region ../runner/src/evaluate-rule.ts
|
|
586
732
|
const failure$1 = (name, cause) => new RuleExecutionError({
|
|
@@ -589,38 +735,30 @@ const failure$1 = (name, cause) => new RuleExecutionError({
|
|
|
589
735
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
590
736
|
});
|
|
591
737
|
/** Both placements call the same rule executor; only input timing and persistence differ. */
|
|
592
|
-
function evaluateRule(rule, input,
|
|
738
|
+
function evaluateRule(rule, input, defaultJudge, placement) {
|
|
593
739
|
return Effect.gen(function* () {
|
|
594
|
-
|
|
595
|
-
|
|
740
|
+
const judgeAgent = rule.kind === "judge" ? rule.agent ?? defaultJudge : void 0;
|
|
741
|
+
if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("This judge has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts")));
|
|
742
|
+
const evaluated = rule.kind === "predicate" ? yield* Effect.tryPromise({
|
|
596
743
|
try: async () => rule.run(input),
|
|
597
744
|
catch: (cause) => failure$1(rule.name, cause)
|
|
598
|
-
})
|
|
745
|
+
}) : (yield* runJudgeAgent(rule, judgeAgent, input, placement)).value;
|
|
599
746
|
const score = yield* Effect.try({
|
|
600
|
-
try: () => normalizeScore(evaluated.
|
|
747
|
+
try: () => normalizeScore(evaluated, rule.passAt, rule.kind === "predicate" ? rule.criteria : void 0),
|
|
601
748
|
catch: (cause) => failure$1(rule.name, cause)
|
|
602
749
|
});
|
|
603
|
-
const withJudge = rule.kind === "judge" && "judge" in evaluated ? {
|
|
604
|
-
...score,
|
|
605
|
-
judge: evaluated.judge
|
|
606
|
-
} : score;
|
|
607
750
|
const validated = yield* Schema.decodeUnknown(ScoreResultSchema)({
|
|
608
751
|
name: rule.name,
|
|
609
752
|
kind: rule.kind,
|
|
610
753
|
durationMs: 0,
|
|
611
|
-
...
|
|
754
|
+
...score
|
|
612
755
|
}).pipe(Effect.mapError((cause) => failure$1(rule.name, cause)));
|
|
613
756
|
if (validated.skipped) return {
|
|
614
757
|
skipped: true,
|
|
615
758
|
...validated.explanation === void 0 ? {} : { explanation: validated.explanation }
|
|
616
759
|
};
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
passed: validated.passed,
|
|
620
|
-
...validated.explanation === void 0 ? {} : { explanation: validated.explanation },
|
|
621
|
-
...validated.evidence === void 0 ? {} : { evidence: validated.evidence },
|
|
622
|
-
...validated.judge === void 0 ? {} : { judge: validated.judge }
|
|
623
|
-
};
|
|
760
|
+
const { name: _name, kind: _kind, durationMs: _duration, ...result } = validated;
|
|
761
|
+
return result;
|
|
624
762
|
});
|
|
625
763
|
}
|
|
626
764
|
//#endregion
|
|
@@ -794,6 +932,7 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
794
932
|
name: step.name,
|
|
795
933
|
timestamp: started.toISOString()
|
|
796
934
|
});
|
|
935
|
+
const tracked = trackAttachments(input.attach);
|
|
797
936
|
return runBoundary(evaluateCheckpoint(step, index, {
|
|
798
937
|
context: input.context,
|
|
799
938
|
artifacts: input.artifacts,
|
|
@@ -803,8 +942,11 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
803
942
|
data: input.data,
|
|
804
943
|
checkpoints: [...progress.checkpoints],
|
|
805
944
|
checkpoint: checkpointLookup(definition.transcript, [...progress.checkpoints]),
|
|
806
|
-
attach:
|
|
807
|
-
}, now,
|
|
945
|
+
attach: tracked.attach
|
|
946
|
+
}, now, input.defaultJudge)).then((result) => ({
|
|
947
|
+
...result,
|
|
948
|
+
...attachedFields(tracked.attached)
|
|
949
|
+
})).catch(async (error) => {
|
|
808
950
|
const recorded = recordError(error);
|
|
809
951
|
progress.checkpoints.push({
|
|
810
952
|
step: index,
|
|
@@ -812,7 +954,8 @@ async function runCheckpoint(definition, step, index, input) {
|
|
|
812
954
|
name: step.name,
|
|
813
955
|
status: "error",
|
|
814
956
|
durationMs: now().getTime() - started.getTime(),
|
|
815
|
-
error: recorded
|
|
957
|
+
error: recorded,
|
|
958
|
+
...attachedFields(tracked.attached)
|
|
816
959
|
});
|
|
817
960
|
try {
|
|
818
961
|
await emitRunner({
|
|
@@ -889,13 +1032,15 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
889
1032
|
timestamp: started.toISOString()
|
|
890
1033
|
});
|
|
891
1034
|
let result;
|
|
1035
|
+
const tracked = trackAttachments(input.attach);
|
|
892
1036
|
try {
|
|
893
1037
|
const score = await runBoundary(evaluateRule(scorer, scoringContext({
|
|
894
1038
|
...input,
|
|
1039
|
+
attach: tracked.attach,
|
|
895
1040
|
context,
|
|
896
1041
|
workspace,
|
|
897
1042
|
transcript: definition.transcript
|
|
898
|
-
}),
|
|
1043
|
+
}), input.defaultJudge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
|
|
899
1044
|
cause,
|
|
900
1045
|
message: cause.message
|
|
901
1046
|
}))));
|
|
@@ -903,7 +1048,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
903
1048
|
name: scorer.name,
|
|
904
1049
|
kind: scorer.kind,
|
|
905
1050
|
durationMs: now().getTime() - started.getTime(),
|
|
906
|
-
...score
|
|
1051
|
+
...score,
|
|
1052
|
+
...attachedFields(tracked.attached)
|
|
907
1053
|
};
|
|
908
1054
|
results.push(result);
|
|
909
1055
|
} catch (error) {
|
|
@@ -913,7 +1059,8 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
913
1059
|
kind: scorer.kind,
|
|
914
1060
|
durationMs: now().getTime() - started.getTime(),
|
|
915
1061
|
error: recorded,
|
|
916
|
-
passed: false
|
|
1062
|
+
passed: false,
|
|
1063
|
+
...attachedFields(tracked.attached)
|
|
917
1064
|
});
|
|
918
1065
|
await emitRunner({
|
|
919
1066
|
kind: "scorer-failed",
|
|
@@ -934,11 +1081,12 @@ async function evaluateFinalScorers(definition, input) {
|
|
|
934
1081
|
//#endregion
|
|
935
1082
|
//#region ../runner/src/metrics.ts
|
|
936
1083
|
/**
|
|
937
|
-
* What EvalKit measures for every run from its trajectory
|
|
1084
|
+
* What EvalKit measures for every run: from its trajectory, the agent's time
|
|
938
1085
|
* (first event to the last one before final scoring), tokens when the agent
|
|
939
|
-
* reported usage, and tool calls
|
|
1086
|
+
* reported usage, and tool calls; from its scores and checkpoints, the
|
|
1087
|
+
* tokens its judges used, kept apart from the agent's.
|
|
940
1088
|
*/
|
|
941
|
-
function builtInMetrics(events) {
|
|
1089
|
+
function builtInMetrics(events, verdicts = []) {
|
|
942
1090
|
const times = events.filter((event) => !event.kind.startsWith("scorer-")).map((event) => Date.parse(event.timestamp)).filter((time) => !Number.isNaN(time));
|
|
943
1091
|
const metrics = [];
|
|
944
1092
|
if (times.length) metrics.push({
|
|
@@ -965,6 +1113,18 @@ function builtInMetrics(events) {
|
|
|
965
1113
|
value: events.filter((event) => event.source === "aut" && event.kind === "tool-call").length,
|
|
966
1114
|
builtIn: true
|
|
967
1115
|
});
|
|
1116
|
+
let judgeTokens;
|
|
1117
|
+
for (const verdict of verdicts) {
|
|
1118
|
+
const used = verdict.judge?.usage && turnTokens(verdict.judge.usage);
|
|
1119
|
+
if (used !== void 0) judgeTokens = (judgeTokens ?? 0) + used;
|
|
1120
|
+
}
|
|
1121
|
+
if (judgeTokens !== void 0) metrics.push({
|
|
1122
|
+
name: "judge tokens",
|
|
1123
|
+
value: judgeTokens,
|
|
1124
|
+
unit: "tokens",
|
|
1125
|
+
better: "lower",
|
|
1126
|
+
builtIn: true
|
|
1127
|
+
});
|
|
968
1128
|
return metrics;
|
|
969
1129
|
}
|
|
970
1130
|
/** Runs the scenario's metrics; a failing metric records its error and the rest still run. */
|
|
@@ -1410,8 +1570,9 @@ async function executeBatch(definition, options) {
|
|
|
1410
1570
|
let hasUser = false;
|
|
1411
1571
|
for (const step of definition.transcript) if (step.kind === "user") hasUser = true;
|
|
1412
1572
|
else if (step.kind !== "agent" && !hasUser) throw new Error("Checkpoint requires a preceding user step");
|
|
1413
|
-
if (definition.judge && definition.judge === definition.agent) throw new Error("Judge agent must be distinct from the agent under test");
|
|
1414
1573
|
assertNoRemovedOptions(definition);
|
|
1574
|
+
assertJudgesIndependent(definition, options.judge);
|
|
1575
|
+
for (const rule of [...definition.transcript, ...definition.scoring]) if (rule.kind === "judge" && !rule.agent && !options.judge) throw new Error(`Judge ${rule.name} in ${definition.id} has no agent: pass judge(name, { agent }) or set judge in evalkit.config.ts`);
|
|
1415
1576
|
if (options.runId) assertUuid(options.runId, "runId");
|
|
1416
1577
|
const repeat = options.repeat ?? definition.repeat ?? 1;
|
|
1417
1578
|
if (!Number.isInteger(repeat) || repeat < 1) throw new Error(`repeat must be a positive integer; received ${repeat}`);
|
|
@@ -1587,7 +1748,8 @@ async function runOnce(definition, options) {
|
|
|
1587
1748
|
data: scoringData,
|
|
1588
1749
|
signal,
|
|
1589
1750
|
progress,
|
|
1590
|
-
attach: attachments.attach
|
|
1751
|
+
attach: attachments.attach,
|
|
1752
|
+
defaultJudge: options.judge
|
|
1591
1753
|
}),
|
|
1592
1754
|
catch: (cause) => cause
|
|
1593
1755
|
}), (opened) => Effect.tryPromise({
|
|
@@ -1630,10 +1792,11 @@ async function runOnce(definition, options) {
|
|
|
1630
1792
|
data: scoringData,
|
|
1631
1793
|
checkpoints,
|
|
1632
1794
|
attach: attachments.attach,
|
|
1795
|
+
defaultJudge: options.judge,
|
|
1633
1796
|
now,
|
|
1634
1797
|
emitRunner
|
|
1635
1798
|
});
|
|
1636
|
-
metrics.push(...builtInMetrics(events));
|
|
1799
|
+
metrics.push(...builtInMetrics(events, [...scoreResults, ...checkpoints]));
|
|
1637
1800
|
if (context && workspace && !wasCancelled) metrics.push(...await evaluateMetrics(definition, scoringContext({
|
|
1638
1801
|
context,
|
|
1639
1802
|
workspace,
|
|
@@ -104,6 +104,7 @@ function upgradeManifest(input) {
|
|
|
104
104
|
*/
|
|
105
105
|
function assertNoRemovedOptions(definition) {
|
|
106
106
|
if ("fixtures" in definition) throw new Error(`Eval ${String(definition.id)} uses fixtures, which were replaced by datasets: [defineDataset(...)]`);
|
|
107
|
+
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)`);
|
|
107
108
|
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)`);
|
|
108
109
|
}
|
|
109
110
|
//#endregion
|