@statelyai/agent 2.0.0-alpha.32 → 2.0.0-alpha.33

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @statelyai/agent
2
2
 
3
+ ## 2.0.0-alpha.33
4
+
5
+ ### Minor Changes
6
+
7
+ - [#181](https://github.com/statelyai/agent/pull/181) [`407e2a6`](https://github.com/statelyai/agent/commit/407e2a6c03e463d4b8d92997cf59e72b30301511) Thanks [@davidkpiano](https://github.com/davidkpiano)! - **Four eval helpers in `@statelyai/agent/testing`.**
8
+
9
+ - `traceCoverage(machine, traces, { reachable? })` reports which states and handlers no trace exercised. Coverage is per handler: a state and an event it handles, with the targets it declares and the ones runs reached. `always` handlers are listed as unobservable, because traces record one step per macrostep.
10
+ - `repeatRuns({ scenarios, k, run, passed })` runs each scenario `k` times and reports passes, pass@k, pass^k, distinct paths, and the state where the runs diverged. `report.passHat(j)` estimates the suite's pass^j. `summarizeRepeats` builds the same report from runs collected elsewhere.
11
+ - `judgeAgreement(rows, { positive?, groupBy? })` compares a judge's labels with confirmed human labels: accuracy, per-label recall, the confusion matrix, disagreements, the true positive and true negative rates, and a correction for the judge's bias.
12
+ - `firstDivergence(actual, reference)` finds the first step where a run departs from a reference trace or an expected trajectory, and names the state it was in.
13
+ - `foldTraces` takes a `blame` option and adds a `blamed` column: failed runs counted once, against the state their failure started in. Rows now always include `blamed`, which is 0 without `blame`.
14
+
3
15
  ## 2.0.0-alpha.32
4
16
 
5
17
  ### Patch Changes
package/dist/testing.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_event_log = require("./event-log-CKWeC7H-.cjs");
3
3
  const require_run_agent = require("./run-agent-Cg0xd3K-.cjs");
4
- const require_verify = require("./verify-D_llRCTr.cjs");
4
+ const require_verify = require("./verify-CS_WLRXn.cjs");
5
5
  //#region src/trajectory.ts
6
6
  /**
7
7
  * Classifies one item. Event-shaped things (log entries, events) become events;
@@ -381,12 +381,47 @@ function statePaths(value, prefix = "") {
381
381
  if (entries.length === 0) return prefix ? [prefix.slice(0, -1)] : [];
382
382
  return entries.flatMap(([key, child]) => statePaths(child, `${prefix}${key}.`));
383
383
  }
384
+ /**
385
+ * A trace's `machine.transition` events as steps, skipping `@xstate.init`.
386
+ * A first-attempt decision's `request.start` names the decision on the step
387
+ * that follows it. Reads live and serialized traces alike.
388
+ * @internal
389
+ */
390
+ function traceSteps(trace) {
391
+ const steps = [];
392
+ let decision;
393
+ for (const event of trace) {
394
+ if (event.type === "request.start" && isFirstDecision(event.request)) {
395
+ const name = event.request.name;
396
+ decision = typeof name === "string" ? name : void 0;
397
+ continue;
398
+ }
399
+ if (event.type !== "machine.transition") continue;
400
+ const type = require_event_log.isRecord(event.event) ? event.event.type : void 0;
401
+ if (type === "@xstate.init") continue;
402
+ const fromValue = valueOf(event.from);
403
+ const toValue = valueOf(event.snapshot);
404
+ steps.push({
405
+ from: statePaths(fromValue),
406
+ fromValue,
407
+ event: typeof type === "string" ? type : "",
408
+ payload: event.event,
409
+ to: statePaths(toValue),
410
+ toValue,
411
+ ignored: event.ignored === true,
412
+ ...decision === void 0 ? {} : { decision }
413
+ });
414
+ decision = void 0;
415
+ }
416
+ return steps;
417
+ }
384
418
  //#endregion
385
419
  //#region src/fold-traces.ts
386
420
  /**
387
421
  * Folds many runs' traces into per-state stats: how many runs visited each
388
422
  * state, how many of those failed, how many options the guards withheld
389
- * at decisions made there, and how many events it ignored. Pure; reads only:
423
+ * at decisions made there, how many events it ignored, and how many failures
424
+ * `blame` traced back to it. Pure; reads only:
390
425
  *
391
426
  * - `machine.transition`: the run visited `from.value` and `snapshot.value`;
392
427
  * with `ignored`, each of those states ignored one event.
@@ -399,6 +434,10 @@ function statePaths(value, prefix = "") {
399
434
  * `{ type: 'request.start', request: result.request }` onto the trace before
400
435
  * sending its event. Parallel states count once per active leaf.
401
436
  *
437
+ * `failures` counts a failed run against every state it visited, so states
438
+ * after a bad decision rank as high as the decision. Sort by `blamed` to see
439
+ * where failures start.
440
+ *
402
441
  * @example
403
442
  * ```ts no-check
404
443
  * const stats = foldTraces(traces, {
@@ -416,7 +455,8 @@ function foldTraces(traces, options) {
416
455
  visits: 0,
417
456
  failures: 0,
418
457
  withheld: 0,
419
- ignored: 0
458
+ ignored: 0,
459
+ blamed: 0
420
460
  };
421
461
  stats.set(state, entry);
422
462
  }
@@ -445,10 +485,249 @@ function foldTraces(traces, options) {
445
485
  entry.visits++;
446
486
  if (failed) entry.failures++;
447
487
  }
488
+ const blamed = failed ? options.blame?.(trace) : void 0;
489
+ if (blamed !== void 0) statsFor(blamed).blamed++;
448
490
  }
449
491
  return [...stats.values()];
450
492
  }
451
493
  //#endregion
494
+ //#region src/trace-coverage.ts
495
+ /** Mirrors xstate@6.0.0-alpha.69 `matchesEventDescriptor` (src/utils.ts), which the package does not export. */
496
+ function matchesDescriptor(eventType, descriptor) {
497
+ if (descriptor === eventType || descriptor === "*") return true;
498
+ if (!descriptor.endsWith(".*")) return false;
499
+ const tokens = descriptor.split(".");
500
+ const eventTokens = eventType.split(".");
501
+ for (let index = 0; index < tokens.length; index++) {
502
+ if (tokens[index] === "*") return index === tokens.length - 1;
503
+ if (tokens[index] !== eventTokens[index]) return false;
504
+ }
505
+ return true;
506
+ }
507
+ /** Whether a definition's `matches` pattern (e.g. an invoke's `actorId`) accepts the traced event. */
508
+ function matchesPattern(definition, event) {
509
+ const pattern = definition.matches;
510
+ if (!require_event_log.isRecord(pattern) || !require_event_log.isRecord(event)) return true;
511
+ return Object.entries(pattern).every(([key, value]) => Object.is(event[key], value));
512
+ }
513
+ /**
514
+ * The descriptor XState would try first at `node` for `event`, guards aside:
515
+ * the exact key, then partial and wildcard keys longest first, as in
516
+ * xstate@6.0.0-alpha.69 `getCandidates` (src/stateUtils.ts).
517
+ */
518
+ function candidateAt(node, type, event) {
519
+ const keys = [...node.transitions.keys()].filter((key) => key !== type && matchesDescriptor(type, key)).sort((a, b) => b.length - a.length);
520
+ if (node.transitions.has(type)) keys.unshift(type);
521
+ return keys.find((key) => node.transitions.get(key).some((d) => matchesPattern(d, event)));
522
+ }
523
+ const pathOf = (node) => node.path.join(".");
524
+ const ratioOf = (visited, total) => total === 0 ? 1 : visited / total;
525
+ /**
526
+ * Measures how much of a machine a set of runs exercised: which atomic and
527
+ * final states they visited, and which handlers they took. A handler is a
528
+ * `(declaring state, event descriptor)` pair from `on`, invoke
529
+ * `onDone`/`onError`, and `after`, with the targets it declares and the
530
+ * leaves the runs landed in after it. Pure; reads only `machine.transition`
531
+ * events, live (`AgentTraceEvent`) or serialized (`serializeTraceEvent`).
532
+ *
533
+ * A step credits one handler per active leaf: the nearest node, from the leaf
534
+ * up, whose exact, partial (`a.*`), or wildcard (`*`) descriptor matches the
535
+ * event, preferred in XState's order. Guards are not replayed, so a step
536
+ * whose nearest handler was rejected and fell through to an ancestor is
537
+ * credited to the nearest one. XState keys invoke and `after` handlers by
538
+ * bare type (`xstate.done.actor`), one per state however many invokes; a
539
+ * definition's `matches` (an invoke's `actorId`) must accept the traced event
540
+ * for its state to be credited. Ignored steps credit no handler. `always`
541
+ * handlers are listed as `unobservable`, never as unvisited.
542
+ *
543
+ * @example
544
+ * ```ts no-check
545
+ * const report = traceCoverage(machine, traces, { reachable: await explorePaths(machine) });
546
+ * expect(report.unvisitedHandlers).toEqual([]);
547
+ * ```
548
+ */
549
+ function traceCoverage(machine, traces, options = {}) {
550
+ const nodes = /* @__PURE__ */ new Map();
551
+ const states = [];
552
+ const handlers = [];
553
+ const handlerAt = /* @__PURE__ */ new Map();
554
+ const unobservable = [];
555
+ const walk = (node) => {
556
+ const state = pathOf(node);
557
+ nodes.set(state, node);
558
+ if (node.type === "atomic" || node.type === "final") states.push({
559
+ state,
560
+ runs: 0
561
+ });
562
+ const byEvent = /* @__PURE__ */ new Map();
563
+ for (const [event, definitions] of node.transitions) {
564
+ const dynamic = definitions.some((d) => typeof d.to === "function");
565
+ const declared = [...new Set(definitions.flatMap((d) => (d.target ?? []).map(pathOf)))];
566
+ const handler = {
567
+ state,
568
+ event,
569
+ runs: 0,
570
+ targets: {
571
+ declared: dynamic ? "dynamic" : declared,
572
+ seen: [],
573
+ unreached: []
574
+ }
575
+ };
576
+ handlers.push(handler);
577
+ byEvent.set(event, handler);
578
+ }
579
+ handlerAt.set(node, byEvent);
580
+ if (node.always?.length) unobservable.push({
581
+ state,
582
+ event: "always"
583
+ });
584
+ for (const child of Object.values(node.states)) walk(child);
585
+ };
586
+ walk(machine.root);
587
+ const stateRuns = new Map(states.map((row) => [row.state, row]));
588
+ const seen = /* @__PURE__ */ new Map();
589
+ for (const trace of traces) {
590
+ const steps = traceSteps(trace);
591
+ const visited = new Set(steps[0]?.from);
592
+ for (const event of trace) {
593
+ if (event.type !== "machine.transition" || !require_event_log.isRecord(event.event)) continue;
594
+ if (event.event.type !== "@xstate.init" || !require_event_log.isRecord(event.snapshot)) continue;
595
+ for (const leaf of statePaths(event.snapshot.value)) visited.add(leaf);
596
+ }
597
+ const credited = /* @__PURE__ */ new Set();
598
+ for (const step of steps) {
599
+ for (const leaf of step.to) visited.add(leaf);
600
+ if (step.ignored) continue;
601
+ const hits = /* @__PURE__ */ new Set();
602
+ for (const leaf of step.from) for (let node = nodes.get(leaf); node; node = node.parent) {
603
+ if (node.type === "final") continue;
604
+ const event = candidateAt(node, step.event, step.payload);
605
+ if (event === void 0) continue;
606
+ hits.add(handlerAt.get(node).get(event));
607
+ break;
608
+ }
609
+ for (const handler of hits) {
610
+ credited.add(handler);
611
+ let leaves = seen.get(handler);
612
+ if (!leaves) seen.set(handler, leaves = /* @__PURE__ */ new Set());
613
+ for (const leaf of step.to) leaves.add(leaf);
614
+ }
615
+ }
616
+ for (const state of visited) {
617
+ const row = stateRuns.get(state);
618
+ if (row) row.runs++;
619
+ }
620
+ for (const handler of credited) handler.runs++;
621
+ }
622
+ for (const handler of handlers) {
623
+ const leaves = [...seen.get(handler) ?? []];
624
+ handler.targets.seen = leaves;
625
+ const { declared } = handler.targets;
626
+ if (declared === "dynamic") continue;
627
+ handler.targets.unreached = declared.filter((target) => !leaves.some((leaf) => leaf === target || leaf.startsWith(`${target}.`)));
628
+ }
629
+ const reachableComplete = options.reachable ? require_verify.isExplorationComplete(options.reachable) : void 0;
630
+ let unreachable;
631
+ if (options.reachable && reachableComplete) {
632
+ const reached = new Set(options.reachable.reachedStates.flatMap((value) => statePaths(value)));
633
+ unreachable = states.filter((row) => !reached.has(row.state) && row.runs === 0).map((row) => row.state);
634
+ }
635
+ const counted = unreachable ? states.filter((row) => !unreachable.includes(row.state)) : states;
636
+ const unvisitedStates = counted.filter((row) => row.runs === 0).map((row) => row.state);
637
+ const unvisitedHandlers = handlers.filter((handler) => handler.runs === 0).map(({ state, event }) => ({
638
+ state,
639
+ event
640
+ }));
641
+ return {
642
+ states,
643
+ handlers,
644
+ unvisitedStates,
645
+ unvisitedHandlers,
646
+ unobservable,
647
+ ...unreachable ? { unreachable } : {},
648
+ ...reachableComplete === void 0 ? {} : { reachableComplete },
649
+ ratio: {
650
+ states: ratioOf(counted.length - unvisitedStates.length, counted.length),
651
+ handlers: ratioOf(handlers.length - unvisitedHandlers.length, handlers.length)
652
+ }
653
+ };
654
+ }
655
+ //#endregion
656
+ //#region src/first-divergence.ts
657
+ const defaultKey = (step) => `${step.from.join(",")}|${step.event}|${step.to.join(",")}`;
658
+ const isTrace = (items) => items.length === 0 || items.some((item) => require_event_log.isRecord(item) && (item.type === "machine.transition" || item.type === "run.start" || item.type === "run.end"));
659
+ /**
660
+ * Finds the first step where a run departs from a reference, so a failure is
661
+ * blamed on the state it started in rather than on every state the run went
662
+ * through afterwards.
663
+ *
664
+ * - Against a reference **trace**, such as a passing run of the same
665
+ * scenario, steps are compared in order by `key`. The first step that
666
+ * differs, or the point where either trace ends early, is the divergence.
667
+ * - Against an expected **trajectory** (`TrajectoryItem[]`, as for
668
+ * {@link matchesTrajectory}), the run is laid out as its initial state, then
669
+ * each step's event and new state. The first expected item it never reaches
670
+ * is the divergence, blamed on the step after the last item it did reach. A
671
+ * run that goes on past the trajectory has not diverged.
672
+ *
673
+ * An empty reference is a trace with no steps, so any step diverges from it.
674
+ * A trace is one run's events, live or serialized. Steps the machine ignored
675
+ * changed nothing and are left out on both sides. Returns `undefined` when the
676
+ * run does not diverge.
677
+ *
678
+ * @example
679
+ * ```ts no-check
680
+ * const stats = foldTraces(traces, {
681
+ * failed,
682
+ * blame: (trace) => firstDivergence(trace, passingRunFor(trace))?.state,
683
+ * });
684
+ * ```
685
+ */
686
+ function firstDivergence(actual, reference, options = {}) {
687
+ const steps = traceSteps(actual).filter((step) => !step.ignored);
688
+ return isTrace(reference) ? againstTrace(steps, traceSteps(reference).filter((step) => !step.ignored), options.key ?? defaultKey) : againstTrajectory(actual, steps, reference);
689
+ }
690
+ function diverged(steps, index, expected, initial = steps[0]?.fromValue) {
691
+ const step = steps[index];
692
+ return {
693
+ index,
694
+ state: (step ? step.from[0] : steps.at(-1)?.to[0] ?? statePaths(initial)[0]) ?? "",
695
+ ...step?.decision === void 0 ? {} : { decision: step.decision },
696
+ ...step ? { actual: step } : {},
697
+ ...expected === void 0 ? {} : { expected }
698
+ };
699
+ }
700
+ function againstTrace(steps, reference, key) {
701
+ const length = Math.max(steps.length, reference.length);
702
+ for (let index = 0; index < length; index++) {
703
+ const step = steps[index];
704
+ const want = reference[index];
705
+ if (step && want && key(step) === key(want)) continue;
706
+ return diverged(steps, index, want, steps[0]?.fromValue ?? reference[0]?.fromValue);
707
+ }
708
+ }
709
+ function againstTrajectory(trace, steps, expected) {
710
+ const initial = initialValue(trace, steps);
711
+ const timeline = initial === void 0 ? [] : [{ value: initial }];
712
+ const stepAt = initial === void 0 ? [] : [0];
713
+ steps.forEach((step, index) => {
714
+ timeline.push(step.payload);
715
+ stepAt.push(index);
716
+ if (JSON.stringify(step.toValue) !== JSON.stringify(step.fromValue)) {
717
+ timeline.push({ value: step.toValue });
718
+ stepAt.push(index);
719
+ }
720
+ });
721
+ const match = matchesTrajectory(timeline, expected);
722
+ if (!match.firstMiss) return void 0;
723
+ const at = match.firstMiss.searchedFrom;
724
+ return diverged(steps, at < timeline.length ? stepAt[at] ?? steps.length : steps.length, match.firstMiss.expected, initial);
725
+ }
726
+ function initialValue(trace, steps) {
727
+ for (const event of trace) if (event.type === "machine.transition" && require_event_log.isRecord(event.snapshot)) return event.snapshot.value;
728
+ return steps[0]?.fromValue;
729
+ }
730
+ //#endregion
452
731
  //#region src/decision-requests.ts
453
732
  /**
454
733
  * The decisions a run asked for, read from its trace: each decision's first
@@ -481,6 +760,212 @@ function getDecisionRequests(trace, options = {}) {
481
760
  return requests;
482
761
  }
483
762
  //#endregion
763
+ //#region src/repeat-runs.ts
764
+ /**
765
+ * Summarizes repeated runs of each scenario: how often each passed, whether
766
+ * all its runs passed (pass^k), how many distinct state paths they took, and
767
+ * where they first went different ways. Pure. Use it for runs collected
768
+ * elsewhere, such as CI shards or replays; {@link repeatRuns} collects them and
769
+ * calls it.
770
+ *
771
+ * A scenario that passes 3 of 5 runs and `divergedAt: 'deciding'` points at one
772
+ * decision, not at a flaky agent.
773
+ *
774
+ * @example
775
+ * ```ts no-check
776
+ * const report = summarizeRepeats(shards.map((s) => ({ id: s.id, scenario: s, runs: s.runs })));
777
+ * report.passHat(3); // the chance 3 runs of a scenario all pass
778
+ * ```
779
+ */
780
+ function summarizeRepeats(groups) {
781
+ const scenarios = groups.map((group) => summarize$1(group));
782
+ const traces = groups.flatMap((group) => group.runs.flatMap((run) => run.trace ? [run.trace] : []));
783
+ const most = Math.max(0, ...groups.map((group) => group.runs.length));
784
+ const passHat = (j) => {
785
+ if (!Number.isInteger(j) || j < 1 || j > most) throw new require_event_log.AgentError("pass-hat-out-of-range", `passHat: j must be a whole number from 1 to ${most}, got ${j}.`);
786
+ const eligible = scenarios.filter((scenario) => scenario.runs.length >= j);
787
+ return eligible.reduce((total, scenario) => total + allPass(scenario.passes, scenario.runs.length, j), 0) / eligible.length;
788
+ };
789
+ return {
790
+ scenarios,
791
+ passHat,
792
+ traces
793
+ };
794
+ }
795
+ /**
796
+ * Runs every scenario `k` times and summarizes the runs with
797
+ * {@link summarizeRepeats}. Runs are sequential, scenario by scenario. A run
798
+ * that throws fails, with its message as `error` and no trace.
799
+ *
800
+ * With a live model, one run per scenario says little: τ-bench found pass^k
801
+ * falls well below the single-run rate. Report pass^k next to it.
802
+ *
803
+ * @example
804
+ * ```ts no-check
805
+ * const report = await repeatRuns({
806
+ * scenarios,
807
+ * k: 5,
808
+ * run: async (scenario) => {
809
+ * const trace: AgentTraceEvent[] = [];
810
+ * const result = await runToQuiescence(
811
+ * createAgentRuntime(machine, { executors, onTrace: (event) => trace.push(event) }),
812
+ * { input: scenario.input },
813
+ * );
814
+ * return { trace, outcome: String(result.snapshot.value) };
815
+ * },
816
+ * passed: (scenario, { outcome }) => outcome === scenario.expected,
817
+ * });
818
+ * report.passHat(5);
819
+ * ```
820
+ */
821
+ async function repeatRuns(options) {
822
+ if (!Number.isInteger(options.k) || options.k < 1) throw new require_event_log.AgentError("invalid-repeat-count", `repeatRuns: k must be a whole number of 1 or more, got ${options.k}.`);
823
+ const groups = [];
824
+ for (const scenario of options.scenarios) {
825
+ const runs = [];
826
+ for (let attempt = 0; attempt < options.k; attempt++) try {
827
+ const result = await options.run(scenario, attempt);
828
+ runs.push({
829
+ passed: options.passed(scenario, result),
830
+ trace: result.trace,
831
+ ...result.outcome === void 0 ? {} : { outcome: result.outcome }
832
+ });
833
+ } catch (error) {
834
+ runs.push({
835
+ passed: false,
836
+ error: error instanceof Error ? error.message : String(error)
837
+ });
838
+ }
839
+ groups.push({
840
+ id: scenario.id,
841
+ scenario,
842
+ runs
843
+ });
844
+ }
845
+ return summarizeRepeats(groups);
846
+ }
847
+ function summarize$1(group) {
848
+ const passes = group.runs.filter((run) => run.passed).length;
849
+ const traces = group.runs.flatMap((run) => run.trace ? [run.trace] : []);
850
+ const paths = new Set(traces.map((trace) => traceSteps(trace).filter((step) => !step.ignored).map((step) => `${step.from.join(",")}|${step.event}|${step.to.join(",")}`).join(" ")));
851
+ const reference = traces.find((trace) => trace.some((event) => event.type === "machine.transition"));
852
+ let earliest;
853
+ for (const trace of reference ? traces : []) {
854
+ if (trace === reference) continue;
855
+ const divergence = firstDivergence(trace, reference);
856
+ if (divergence && (!earliest || divergence.index < earliest.index)) earliest = divergence;
857
+ }
858
+ return {
859
+ ...group,
860
+ passes,
861
+ passAny: passes > 0,
862
+ passAll: group.runs.length > 0 && passes === group.runs.length,
863
+ distinctPaths: paths.size,
864
+ ...earliest ? { divergedAt: earliest.state } : {}
865
+ };
866
+ }
867
+ /** C(passes, j) / C(runs, j), as a product so large counts don't overflow. */
868
+ function allPass(passes, runs, j) {
869
+ let chance = 1;
870
+ for (let i = 0; i < j; i++) chance *= Math.max(0, passes - i) / (runs - i);
871
+ return chance;
872
+ }
873
+ //#endregion
874
+ //#region src/judge-agreement.ts
875
+ /**
876
+ * Measures how well a judge agrees with people on the same cases: accuracy,
877
+ * per-label recall, the confusion matrix, and the rows that disagree. With
878
+ * `positive`, also the TPR and TNR, the recommended view for binary judges
879
+ * (Husain/Shankar), and a correction for the judge's bias on unlabeled rows.
880
+ * Pure.
881
+ *
882
+ * Build rows from confirmed labels only; draft labels gate nothing. Group by
883
+ * decision state or request name so each narrow judge is aligned on its own.
884
+ * A row with no group key counts in the totals and in no group. Run it on a
885
+ * dev split and report the held-out split separately.
886
+ *
887
+ * Throws `AgentError` with code `'empty-agreement-rows'` when `rows` is empty.
888
+ *
889
+ * @example
890
+ * ```ts no-check
891
+ * const report = judgeAgreement(
892
+ * labeled.map((s) => ({ id: s.id, human: s.label, judge: verdicts[s.id], group: s.request })),
893
+ * { positive: 'fail' },
894
+ * );
895
+ * report.binary; // { tpr: 0.9, tnr: 0.8 }
896
+ * report.correct?.(0.3); // estimated true failure rate in production
897
+ * ```
898
+ */
899
+ function judgeAgreement(rows, options = {}) {
900
+ if (rows.length === 0) throw new require_event_log.AgentError("empty-agreement-rows", "judgeAgreement: rows must contain at least one row.");
901
+ const report = summarize(rows, options.positive);
902
+ const groupOf = options.groupBy ?? ((row) => row.group);
903
+ if (options.groupBy || rows.some((row) => row.group !== void 0)) {
904
+ const grouped = /* @__PURE__ */ new Map();
905
+ for (const row of rows) {
906
+ const key = groupOf(row);
907
+ if (key === void 0) continue;
908
+ const members = grouped.get(key);
909
+ if (members) members.push(row);
910
+ else grouped.set(key, [row]);
911
+ }
912
+ const groups = Object.create(null);
913
+ for (const [key, members] of grouped) groups[key] = summarize(members, options.positive);
914
+ report.groups = groups;
915
+ }
916
+ return report;
917
+ }
918
+ function summarize(rows, positive) {
919
+ const perLabel = Object.create(null);
920
+ const confusion = Object.create(null);
921
+ const disagreements = [];
922
+ let agreed = 0;
923
+ for (const { id, human, judge } of rows) {
924
+ const label = perLabel[human] ??= {
925
+ human: 0,
926
+ agreed: 0
927
+ };
928
+ const row = confusion[human] ??= Object.create(null);
929
+ label.human++;
930
+ row[judge] = (row[judge] ?? 0) + 1;
931
+ if (human === judge) {
932
+ label.agreed++;
933
+ agreed++;
934
+ } else disagreements.push({
935
+ id,
936
+ human,
937
+ judge
938
+ });
939
+ }
940
+ const summary = {
941
+ total: rows.length,
942
+ accuracy: agreed / rows.length,
943
+ perLabel,
944
+ confusion,
945
+ disagreements
946
+ };
947
+ if (positive === void 0) return summary;
948
+ let positives = 0;
949
+ let truePositives = 0;
950
+ let negatives = 0;
951
+ let trueNegatives = 0;
952
+ for (const { human, judge } of rows) if (human === positive) {
953
+ positives++;
954
+ if (judge === positive) truePositives++;
955
+ } else {
956
+ negatives++;
957
+ if (judge !== positive) trueNegatives++;
958
+ }
959
+ const tpr = positives === 0 ? void 0 : truePositives / positives;
960
+ const tnr = negatives === 0 ? void 0 : trueNegatives / negatives;
961
+ summary.binary = {
962
+ tpr,
963
+ tnr
964
+ };
965
+ if (tpr !== void 0 && tnr !== void 0 && tpr + tnr > 1) summary.correct = (p) => Math.min(1, Math.max(0, (p + tnr - 1) / (tpr + tnr - 1)));
966
+ return summary;
967
+ }
968
+ //#endregion
484
969
  //#region src/compare-runs.ts
485
970
  /**
486
971
  * Runs every scenario through a baseline and a candidate implementation and
@@ -592,9 +1077,14 @@ exports.assertAgentMachine = require_verify.assertAgentMachine;
592
1077
  exports.canReach = require_verify.canReach;
593
1078
  exports.compareRuns = compareRuns;
594
1079
  exports.explorePaths = require_verify.explorePaths;
1080
+ exports.firstDivergence = firstDivergence;
595
1081
  exports.foldTraces = foldTraces;
596
1082
  exports.getDecisionRequests = getDecisionRequests;
1083
+ exports.judgeAgreement = judgeAgreement;
597
1084
  exports.lintAgentMachine = require_verify.lintAgentMachine;
598
1085
  exports.matchesTrajectory = matchesTrajectory;
1086
+ exports.repeatRuns = repeatRuns;
599
1087
  exports.runSeam = runSeam;
600
1088
  exports.simulateAgent = require_verify.simulateAgent;
1089
+ exports.summarizeRepeats = summarizeRepeats;
1090
+ exports.traceCoverage = traceCoverage;