stageflow 0.44.0 → 0.46.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.
Files changed (83) hide show
  1. package/dist/agents/evalGate.js +1 -0
  2. package/dist/cli/evalCommand.d.ts +1 -1
  3. package/dist/cli/evalCommand.js +39 -2
  4. package/dist/cli/hostClient.d.ts +6 -0
  5. package/dist/cli/hostClient.js +4 -0
  6. package/dist/config/loadEval.d.ts +3 -1
  7. package/dist/config/loadEval.js +75 -7
  8. package/dist/config/validateEval.js +17 -1
  9. package/dist/evals/calibration.d.ts +21 -0
  10. package/dist/evals/calibration.js +69 -0
  11. package/dist/evals/calibrationSuggest.d.ts +29 -0
  12. package/dist/evals/calibrationSuggest.js +72 -0
  13. package/dist/evals/calibrator.d.ts +46 -0
  14. package/dist/evals/calibrator.js +148 -0
  15. package/dist/evals/compare.d.ts +1 -1
  16. package/dist/evals/compare.js +3 -0
  17. package/dist/evals/constants.d.ts +4 -2
  18. package/dist/evals/constants.js +4 -0
  19. package/dist/evals/engine/FakeEngine.d.ts +1 -0
  20. package/dist/evals/engine/FakeEngine.js +7 -2
  21. package/dist/evals/engine/promptfoo/PromptfooEngine.js +2 -1
  22. package/dist/evals/engine/promptfoo/generate.d.ts +2 -0
  23. package/dist/evals/engine/promptfoo/generate.js +33 -16
  24. package/dist/evals/engine/promptfoo/judgeProviders.js +2 -1
  25. package/dist/evals/engine/promptfoo/normalize.d.ts +4 -1
  26. package/dist/evals/engine/promptfoo/normalize.js +16 -3
  27. package/dist/evals/engine/promptfoo/rubricPrompt.d.ts +2 -0
  28. package/dist/evals/engine/promptfoo/rubricPrompt.js +10 -2
  29. package/dist/evals/errors.d.ts +7 -0
  30. package/dist/evals/errors.js +10 -0
  31. package/dist/evals/judgeProviders.d.ts +6 -0
  32. package/dist/evals/judgeProviders.js +14 -0
  33. package/dist/evals/judged.d.ts +7 -0
  34. package/dist/evals/judged.js +7 -0
  35. package/dist/evals/manager.d.ts +14 -8
  36. package/dist/evals/manager.js +63 -10
  37. package/dist/evals/plan.d.ts +2 -0
  38. package/dist/evals/plan.js +5 -4
  39. package/dist/evals/planInternalAgent.d.ts +1 -1
  40. package/dist/evals/planInternalAgent.js +2 -6
  41. package/dist/evals/read/api.d.ts +6 -0
  42. package/dist/evals/read/api.js +5 -1
  43. package/dist/evals/read/labelQueue.d.ts +45 -0
  44. package/dist/evals/read/labelQueue.js +83 -0
  45. package/dist/evals/stats.d.ts +40 -0
  46. package/dist/evals/stats.js +69 -0
  47. package/dist/evals/storeTypes.d.ts +42 -0
  48. package/dist/evals/types.d.ts +13 -0
  49. package/dist/evals/votes.d.ts +10 -0
  50. package/dist/evals/votes.js +60 -0
  51. package/dist/mcp/evalTools.js +39 -0
  52. package/dist/operatorAgent/profiles/guideConfirm.js +1 -1
  53. package/dist/operatorAgent/profiles/stageflowGuide.d.ts +1 -1
  54. package/dist/operatorAgent/profiles/stageflowGuide.js +8 -7
  55. package/dist/package-meta.d.ts +1 -1
  56. package/dist/package-meta.js +1 -1
  57. package/dist/runstore/port.d.ts +13 -1
  58. package/dist/runstore/sqlite/SqliteRunStore.d.ts +10 -1
  59. package/dist/runstore/sqlite/SqliteRunStore.js +25 -0
  60. package/dist/runstore/sqlite/calibrationStore.d.ts +10 -0
  61. package/dist/runstore/sqlite/calibrationStore.js +106 -0
  62. package/dist/runstore/sqlite/evalStore.d.ts +5 -0
  63. package/dist/runstore/sqlite/evalStore.js +7 -0
  64. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.d.ts +11 -0
  65. package/dist/runstore/sqlite/migrations/018-eval-grader-labels.js +43 -0
  66. package/dist/runstore/sqlite/migrations/index.js +2 -0
  67. package/dist/server/evalRoutes.js +213 -3
  68. package/dist/toolkit/meta.js +2 -0
  69. package/dist/ui/assets/{MarkdownText-S5C_vemc.js → MarkdownText-BS2BBbCx.js} +1 -1
  70. package/dist/ui/assets/adapters-DPD-cAsy.js +17 -0
  71. package/dist/ui/assets/chat-BD1-VTT6.js +2 -0
  72. package/dist/ui/assets/chat-DXnQv9st.css +2 -0
  73. package/dist/ui/assets/main-AX5XNLtX.js +278 -0
  74. package/dist/ui/assets/{main-Del1GeLf.css → main-CFX0_n8J.css} +1 -1
  75. package/dist/ui/chat.html +3 -3
  76. package/dist/ui/index.html +3 -3
  77. package/package.json +1 -1
  78. package/skills/stageflow/references/control-surface.md +1 -1
  79. package/skills/stageflow/references/evals.md +1 -0
  80. package/dist/ui/assets/adapters-C0l4ruiR.js +0 -17
  81. package/dist/ui/assets/chat-CjnsmvqR.js +0 -2
  82. package/dist/ui/assets/chat-DFDd4UjS.css +0 -2
  83. package/dist/ui/assets/main-_yXVqcW9.js +0 -277
@@ -10,19 +10,14 @@ import { scoreResult } from "./scoring.js";
10
10
  import { computeStats } from "./stats.js";
11
11
  import { toGradeRecords, toResultRecord } from "./statsAdapter.js";
12
12
  import { isConversationTarget } from "./types.js";
13
+ import { isJudged } from "./judged.js";
14
+ import { measureCalibration } from "./calibrator.js";
15
+ import { EvalRunError } from "./errors.js";
16
+ export { CALIBRATION_DEFAULT_MAX_COST_USD } from "./calibrator.js";
17
+ export { EvalRunError };
13
18
  export const EVAL_HOST_STOPPED_REASON = "Host stopped while the run was in progress";
14
19
  export const EVAL_DEFAULT_ENGINE = "promptfoo";
15
20
  export const EVAL_ORPHAN_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
16
- export class EvalRunError extends Error {
17
- code;
18
- findings;
19
- constructor(code, message, findings) {
20
- super(message);
21
- this.code = code;
22
- this.findings = findings;
23
- this.name = "EvalRunError";
24
- }
25
- }
26
21
  const key = (caseId, repeatIndex) => `${caseId}:${repeatIndex}`;
27
22
  function isRecord(value) {
28
23
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -96,6 +91,7 @@ export class EvalRunManager {
96
91
  reserved = false;
97
92
  trying = false;
98
93
  tryingRunId = null;
94
+ calibrationCancel = null;
99
95
  constructor(deps) {
100
96
  this.deps = deps;
101
97
  this.store = deps.store;
@@ -542,6 +538,10 @@ export class EvalRunManager {
542
538
  }
543
539
  }
544
540
  async cancel(evalRunId) {
541
+ if (this.calibrationCancel !== null && this.tryingRunId === evalRunId) {
542
+ this.calibrationCancel.abort();
543
+ return true;
544
+ }
545
545
  const live = this.live;
546
546
  if (live === undefined || live.evalRunId !== evalRunId) {
547
547
  if ((await this.store.getEvalRun(evalRunId)) === null) {
@@ -676,6 +676,59 @@ export class EvalRunManager {
676
676
  this.deps.logger?.warn("eval.try_session_cleanup_failed", safeMessage(err));
677
677
  }
678
678
  }
679
+ logEngine(level, message) {
680
+ if (level === "info")
681
+ this.deps.logger?.info?.("eval.engine", message);
682
+ else
683
+ this.deps.logger?.[level]("eval.engine", message);
684
+ }
685
+ /**
686
+ * Re-judges the stored outputs of results the author labeled for one grader and compares the verdicts to the
687
+ * labels. No stage or agent runs: the engine's case callback replays the stored output. Nothing is added to the
688
+ * run list; the measurement is stored as a calibration.
689
+ */
690
+ async calibrate(input) {
691
+ this.claim();
692
+ this.reserved = false;
693
+ this.trying = true;
694
+ const evalRunId = `${EVAL_RUN_ID_PREFIX}${randomBytes(6).toString("hex")}`;
695
+ this.tryingRunId = evalRunId;
696
+ const abort = new AbortController();
697
+ const cancel = new AbortController();
698
+ this.calibrationCancel = cancel;
699
+ const relay = () => cancel.abort();
700
+ if (input.signal?.aborted)
701
+ relay();
702
+ input.signal?.addEventListener("abort", relay, { once: true });
703
+ try {
704
+ const engine = this.engines(this.engineName);
705
+ const engineInfo = { name: engine.name, version: await engine.version(), nodeVersion: process.versions.node };
706
+ const built = await buildEvalPlan({
707
+ projectRoot: input.projectRoot,
708
+ evalPath: input.evalPath,
709
+ overrides: {},
710
+ evalRunId,
711
+ engine: engineInfo,
712
+ ...(this.deps.isKnownModel ? { isKnownModel: this.deps.isKnownModel } : {}),
713
+ ...this.agentsInput(),
714
+ });
715
+ if (!built.ok)
716
+ throw new EvalRunError("EVAL_INVALID", "Eval is invalid", built.findings);
717
+ const base = built.plan;
718
+ const found = base.graders.find((g) => g.id === input.graderId);
719
+ if (!found || !isJudged(found)) {
720
+ throw new EvalRunError("EVAL_INVALID", `Grader ${input.graderId} is not a judged grader of this eval`);
721
+ }
722
+ return await measureCalibration({ store: this.store, engine, log: (level, message) => this.logEngine(level, message) }, input, base, found, abort, cancel.signal);
723
+ }
724
+ finally {
725
+ input.signal?.removeEventListener("abort", relay);
726
+ this.calibrationCancel = null;
727
+ this.trying = false;
728
+ this.tryingRunId = null;
729
+ await this.cleanupWorkDir(evalRunId);
730
+ }
731
+ }
679
732
  async tryOne(input) {
680
733
  this.claim();
681
734
  this.reserved = false;
@@ -1,3 +1,4 @@
1
+ import { type LoadedEval } from "../config/loadEval.js";
1
2
  import { type AgentCatalogPort } from "../config/resolveAgentTarget.js";
2
3
  import type { ValidationFinding } from "../config/validateCatalog.js";
3
4
  import { type ConfigComponents, type ConfigSnapshot } from "./snapshot.js";
@@ -62,4 +63,5 @@ export type BuildEvalPlanResult = {
62
63
  code: "EVAL_INVALID";
63
64
  findings: ValidationFinding[];
64
65
  };
66
+ export declare function evalDefinition(loaded: LoadedEval, projectRoot: string): Record<string, unknown>;
65
67
  export declare function buildEvalPlan(input: BuildEvalPlanInput): Promise<BuildEvalPlanResult>;
@@ -13,6 +13,7 @@ import { EVAL_LIMITS, SIMULATOR_PROMPT_VERSION } from "./constants.js";
13
13
  import { buildComponents, buildSnapshot, computeFingerprint, } from "./snapshot.js";
14
14
  import { planInternalAgent } from "./planInternalAgent.js";
15
15
  import { isConversationTarget } from "./types.js";
16
+ import { isJudged, isJudgedType } from "./judged.js";
16
17
  const SELECTION_KEYS = ["case_ids", "tags", "only_failed_from"];
17
18
  const RUN_SIZE_CODES = new Set(["eval.large_run", "eval.too_large_run"]);
18
19
  function posix(projectRoot, abs) {
@@ -106,10 +107,10 @@ function withOverrides(loaded, overrides) {
106
107
  ...(overrides.max_cost_usd !== undefined ? { max_cost_usd: overrides.max_cost_usd } : {}),
107
108
  };
108
109
  const graders = loaded.graders.map((grader) => {
109
- if ((grader.type !== "rubric" && grader.type !== "goal_reached") || overrides.judge === undefined)
110
+ if (!isJudged(grader) || overrides.judge === undefined)
110
111
  return grader;
111
112
  const raw = loaded.file.graders.find((g) => g.id === grader.id);
112
- if ((raw?.type === "rubric" || raw?.type === "goal_reached") && raw.judge !== undefined)
113
+ if (raw !== undefined && isJudgedType(raw.type) && "judge" in raw && raw.judge !== undefined)
113
114
  return grader;
114
115
  const { hash: _old, ...rest } = { ...grader, judge: overrides.judge };
115
116
  return { ...rest, hash: graderHash(rest) };
@@ -133,7 +134,7 @@ function selectCases(cases, overrides, failedIds) {
133
134
  return cases.filter((c) => failedIds.has(c.id));
134
135
  return [...cases];
135
136
  }
136
- function evalDefinition(loaded, projectRoot) {
137
+ export function evalDefinition(loaded, projectRoot) {
137
138
  const { file } = loaded;
138
139
  const paths = resolveEvalPaths(file, loaded.path);
139
140
  const rel = (abs) => abs === undefined ? undefined : posix(projectRoot, abs);
@@ -508,7 +509,7 @@ async function buildAgentResult(a) {
508
509
  });
509
510
  const fingerprint = computeFingerprint(components);
510
511
  const { overrides } = a;
511
- const hasRubric = loaded.graders.some((g) => g.type === "rubric" || g.type === "goal_reached");
512
+ const hasRubric = loaded.graders.some(isJudged);
512
513
  const plainDefinition = JSON.parse(JSON.stringify(definition));
513
514
  const snapshot = buildSnapshot({
514
515
  evalId: loaded.id,
@@ -1,7 +1,7 @@
1
1
  import type { DatasetCase } from "../config/datasetTypes.js";
2
2
  import { type LoadedEval } from "../config/loadEval.js";
3
3
  import type { ValidationFinding } from "../config/validateCatalog.js";
4
- import type { BuildEvalPlanInput, BuildEvalPlanResult, EvalOverrides } from "./plan.js";
4
+ import { type BuildEvalPlanInput, type BuildEvalPlanResult, type EvalOverrides } from "./plan.js";
5
5
  type Dataset = {
6
6
  path: string;
7
7
  contentHash: string;
@@ -5,6 +5,7 @@ import { EVAL_LARGE_RUN, EVAL_TOO_LARGE_RUN, evalFinding } from "../config/valid
5
5
  import { PACKAGE_VERSION } from "../package-meta.js";
6
6
  import { sha256Hex } from "./canonical.js";
7
7
  import { EVAL_LIMITS, SIMULATOR_PROMPT_VERSION } from "./constants.js";
8
+ import { evalDefinition } from "./plan.js";
8
9
  import { buildComponents, buildSnapshot, computeFingerprint } from "./snapshot.js";
9
10
  function posix(projectRoot, abs) {
10
11
  return path.relative(projectRoot, abs).split(path.sep).join("/");
@@ -127,12 +128,7 @@ export function planInternalAgent(args) {
127
128
  evalId: loaded.id,
128
129
  evalPath: loaded.relPath,
129
130
  evalHash: loaded.fileHash,
130
- evalDefinition: {
131
- version: 1,
132
- id: loaded.id,
133
- target: { kind: "internal_agent", profile: profileId },
134
- dataset: posix(projectRoot, paths.datasetPath),
135
- },
131
+ evalDefinition: evalDefinition(loaded, projectRoot),
136
132
  overrides: snapshotOverrides,
137
133
  target: {
138
134
  profile_id: profileId,
@@ -159,6 +159,12 @@ export declare function buildResultDetail(store: RunStore, id: string, caseId: s
159
159
  } | null;
160
160
  tags: string[];
161
161
  suggestion: import("./suggestions.js").WireSuggestion | null;
162
+ grader_labels: {
163
+ [k: string]: {
164
+ label: "fail" | "pass";
165
+ note: string | null;
166
+ };
167
+ };
162
168
  }[];
163
169
  }>;
164
170
  export declare function loadEvalListItems(input: {
@@ -227,11 +227,12 @@ export async function buildResultsPage(store, id, input) {
227
227
  }
228
228
  export async function buildResultDetail(store, id, caseId) {
229
229
  const row = await runByIdOr404(store, id);
230
- const [cases, results, reviews, suggestions] = await Promise.all([
230
+ const [cases, results, reviews, suggestions, graderLabels] = await Promise.all([
231
231
  store.getEvalCases(row.evalRunId),
232
232
  store.getEvalResultsForCase(row.evalRunId, caseId),
233
233
  store.getEvalReviews(row.evalRunId),
234
234
  store.listEvalSuggestions(row.evalRunId),
235
+ store.listEvalGraderLabels(row.evalRunId),
235
236
  ]);
236
237
  const caseRow = cases.find((x) => x.caseId === caseId);
237
238
  if (!caseRow || results.length === 0) {
@@ -245,6 +246,9 @@ export async function buildResultDetail(store, id, caseId) {
245
246
  .map((r) => ({
246
247
  ...resultToWire(r, reviewByKey.get(resultKey(r.caseId, r.repeatIndex)), row.targetKind === "agent" ? (row.targetRef.agent_id ?? "") : undefined),
247
248
  suggestion: suggestionFor(suggestions, r.caseId, r.repeatIndex),
249
+ grader_labels: Object.fromEntries(graderLabels
250
+ .filter((l) => l.caseId === r.caseId && l.repeatIndex === r.repeatIndex)
251
+ .map((l) => [l.graderId, { label: l.label, note: l.note }])),
248
252
  })),
249
253
  };
250
254
  }
@@ -0,0 +1,45 @@
1
+ import type { RunStore } from "../../runstore/port.js";
2
+ export type LabelCounts = {
3
+ pass: number;
4
+ fail: number;
5
+ };
6
+ /**
7
+ * Everything the console's labeling screen needs in one read: each result the judge graded for one grader, with the
8
+ * conversation, what the case expects, the judge's answer and the author's label so far. Results the judge errored on
9
+ * are left out (there is no verdict to compare with), and so are results that never ran.
10
+ */
11
+ export declare function buildLabelQueue(store: RunStore, id: string, graderId: string): Promise<{
12
+ grader_id: string;
13
+ items: {
14
+ case: {
15
+ position: number;
16
+ };
17
+ result: import("./runs.js").WireResultDetail;
18
+ judge: {
19
+ passed: boolean;
20
+ score: number | null;
21
+ reason: string | null;
22
+ };
23
+ label: {
24
+ label: "fail" | "pass";
25
+ note: string | null;
26
+ } | null;
27
+ }[];
28
+ labeled_here: LabelCounts;
29
+ labeled_eval: LabelCounts;
30
+ }>;
31
+ /**
32
+ * The label counts across the eval's runs, and which results of this run the judge graded that still have no label
33
+ * from you. No conversations: this is for a progress line and the Cases filter.
34
+ */
35
+ export declare function buildLabelSummary(store: RunStore, id: string, graderId: string): Promise<{
36
+ grader_id: string;
37
+ labeled_eval: {
38
+ pass: number;
39
+ fail: number;
40
+ };
41
+ unlabeled: {
42
+ case_id: string;
43
+ repeat_index: number;
44
+ }[];
45
+ }>;
@@ -0,0 +1,83 @@
1
+ import { resultKey } from "../statsTypes.js";
2
+ import { EvalApiError, runByIdOr404 } from "./api.js";
3
+ import { resultToWire } from "./runs.js";
4
+ import { isJudgedType } from "../judged.js";
5
+ /**
6
+ * Everything the console's labeling screen needs in one read: each result the judge graded for one grader, with the
7
+ * conversation, what the case expects, the judge's answer and the author's label so far. Results the judge errored on
8
+ * are left out (there is no verdict to compare with), and so are results that never ran.
9
+ */
10
+ export async function buildLabelQueue(store, id, graderId) {
11
+ const row = await runByIdOr404(store, id);
12
+ const [cases, results, labels, evalLabels] = await Promise.all([
13
+ store.getEvalCases(row.evalRunId),
14
+ store.listEvalResults(row.evalRunId),
15
+ store.listEvalGraderLabels(row.evalRunId),
16
+ store.listEvalGraderLabelsForEval(row.projectRoot, row.evalId, graderId),
17
+ ]);
18
+ const caseById = new Map(cases.map((c) => [c.caseId, c]));
19
+ const labelByKey = new Map(labels.filter((l) => l.graderId === graderId).map((l) => [resultKey(l.caseId, l.repeatIndex), l]));
20
+ const gradeOf = (grades) => grades.find((g) => g.graderId === graderId);
21
+ const known = results.some((r) => gradeOf(r.grades) !== undefined);
22
+ if (!known) {
23
+ throw new EvalApiError(404, "EVAL_GRADER_NOT_FOUND", `Grader ${graderId} has no grades in run ${id}`);
24
+ }
25
+ const judged = results.find((r) => {
26
+ const g = gradeOf(r.grades);
27
+ return g !== undefined && isJudgedType(g.graderType);
28
+ });
29
+ if (!judged) {
30
+ throw new EvalApiError(400, "invalid_request", `Grader ${graderId} is not judged by a model, so it has nothing to label`);
31
+ }
32
+ const items = [...results]
33
+ .sort((a, b) => (caseById.get(a.caseId)?.position ?? 0) - (caseById.get(b.caseId)?.position ?? 0) || a.repeatIndex - b.repeatIndex)
34
+ .flatMap((r) => {
35
+ const grade = gradeOf(r.grades);
36
+ const caseRow = caseById.get(r.caseId);
37
+ if (!grade || grade.status !== "graded" || !caseRow)
38
+ return [];
39
+ const label = labelByKey.get(resultKey(r.caseId, r.repeatIndex));
40
+ return [
41
+ {
42
+ case: { ...caseRow.case, position: caseRow.position },
43
+ result: resultToWire(r, undefined),
44
+ judge: { passed: grade.passed === true, score: grade.score, reason: grade.reason },
45
+ label: label ? { label: label.label, note: label.note } : null,
46
+ },
47
+ ];
48
+ });
49
+ const count = (rows) => ({
50
+ pass: rows.filter((l) => l.label === "pass").length,
51
+ fail: rows.filter((l) => l.label === "fail").length,
52
+ });
53
+ return {
54
+ grader_id: graderId,
55
+ items,
56
+ labeled_here: count(labels.filter((l) => l.graderId === graderId)),
57
+ labeled_eval: count(evalLabels),
58
+ };
59
+ }
60
+ /**
61
+ * The label counts across the eval's runs, and which results of this run the judge graded that still have no label
62
+ * from you. No conversations: this is for a progress line and the Cases filter.
63
+ */
64
+ export async function buildLabelSummary(store, id, graderId) {
65
+ const row = await runByIdOr404(store, id);
66
+ const [labels, runLabels, graded] = await Promise.all([
67
+ store.listEvalGraderLabelsForEval(row.projectRoot, row.evalId, graderId),
68
+ store.listEvalGraderLabels(row.evalRunId),
69
+ store.listGradedResultKeys(row.evalRunId, graderId),
70
+ ]);
71
+ const labeled = new Set(runLabels.filter((l) => l.graderId === graderId).map((l) => resultKey(l.caseId, l.repeatIndex)));
72
+ const unlabeled = graded
73
+ .filter((r) => !labeled.has(resultKey(r.caseId, r.repeatIndex)))
74
+ .map((r) => ({ case_id: r.caseId, repeat_index: r.repeatIndex }));
75
+ return {
76
+ grader_id: graderId,
77
+ labeled_eval: {
78
+ pass: labels.filter((l) => l.label === "pass").length,
79
+ fail: labels.filter((l) => l.label === "fail").length,
80
+ },
81
+ unlabeled,
82
+ };
83
+ }
@@ -93,3 +93,43 @@ export declare function clusteredCi(center: number, perCase: number[], lo: numbe
93
93
  export declare function buildCaseStats(results: ResultRecord[]): CaseStat[];
94
94
  export declare function computeByGrader(graders: GraderRef[], grades: GradeRecord[], previousRun: PreviousRunLite | null | undefined): GraderStat[];
95
95
  export declare function computeStats(input: ComputeStatsInput): Stats;
96
+ export declare const CALIBRATION_MIN_LABELS = 20;
97
+ export declare const CALIBRATION_MINORITY_SHARE = 0.2;
98
+ export interface CalibrationSample {
99
+ caseId: string;
100
+ repeatIndex: number;
101
+ label: boolean;
102
+ judge: boolean;
103
+ }
104
+ export interface CalibrationStats {
105
+ n: number;
106
+ agreement: number | null;
107
+ cohen_kappa: number | null;
108
+ tpr: number | null;
109
+ tnr: number | null;
110
+ confusion: {
111
+ tp: number;
112
+ fp: number;
113
+ tn: number;
114
+ fn: number;
115
+ };
116
+ warnings: Array<"low_n" | "imbalanced" | "no_variance">;
117
+ }
118
+ /** Judge pass is the positive class; the human label is the truth. */
119
+ export declare function computeCalibration(samples: CalibrationSample[]): CalibrationStats;
120
+ /** A strict majority of valid votes passes; a tie fails. */
121
+ export declare function majorityPasses(passes: number, valid: number): boolean;
122
+ export declare function meanOf(values: readonly number[]): number;
123
+ /** Deterministic 70/30 split by a stable hash of the case id, so every repeat of a case lands on the same side. */
124
+ export declare function calibrationSplitOf(caseId: string): "dev" | "held_out";
125
+ export interface StabilityStats {
126
+ /** Judge calls per output. */
127
+ k: number;
128
+ /** Outputs with at least two valid votes. */
129
+ n: number;
130
+ /** Outputs whose valid votes were not all the same verdict. */
131
+ unstable: number;
132
+ flip_rate: number | null;
133
+ }
134
+ /** How often the judge changes its mind on the same output: the share of outputs with a split vote. */
135
+ export declare function computeStability(voteSets: boolean[][], k: number): StabilityStats;
@@ -218,3 +218,72 @@ export function computeStats(input) {
218
218
  ...(hasAgentData ? { mean_turns: mean(turnValues), mean_tool_calls: mean(toolCallValues) } : {}),
219
219
  };
220
220
  }
221
+ export const CALIBRATION_MIN_LABELS = 20;
222
+ export const CALIBRATION_MINORITY_SHARE = 0.2;
223
+ /** Judge pass is the positive class; the human label is the truth. */
224
+ export function computeCalibration(samples) {
225
+ let tp = 0;
226
+ let fp = 0;
227
+ let tn = 0;
228
+ let fn = 0;
229
+ for (const s of samples) {
230
+ if (s.judge && s.label)
231
+ tp++;
232
+ else if (s.judge && !s.label)
233
+ fp++;
234
+ else if (!s.judge && !s.label)
235
+ tn++;
236
+ else
237
+ fn++;
238
+ }
239
+ const n = tp + fp + tn + fn;
240
+ const warnings = [];
241
+ if (n < CALIBRATION_MIN_LABELS)
242
+ warnings.push("low_n");
243
+ const labelPass = tp + fn;
244
+ const labelFail = fp + tn;
245
+ if (n > 0 && Math.min(labelPass, labelFail) / n < CALIBRATION_MINORITY_SHARE)
246
+ warnings.push("imbalanced");
247
+ const agreement = n > 0 ? (tp + tn) / n : null;
248
+ const judgePass = tp + fp;
249
+ const judgeFail = fn + tn;
250
+ const expected = n > 0 ? (judgePass * labelPass + judgeFail * labelFail) / (n * n) : 0;
251
+ let kappa = null;
252
+ if (n > 0 && agreement !== null) {
253
+ if (expected >= 1)
254
+ warnings.push("no_variance");
255
+ else
256
+ kappa = (agreement - expected) / (1 - expected);
257
+ }
258
+ return {
259
+ n,
260
+ agreement,
261
+ cohen_kappa: kappa,
262
+ tpr: labelPass > 0 ? tp / labelPass : null,
263
+ tnr: labelFail > 0 ? tn / labelFail : null,
264
+ confusion: { tp, fp, tn, fn },
265
+ warnings,
266
+ };
267
+ }
268
+ /** A strict majority of valid votes passes; a tie fails. */
269
+ export function majorityPasses(passes, valid) {
270
+ return passes * 2 > valid;
271
+ }
272
+ export function meanOf(values) {
273
+ return values.reduce((total, v) => total + v, 0) / values.length;
274
+ }
275
+ /** Deterministic 70/30 split by a stable hash of the case id, so every repeat of a case lands on the same side. */
276
+ export function calibrationSplitOf(caseId) {
277
+ let h = 2166136261;
278
+ for (let i = 0; i < caseId.length; i++) {
279
+ h ^= caseId.charCodeAt(i);
280
+ h = Math.imul(h, 16777619) >>> 0;
281
+ }
282
+ return h % 10 < 7 ? "dev" : "held_out";
283
+ }
284
+ /** How often the judge changes its mind on the same output: the share of outputs with a split vote. */
285
+ export function computeStability(voteSets, k) {
286
+ const counted = voteSets.filter((votes) => votes.length >= 2);
287
+ const unstable = counted.filter((votes) => votes.some((v) => v !== votes[0])).length;
288
+ return { k, n: counted.length, unstable, flip_rate: counted.length > 0 ? unstable / counted.length : null };
289
+ }
@@ -205,3 +205,45 @@ export type EvalGraderVersionInput = {
205
205
  graderId: string;
206
206
  graderHash: string;
207
207
  };
208
+ export type EvalGraderLabelRow = {
209
+ evalRunId: string;
210
+ caseId: string;
211
+ repeatIndex: number;
212
+ graderId: string;
213
+ label: EvalReviewLabel;
214
+ note: string | null;
215
+ createdAt: string;
216
+ updatedAt: string;
217
+ };
218
+ export type EvalGraderLabelPatch = {
219
+ label: EvalReviewLabel | null;
220
+ note?: string | null;
221
+ };
222
+ export type EvalCalibrationSplit = "all" | "dev" | "held_out";
223
+ export type EvalCalibrationDisagreement = {
224
+ /** The run the labeled result came from; absent on calibrations stored before this field existed. */
225
+ evalRunId?: string;
226
+ caseId: string;
227
+ repeatIndex: number;
228
+ label: EvalReviewLabel;
229
+ judge: EvalReviewLabel;
230
+ reason: string | null;
231
+ };
232
+ export type EvalCalibrationInput = {
233
+ projectRoot: string;
234
+ evalId: string;
235
+ graderId: string;
236
+ graderHash: string;
237
+ judge: string;
238
+ promptVersion: number;
239
+ split: EvalCalibrationSplit;
240
+ candidate: boolean;
241
+ stats: EvalJson;
242
+ disagreements: EvalCalibrationDisagreement[];
243
+ costUsd: number;
244
+ };
245
+ export type EvalCalibrationRow = Omit<EvalCalibrationInput, "stats"> & {
246
+ id: string;
247
+ stats: EvalJson;
248
+ createdAt: string;
249
+ };
@@ -57,9 +57,18 @@ export type EvalTask = {
57
57
  context?: string;
58
58
  constraints?: string;
59
59
  };
60
+ export type JudgeSettings = {
61
+ /** Sampling temperature for the judge call; unset leaves the provider default. */
62
+ temperature?: number;
63
+ /** The judge writes its reason before the verdict. */
64
+ reasoning?: boolean;
65
+ /** Judge calls per grade (odd, 1 to 5); the majority verdict wins. */
66
+ votes?: number;
67
+ };
60
68
  export type EvalDefaults = {
61
69
  repeats?: number;
62
70
  judge?: string;
71
+ judge_settings?: JudgeSettings;
63
72
  pass_threshold?: number;
64
73
  max_cost_usd?: number;
65
74
  concurrency?: number;
@@ -103,6 +112,7 @@ export type GraderConfig = (GraderCommon & {
103
112
  scale?: "pass_fail" | "1_to_5";
104
113
  pass_at?: number;
105
114
  judge?: string;
115
+ judge_settings?: JudgeSettings;
106
116
  examples?: RubricExample[];
107
117
  }) | (GraderCommon & {
108
118
  type: "code";
@@ -122,6 +132,7 @@ export type GraderConfig = (GraderCommon & {
122
132
  }) | (GraderCommon & {
123
133
  type: "goal_reached";
124
134
  judge?: string;
135
+ judge_settings?: JudgeSettings;
125
136
  guidance?: string;
126
137
  });
127
138
  export type EvalFile = {
@@ -178,6 +189,7 @@ export type ResolvedGrader = (ResolvedGraderCommon & {
178
189
  scale: "pass_fail" | "1_to_5";
179
190
  passAt: number;
180
191
  judge: string;
192
+ judgeSettings?: JudgeSettings;
181
193
  examples: RubricExample[];
182
194
  }) | (ResolvedGraderCommon & {
183
195
  type: "code";
@@ -197,6 +209,7 @@ export type ResolvedGrader = (ResolvedGraderCommon & {
197
209
  }) | (ResolvedGraderCommon & {
198
210
  type: "goal_reached";
199
211
  judge: string;
212
+ judgeSettings?: JudgeSettings;
200
213
  guidance?: string;
201
214
  });
202
215
  export type PlanCase = {
@@ -0,0 +1,10 @@
1
+ import type { NormalizedGrade } from "./types.js";
2
+ export declare const JUDGE_ERROR_CODE = "EVAL_JUDGE_ERROR";
3
+ /**
4
+ * One grade from several votes: the majority verdict for pass/fail (a tie fails), the mean raw score for 1 to 5.
5
+ * Errored votes are left out; if every vote errored the grade is an error. Tokens and cost add up over all votes.
6
+ */
7
+ export declare function combineVotes(grader: {
8
+ scale: "pass_fail" | "1_to_5";
9
+ passAt: number;
10
+ }, votes: readonly NormalizedGrade[]): NormalizedGrade;
@@ -0,0 +1,60 @@
1
+ import { normalizeOneToFive } from "./scoring.js";
2
+ import { majorityPasses, meanOf } from "./stats.js";
3
+ export const JUDGE_ERROR_CODE = "EVAL_JUDGE_ERROR";
4
+ /**
5
+ * One grade from several votes: the majority verdict for pass/fail (a tie fails), the mean raw score for 1 to 5.
6
+ * Errored votes are left out; if every vote errored the grade is an error. Tokens and cost add up over all votes.
7
+ */
8
+ export function combineVotes(grader, votes) {
9
+ const first = votes[0];
10
+ if (votes.length === 1)
11
+ return first;
12
+ const valid = votes.filter((g) => g.status === "graded" && g.passed !== undefined);
13
+ const sum = (pick) => votes.reduce((total, g) => total + (pick(g) ?? 0), 0);
14
+ const totals = {
15
+ judgeTokensIn: sum((g) => g.judgeTokensIn),
16
+ judgeTokensOut: sum((g) => g.judgeTokensOut),
17
+ judgeCostUsd: sum((g) => g.judgeCostUsd),
18
+ };
19
+ const priceUnknown = votes.some((g) => g.details?.judgeCostUnknown);
20
+ const record = votes.map((g) => ({
21
+ status: g.status,
22
+ ...(g.passed !== undefined ? { passed: g.passed } : {}),
23
+ ...(g.rawValue !== undefined ? { raw_value: g.rawValue } : {}),
24
+ ...(g.reason !== undefined ? { reason: g.reason } : {}),
25
+ }));
26
+ const baseDetails = {
27
+ ...(first.details ?? {}),
28
+ ...(priceUnknown ? { judgeCostUnknown: true } : {}),
29
+ votes: record,
30
+ };
31
+ if (valid.length === 0) {
32
+ return { ...first, ...totals, status: "error", errorCode: first.errorCode ?? JUDGE_ERROR_CODE, details: baseDetails };
33
+ }
34
+ let passed;
35
+ let rawValue;
36
+ let score;
37
+ if (grader.scale === "1_to_5") {
38
+ rawValue = meanOf(valid.map((g) => g.rawValue ?? 0));
39
+ score = normalizeOneToFive(rawValue);
40
+ passed = rawValue >= grader.passAt;
41
+ }
42
+ else {
43
+ passed = majorityPasses(valid.filter((g) => g.passed === true).length, valid.length);
44
+ rawValue = passed ? 1 : 0;
45
+ score = rawValue;
46
+ }
47
+ const lead = valid.find((g) => g.passed === passed) ?? valid[0];
48
+ return {
49
+ graderId: first.graderId,
50
+ graderType: first.graderType,
51
+ status: "graded",
52
+ passed,
53
+ score,
54
+ rawValue,
55
+ ...(lead.reason !== undefined ? { reason: lead.reason } : {}),
56
+ ...(first.judgeModel !== undefined ? { judgeModel: first.judgeModel } : {}),
57
+ ...totals,
58
+ details: baseDetails,
59
+ };
60
+ }