@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.
@@ -1,5 +1,5 @@
1
- import { $ as relativeDatasetPath, A as RunManifestSchema, B as turnResultEvents, C as CheckpointResultSchema, F as RunSummarySchema, G as DEFAULT_ATTACHMENT_LIMITS, H as canonicalParameters, I as ScoreResultSchema, L as TrajectoryEventSchema, M as RunScoringSchema, U as turnTokens, W as DEFAULT_ARTIFACT_EXCLUDE, X as toJsonObject, Y as toJson, _ as parseTrajectoryJsonl, a as authoringId, i as agentRuntimes, j as RunMetadataSchema, ot as resourceUri, p as recordError, w as JsonValueSchema, z as normalizeAutEvent } from "./src-BrfIXfO2.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-60SIRius.mjs";
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
- * `passAt` turns a numeric value into a verdict; an explicit `passed` wins.
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 || !Number.isFinite(normalized.value) || normalized.value < 0 || normalized.value > 1) throw new Error(`Score value must be a finite number from 0 to 1; received ${normalized?.value}`);
497
- if ("judge" in normalized && normalized.judge) {
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 (usage && Object.values(usage).some((tokens) => !Number.isSafeInteger(tokens) || tokens < 0)) throw new Error("Judge token usage must contain nonnegative integers");
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 passed = "passed" in normalized && normalized.passed !== void 0 ? normalized.passed : normalized.value >= (passAt ?? 1);
571
+ const mark = passAt ?? 1;
502
572
  return {
503
- value: normalized.value,
504
- passed,
505
- ..."explanation" in normalized && normalized.explanation ? { explanation: normalized.explanation } : {},
506
- ..."evidence" in normalized && normalized.evidence !== void 0 ? { evidence: toJson(normalized.evidence, "evidence") } : {},
507
- ..."judge" in normalized && normalized.judge !== void 0 ? { judge: normalized.judge } : {}
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
- /** Drive a distinct agent session for one assessment; do not add its events to AUT trajectory. */
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 prompt = JSON.stringify({
527
- name: rule.name,
528
- rubric: rule.rubric,
529
- placement,
530
- evidence: placement === "transcript" ? { turn: input.turn?.events ?? [] } : { trajectory: input.trajectory.events },
531
- instruction: "Return only a JSON verdict with value (a number from 0 to 1), optional passed, explanation, and evidence."
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 closeError;
534
- return Effect.gen(function* () {
535
- yield* Effect.acquireUseRelease(Effect.tryPromise({
536
- try: () => agent.start({
537
- context,
538
- attach: input.attach,
539
- onEvent: (event) => {
540
- const observed = Schema.decodeUnknownSync(TrajectoryEventSchema)({
541
- ...normalizeAutEvent(event, () => /* @__PURE__ */ new Date()),
542
- source: "aut"
543
- });
544
- if (observed.source !== "aut") throw new Error("Invalid judge event source");
545
- const { source: _, ...validated } = observed;
546
- events.push(validated);
547
- }
548
- }),
549
- catch: (cause) => failure$2(rule.name, cause)
550
- }), (session) => Effect.tryPromise({
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, judgeAgent, placement) {
738
+ function evaluateRule(rule, input, defaultJudge, placement) {
593
739
  return Effect.gen(function* () {
594
- if (rule.kind === "judge" && !judgeAgent) return yield* Effect.fail(failure$1(rule.name, /* @__PURE__ */ new Error("Judge rules require an eval judge agent")));
595
- const evaluated = rule.kind === "predicate" ? { value: yield* Effect.tryPromise({
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
- }) } : yield* runJudgeAgent(rule, judgeAgent, input, placement);
745
+ }) : (yield* runJudgeAgent(rule, judgeAgent, input, placement)).value;
599
746
  const score = yield* Effect.try({
600
- try: () => normalizeScore(evaluated.value, rule.kind === "predicate" ? rule.passAt : void 0),
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
- ...withJudge
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
- return {
618
- value: validated.value,
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: input.attach
807
- }, now, definition.judge)).catch(async (error) => {
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
- }), definition.judge, "scoring").pipe(Effect.mapError((cause) => new ScoringError({
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: the agent's time
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leostera/evalkit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Author and run agent evals as code",
5
5
  "type": "module",
6
6
  "license": "MIT",