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
@@ -9,6 +9,7 @@ const ALWAYS_BLOCKED = new Set([
9
9
  "stageflow.agent_chat",
10
10
  "stageflow.fire_trigger",
11
11
  "stageflow.sort_eval_failures",
12
+ "stageflow.calibrate_eval_grader",
12
13
  "stageflow.cancel_eval_run",
13
14
  ]);
14
15
  const BLOCKED_UNLESS_REAL = new Set(["stageflow.probe_project_mcp"]);
@@ -1,6 +1,6 @@
1
1
  import { type RunNpm } from "../evals/engine/promptfoo/install.js";
2
2
  import { type EnsureGlobalServiceResult } from "../server/ensureGlobalService.js";
3
- export declare const EVAL_USAGE = "Usage:\n sf eval list [--json]\n sf eval show <id|path> [--json]\n sf eval validate [<id|path>] [--json] [--strict]\n sf eval run <id|path> [--repeats <n>] [--judge <model>] [--model <id>] [--user-model <model>] [--allow-real-tools] [--max-cost <usd>] [--case <id> ...] [--tag <tag> ...] [--label <text>] [--no-wait] [--json]\n sf eval runs [<evalId>] [--json]\n sf eval runs show <evalRunId> [--json]\n sf eval runs cancel <evalRunId>\n sf eval runs delete <evalRunId>\n sf eval sort <evalRunId> [--classifier auto|jev|judge] [--limit <n>] [--resort] [--json]\n sf eval suggestions <evalRunId> [--status pending|accepted|dismissed] [--json]\n sf eval compare <evalRunIdA> <evalRunIdB> [--json]\n sf eval datasets [--json]\n sf eval engine status [--json]\n sf eval engine install\n\n Agent evals: --model overrides the agent's model, --user-model the model that plays the user. An eval whose\n conversation.tools is real also needs --allow-real-tools.\n\n Every subcommand also accepts --project-root <id-or-path>.";
3
+ export declare const EVAL_USAGE = "Usage:\n sf eval list [--json]\n sf eval show <id|path> [--json]\n sf eval validate [<id|path>] [--json] [--strict]\n sf eval run <id|path> [--repeats <n>] [--judge <model>] [--model <id>] [--user-model <model>] [--allow-real-tools] [--max-cost <usd>] [--case <id> ...] [--tag <tag> ...] [--label <text>] [--no-wait] [--json]\n sf eval runs [<evalId>] [--json]\n sf eval runs show <evalRunId> [--json]\n sf eval runs cancel <evalRunId>\n sf eval runs delete <evalRunId>\n sf eval sort <evalRunId> [--classifier auto|jev|judge] [--limit <n>] [--resort] [--json]\n sf eval calibrate <evalId> <graderId> [--split all|dev|held_out] [--max-cost <usd>] [--stability] [--json]\n sf eval suggestions <evalRunId> [--status pending|accepted|dismissed] [--json]\n sf eval compare <evalRunIdA> <evalRunIdB> [--json]\n sf eval datasets [--json]\n sf eval engine status [--json]\n sf eval engine install\n\n Agent evals: --model overrides the agent's model, --user-model the model that plays the user. An eval whose\n conversation.tools is real also needs --allow-real-tools.\n\n Every subcommand also accepts --project-root <id-or-path>.";
4
4
  export type EvalCommandIo = {
5
5
  log: (line: string) => void;
6
6
  error: (line: string) => void;
@@ -9,7 +9,7 @@ import { installPromptfoo } from "../evals/engine/promptfoo/install.js";
9
9
  import { engineStatus } from "../evals/engine/promptfoo/status.js";
10
10
  import { globalStageflowHome } from "../project/globalHome.js";
11
11
  import { ensureGlobalService, hostBaseUrl, } from "../server/ensureGlobalService.js";
12
- import { httpCancelEvalRun, httpCompareEvalRuns, httpDeleteEvalRun, httpEnsureProject, httpGetEval, httpGetEvalRun, httpListDatasets, httpListEvalRuns, httpListEvalSuggestions, httpSortEvalFailures, httpListEvals, httpStartEvalRun, } from "./hostClient.js";
12
+ import { httpCancelEvalRun, httpCompareEvalRuns, httpDeleteEvalRun, httpEnsureProject, httpGetEval, httpGetEvalRun, httpListDatasets, httpListEvalRuns, httpListEvalSuggestions, httpCalibrateEvalGrader, httpSortEvalFailures, httpListEvals, httpStartEvalRun, } from "./hostClient.js";
13
13
  import { resolveCliProjectRootLocal } from "./resolveCliProjectRoot.js";
14
14
  import { exitCodeForValidation, formatValidationHuman, formatValidationJson, } from "./validateOutput.js";
15
15
  export const EVAL_USAGE = `Usage:
@@ -22,6 +22,7 @@ export const EVAL_USAGE = `Usage:
22
22
  sf eval runs cancel <evalRunId>
23
23
  sf eval runs delete <evalRunId>
24
24
  sf eval sort <evalRunId> [--classifier auto|jev|judge] [--limit <n>] [--resort] [--json]
25
+ sf eval calibrate <evalId> <graderId> [--split all|dev|held_out] [--max-cost <usd>] [--stability] [--json]
25
26
  sf eval suggestions <evalRunId> [--status pending|accepted|dismissed] [--json]
26
27
  sf eval compare <evalRunIdA> <evalRunIdB> [--json]
27
28
  sf eval datasets [--json]
@@ -61,6 +62,7 @@ const FLAGS = {
61
62
  runs: {},
62
63
  sort: { "--classifier": { value: true }, "--limit": { value: true }, "--resort": {} },
63
64
  suggestions: { "--status": { value: true } },
65
+ calibrate: { "--split": { value: true }, "--max-cost": { value: true }, "--stability": {} },
64
66
  compare: {},
65
67
  datasets: {},
66
68
  engine: {},
@@ -76,6 +78,7 @@ function parseArgs(command, rest) {
76
78
  noWait: false,
77
79
  allowRealTools: false,
78
80
  resort: false,
81
+ stability: false,
79
82
  cases: [],
80
83
  tags: [],
81
84
  positionals: [],
@@ -150,6 +153,15 @@ function parseArgs(command, rest) {
150
153
  case "--resort":
151
154
  parsed.resort = true;
152
155
  break;
156
+ case "--stability":
157
+ parsed.stability = true;
158
+ break;
159
+ case "--split":
160
+ if (value !== "all" && value !== "dev" && value !== "held_out") {
161
+ throw new UsageError("--split must be all, dev or held_out");
162
+ }
163
+ parsed.split = value;
164
+ break;
153
165
  case "--status":
154
166
  if (value !== "pending" && value !== "accepted" && value !== "dismissed") {
155
167
  throw new UsageError("--status must be pending, accepted or dismissed");
@@ -404,7 +416,9 @@ export async function runEvalCommand(args, options = {}) {
404
416
  ? expectArgs(2, 2, "<evalRunIdA> <evalRunIdB>")
405
417
  : command === "sort" || command === "suggestions"
406
418
  ? expectArgs(1, 1, "<evalRunId>")
407
- : null;
419
+ : command === "calibrate"
420
+ ? expectArgs(2, 2, "<evalId> <graderId>")
421
+ : null;
408
422
  if (argError)
409
423
  return usageError(argError);
410
424
  let runsSub;
@@ -554,6 +568,29 @@ export async function runEvalCommand(args, options = {}) {
554
568
  out.log(`Review and accept in the console, or POST /api/eval-runs/${positionals[0]}/failures/suggestions/accept`);
555
569
  });
556
570
  }
571
+ case "calibrate": {
572
+ const result = await httpCalibrateEvalGrader(base, positionals[0], positionals[1], {
573
+ project_root: wireRoot,
574
+ ...(parsed.split !== undefined ? { split: parsed.split } : {}),
575
+ ...(parsed.maxCost !== undefined ? { max_cost_usd: parsed.maxCost } : {}),
576
+ ...(parsed.stability ? { stability: true } : {}),
577
+ });
578
+ return show(result, (b) => {
579
+ const st = b.stats;
580
+ const num = (v) => (typeof v === "number" ? v.toFixed(2) : "n/a");
581
+ out.log(`${b.grader_id} (${b.judge}): ${st.n} labels, agreement ${num(st.agreement)}, kappa ${num(st.cohen_kappa)}, pass recall ${num(st.tpr)}, fail recall ${num(st.tnr)}; cost ${usd(b.cost_usd)}`);
582
+ const cm = st.confusion;
583
+ out.log(`judge pass / human pass ${cm.tp}\tjudge pass / human fail ${cm.fp}\tjudge fail / human fail ${cm.tn}\tjudge fail / human pass ${cm.fn}`);
584
+ if (st.stability) {
585
+ out.log(`stability over ${st.stability.k} judge calls per output: ${st.stability.unstable} of ${st.stability.n} outputs split (flip rate ${num(st.stability.flip_rate)})`);
586
+ }
587
+ for (const w of st.warnings)
588
+ out.log(`warning: ${w}`);
589
+ for (const d of b.disagreements) {
590
+ out.log(`${d.case_id}#${d.repeat_index}\thuman ${d.label}\tjudge ${d.judge}\t${d.reason ?? ""}`);
591
+ }
592
+ });
593
+ }
557
594
  case "suggestions":
558
595
  return show(await httpListEvalSuggestions(base, positionals[0], parsed.status), (b) => {
559
596
  for (const s of b.items)
@@ -119,6 +119,12 @@ export declare function httpSortEvalFailures(base: string, evalRunId: string, in
119
119
  only_unsorted?: boolean;
120
120
  limit?: number;
121
121
  }): Promise<HttpEvalResult<Record<string, any>>>;
122
+ export declare function httpCalibrateEvalGrader(base: string, evalId: string, graderId: string, input: {
123
+ project_root?: string;
124
+ split?: "all" | "dev" | "held_out";
125
+ max_cost_usd?: number;
126
+ stability?: boolean;
127
+ }): Promise<HttpEvalResult<Record<string, any>>>;
122
128
  export declare function httpListEvalSuggestions(base: string, evalRunId: string, status?: string): Promise<HttpEvalResult<{
123
129
  items: Array<Record<string, any>>;
124
130
  summary: Record<string, any>;
@@ -342,6 +342,10 @@ export async function httpSortEvalFailures(base, evalRunId, input) {
342
342
  const { status, body } = await postJson(base, `/api/eval-runs/${enc(evalRunId)}/failures/sort`, input);
343
343
  return status === 200 ? { ok: true, body: body } : evalFailure(status, body);
344
344
  }
345
+ export async function httpCalibrateEvalGrader(base, evalId, graderId, input) {
346
+ const { status, body } = await postJson(base, `/api/evals/${enc(evalId)}/graders/${enc(graderId)}/calibrate`, input);
347
+ return status === 200 ? { ok: true, body: body } : evalFailure(status, body);
348
+ }
345
349
  export function httpListEvalSuggestions(base, evalRunId, status) {
346
350
  return evalGet(base, `/api/eval-runs/${enc(evalRunId)}/failures/suggestions${evalQuery({ status })}`);
347
351
  }
@@ -1,4 +1,4 @@
1
- import type { EvalConversation, EvalFile, ResolvedConversation, ResolvedGrader } from "../evals/types.js";
1
+ import type { EvalConversation, JudgeSettings, EvalFile, ResolvedConversation, ResolvedGrader } from "../evals/types.js";
2
2
  import { type LoadOutcome } from "./loadOutcome.js";
3
3
  type UnhashedGrader = ResolvedGrader extends infer G ? (G extends unknown ? Omit<G, "hash"> : never) : never;
4
4
  export type LoadedEval = {
@@ -25,6 +25,8 @@ export type EvalPaths = {
25
25
  };
26
26
  /** Paths in an eval file resolve against the eval file's directory (P6). */
27
27
  export declare function resolveEvalPaths(file: EvalFile, evalAbsPath: string): EvalPaths;
28
+ /** Grader settings over the eval defaults, key by key; undefined when neither sets anything. */
29
+ export declare function mergeJudgeSettings(base: JudgeSettings | undefined, over: JudgeSettings | undefined): JudgeSettings | undefined;
28
30
  export declare function graderHash(grader: UnhashedGrader): string;
29
31
  export declare function graderHashShort(grader: Pick<ResolvedGrader, "hash">): string;
30
32
  export declare function parseEvalOutcome(yamlText: string, filePath: string, projectRoot: string): LoadOutcome<LoadedEval>;
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { parse as parseYaml } from "yaml";
4
4
  import { AGENT_ID_PATTERN, isReservedAgentId } from "../agents/agentId.js";
5
5
  import { canonicalJson, hash6, sha256Hex } from "../evals/canonical.js";
6
+ import { EVAL_LIMITS } from "../evals/constants.js";
6
7
  import { loadFailure, loadSuccess } from "./loadOutcome.js";
7
8
  import { yamlParsePosition } from "./yamlParsePosition.js";
8
9
  export const EVAL_DEFAULT_REPEATS = 3;
@@ -89,6 +90,7 @@ const TASK_KEYS = ["goal", "context", "constraints"];
89
90
  const DEFAULTS_KEYS = [
90
91
  "repeats",
91
92
  "judge",
93
+ "judge_settings",
92
94
  "pass_threshold",
93
95
  "max_cost_usd",
94
96
  "concurrency",
@@ -101,11 +103,11 @@ const GRADER_TYPE_KEYS = {
101
103
  schema: [],
102
104
  field: ["checks"],
103
105
  reference: ["mode", "path", "expected", "pass_at"],
104
- rubric: ["rubric", "scale", "pass_at", "judge", "examples"],
106
+ rubric: ["rubric", "scale", "pass_at", "judge", "judge_settings", "examples"],
105
107
  code: ["code"],
106
108
  tool_calls: ["must_call", "must_not_call", "must_ask_first", "max_calls", "args"],
107
109
  conversation_check: ["max_turns", "max_agent_questions_per_turn", "final_reply"],
108
- goal_reached: ["judge", "guidance"],
110
+ goal_reached: ["judge", "judge_settings", "guidance"],
109
111
  };
110
112
  const TOOL_ARG_KEYS = ["tool", "path", "op", "value"];
111
113
  const FINAL_REPLY_KEYS = ["op", "value"];
@@ -285,6 +287,51 @@ function parseTask(c, raw) {
285
287
  }
286
288
  return out;
287
289
  }
290
+ const JUDGE_SETTINGS_KEYS = ["temperature", "reasoning", "votes"];
291
+ /** A mapping of judge call settings, or undefined when absent or invalid (findings added). */
292
+ function parseJudgeSettings(c, raw, label) {
293
+ if (raw === undefined)
294
+ return undefined;
295
+ if (!isPlainObject(raw)) {
296
+ c.add("eval.invalid_field", `${label} "judge_settings" must be a mapping`);
297
+ return undefined;
298
+ }
299
+ c.unknownKeys(raw, JUDGE_SETTINGS_KEYS, `${label} judge_settings`);
300
+ const out = {};
301
+ let ok = true;
302
+ if (raw.temperature !== undefined) {
303
+ if (typeof raw.temperature === "number" && raw.temperature >= 0 && raw.temperature <= 1) {
304
+ out.temperature = raw.temperature;
305
+ }
306
+ else {
307
+ c.add("eval.invalid_field", `${label} judge_settings "temperature" must be a number from 0 to 1`);
308
+ ok = false;
309
+ }
310
+ }
311
+ if (raw.reasoning !== undefined) {
312
+ if (typeof raw.reasoning === "boolean")
313
+ out.reasoning = raw.reasoning;
314
+ else {
315
+ c.add("eval.invalid_field", `${label} judge_settings "reasoning" must be true or false`);
316
+ ok = false;
317
+ }
318
+ }
319
+ if (raw.votes !== undefined) {
320
+ if (typeof raw.votes === "number" && Number.isInteger(raw.votes) && raw.votes >= 1 && raw.votes <= 5 && raw.votes % 2 === 1) {
321
+ out.votes = raw.votes;
322
+ }
323
+ else {
324
+ c.add("eval.invalid_field", `${label} judge_settings "votes" must be 1, 3 or 5`);
325
+ ok = false;
326
+ }
327
+ }
328
+ return ok ? out : undefined;
329
+ }
330
+ /** Grader settings over the eval defaults, key by key; undefined when neither sets anything. */
331
+ export function mergeJudgeSettings(base, over) {
332
+ const merged = { ...(base ?? {}), ...(over ?? {}) };
333
+ return Object.keys(merged).length === 0 ? undefined : merged;
334
+ }
288
335
  function parseDefaults(c, raw, kind) {
289
336
  if (raw === undefined)
290
337
  return undefined;
@@ -300,6 +347,9 @@ function parseDefaults(c, raw, kind) {
300
347
  const judge = optionalString(c, raw, "judge", "defaults", { min: 1 });
301
348
  if (judge !== undefined)
302
349
  out.judge = judge;
350
+ const judgeSettings = parseJudgeSettings(c, raw.judge_settings, "defaults");
351
+ if (judgeSettings !== undefined)
352
+ out.judge_settings = judgeSettings;
303
353
  const passThreshold = optionalNumber(c, raw, "pass_threshold", "defaults", "a number greater than 0 and at most 1", (n) => n > 0 && n <= 1);
304
354
  if (passThreshold !== undefined)
305
355
  out.pass_threshold = passThreshold;
@@ -483,8 +533,8 @@ function parseChecks(c, raw, where) {
483
533
  return ok ? checks : undefined;
484
534
  }
485
535
  function parseExamples(c, raw, scale, where) {
486
- if (!Array.isArray(raw) || raw.length > 6) {
487
- c.add("eval.grader_invalid", `${where} "examples" must be an array of at most 6 items`);
536
+ if (!Array.isArray(raw) || raw.length > EVAL_LIMITS.rubricExamplesMax) {
537
+ c.add("eval.grader_invalid", `${where} "examples" must be an array of at most ${EVAL_LIMITS.rubricExamplesMax} items`);
488
538
  return undefined;
489
539
  }
490
540
  const out = [];
@@ -610,7 +660,7 @@ function parseFinalReply(c, raw, where) {
610
660
  });
611
661
  return ok ? out : undefined;
612
662
  }
613
- function parseGrader(c, raw, index, defaultJudge, kind) {
663
+ function parseGrader(c, raw, index, defaultJudge, kind, defaultJudgeSettings) {
614
664
  if (!isPlainObject(raw)) {
615
665
  c.add("eval.grader_invalid", `graders[${index}] must be a mapping`);
616
666
  return undefined;
@@ -769,6 +819,10 @@ function parseGrader(c, raw, index, defaultJudge, kind) {
769
819
  if (examples === undefined)
770
820
  ok = false;
771
821
  }
822
+ const ownSettings = parseJudgeSettings(c, raw.judge_settings, label);
823
+ if (raw.judge_settings !== undefined && ownSettings === undefined)
824
+ ok = false;
825
+ const judgeSettings = mergeJudgeSettings(defaultJudgeSettings, ownSettings);
772
826
  if (ok) {
773
827
  result = {
774
828
  config: {
@@ -778,6 +832,7 @@ function parseGrader(c, raw, index, defaultJudge, kind) {
778
832
  ...(raw.scale !== undefined ? { scale } : {}),
779
833
  ...(passAt !== undefined ? { pass_at: passAt } : {}),
780
834
  ...(judge !== undefined ? { judge } : {}),
835
+ ...(ownSettings !== undefined ? { judge_settings: ownSettings } : {}),
781
836
  ...(examples !== undefined ? { examples } : {}),
782
837
  },
783
838
  resolved: {
@@ -787,6 +842,7 @@ function parseGrader(c, raw, index, defaultJudge, kind) {
787
842
  scale,
788
843
  passAt: scale === "1_to_5" ? (passAt ?? 4) : 1,
789
844
  judge: judge ?? defaultJudge ?? "",
845
+ ...(judgeSettings !== undefined ? { judgeSettings } : {}),
790
846
  examples: examples ?? [],
791
847
  },
792
848
  };
@@ -882,18 +938,24 @@ function parseGrader(c, raw, index, defaultJudge, kind) {
882
938
  ok = false;
883
939
  }
884
940
  }
941
+ const goalSettings = parseJudgeSettings(c, raw.judge_settings, label);
942
+ if (raw.judge_settings !== undefined && goalSettings === undefined)
943
+ ok = false;
944
+ const goalMerged = mergeJudgeSettings(defaultJudgeSettings, goalSettings);
885
945
  if (ok) {
886
946
  result = {
887
947
  config: {
888
948
  ...common,
889
949
  type: "goal_reached",
890
950
  ...(judge !== undefined ? { judge } : {}),
951
+ ...(goalSettings !== undefined ? { judge_settings: goalSettings } : {}),
891
952
  ...(guidance !== undefined ? { guidance } : {}),
892
953
  },
893
954
  resolved: {
894
955
  ...resolvedCommon,
895
956
  type: "goal_reached",
896
957
  judge: judge ?? defaultJudge ?? "",
958
+ ...(goalMerged !== undefined ? { judgeSettings: goalMerged } : {}),
897
959
  ...(guidance !== undefined ? { guidance } : {}),
898
960
  },
899
961
  };
@@ -932,6 +994,7 @@ function graderHashObject(grader) {
932
994
  scale: grader.scale,
933
995
  ...(grader.scale === "1_to_5" ? { pass_at: grader.passAt } : {}),
934
996
  judge: grader.judge,
997
+ ...(grader.judgeSettings !== undefined ? { judge_settings: grader.judgeSettings } : {}),
935
998
  examples: grader.examples,
936
999
  };
937
1000
  case "code":
@@ -953,7 +1016,12 @@ function graderHashObject(grader) {
953
1016
  final_reply: grader.finalReply,
954
1017
  };
955
1018
  case "goal_reached":
956
- return { ...base, judge: grader.judge, guidance: grader.guidance };
1019
+ return {
1020
+ ...base,
1021
+ judge: grader.judge,
1022
+ ...(grader.judgeSettings !== undefined ? { judge_settings: grader.judgeSettings } : {}),
1023
+ guidance: grader.guidance,
1024
+ };
957
1025
  }
958
1026
  }
959
1027
  export function graderHash(grader) {
@@ -1047,7 +1115,7 @@ export function parseEvalOutcome(yamlText, filePath, projectRoot) {
1047
1115
  else {
1048
1116
  const seen = new Set();
1049
1117
  raw.graders.forEach((rawGrader, index) => {
1050
- const parsed = parseGrader(c, rawGrader, index, defaults?.judge, target?.kind);
1118
+ const parsed = parseGrader(c, rawGrader, index, defaults?.judge, target?.kind, defaults?.judge_settings);
1051
1119
  if (isPlainObject(rawGrader) && typeof rawGrader.id === "string") {
1052
1120
  if (seen.has(rawGrader.id)) {
1053
1121
  c.add("eval.grader_duplicate_id", `Duplicate grader id "${rawGrader.id}"`);
@@ -2,7 +2,7 @@ import { internalAgentsVisible } from "../operatorAgent/internalAgents.js";
2
2
  import { PROFILE_GRANTS } from "../toolkit/profiles.js";
3
3
  import { realpath, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { mapJudge } from "../evals/judgeProviders.js";
5
+ import { isFloatingModelId, mapJudge } from "../evals/judgeProviders.js";
6
6
  import { classifyTool } from "../agents/toolRisk.js";
7
7
  import { checkDatasetAgainstAgent } from "./datasetAgentCheck.js";
8
8
  import { checkDatasetAgainstStage } from "./datasetStageCheck.js";
@@ -106,6 +106,7 @@ export async function validateEval(loaded, ctx) {
106
106
  }
107
107
  const stageModel = file.defaults?.model ?? resolved?.modelResolved;
108
108
  const unknownChecked = new Set();
109
+ const unpinnedWarned = new Set();
109
110
  for (const grader of loaded.graders) {
110
111
  if (grader.type !== "rubric")
111
112
  continue;
@@ -122,6 +123,13 @@ export async function validateEval(loaded, ctx) {
122
123
  if (stageModel !== undefined && grader.judge === stageModel) {
123
124
  add("warning", "eval.judge_same_as_stage", `Rubric grader "${grader.id}" uses judge "${grader.judge}", the same model as the stage (self-preference bias)`);
124
125
  }
126
+ if (isFloatingModelId(grader.judge) && !unpinnedWarned.has(grader.judge)) {
127
+ unpinnedWarned.add(grader.judge);
128
+ add("warning", "eval.judge_unpinned", `Judge model "${grader.judge}" is a floating id; its behavior can change under stored runs. Pin a dated version`);
129
+ }
130
+ if ((grader.judgeSettings?.votes ?? 1) > 1 && grader.judgeSettings?.temperature === 0) {
131
+ add("warning", "eval.judge_votes_no_variation", `Grader "${grader.id}" asks for ${grader.judgeSettings?.votes} votes at temperature 0, so every vote is the same call and the cost is multiplied for nothing`);
132
+ }
125
133
  }
126
134
  const paths = resolveEvalPaths(file, loaded.path);
127
135
  const datasetProblem = ctx.datasetInline
@@ -267,6 +275,7 @@ async function validateAgentEval(loaded, ctx, findings) {
267
275
  }
268
276
  const agentModel = file.defaults?.model ?? resolved?.modelResolved;
269
277
  const unknownChecked = new Set();
278
+ const unpinnedWarned = new Set();
270
279
  for (const grader of loaded.graders) {
271
280
  if (grader.type !== "rubric" && grader.type !== "goal_reached")
272
281
  continue;
@@ -283,6 +292,13 @@ async function validateAgentEval(loaded, ctx, findings) {
283
292
  if (agentModel !== undefined && grader.judge === agentModel) {
284
293
  add("warning", "eval.judge_same_as_stage", `Grader "${grader.id}" uses judge "${grader.judge}", the same model as the agent (self-preference bias)`);
285
294
  }
295
+ if (isFloatingModelId(grader.judge) && !unpinnedWarned.has(grader.judge)) {
296
+ unpinnedWarned.add(grader.judge);
297
+ add("warning", "eval.judge_unpinned", `Judge model "${grader.judge}" is a floating id; its behavior can change under stored runs. Pin a dated version`);
298
+ }
299
+ if ((grader.judgeSettings?.votes ?? 1) > 1 && grader.judgeSettings?.temperature === 0) {
300
+ add("warning", "eval.judge_votes_no_variation", `Grader "${grader.id}" asks for ${grader.judgeSettings?.votes} votes at temperature 0, so every vote is the same call and the cost is multiplied for nothing`);
301
+ }
286
302
  }
287
303
  if (resolved) {
288
304
  const grantWarn = (names, where) => {
@@ -0,0 +1,21 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { EvalCalibrationSplit, EvalGraderLabelRow } from "./storeTypes.js";
3
+ import type { CaseOutput } from "./types.js";
4
+ export type CalibrationSampleSource = {
5
+ caseId: string;
6
+ /** Position within the case's samples; the replay repeat index. */
7
+ repeatIndex: number;
8
+ label: "pass" | "fail";
9
+ output: CaseOutput;
10
+ originalRunId: string;
11
+ originalRepeatIndex: number;
12
+ };
13
+ export type CalibrationStore = Pick<RunStore, "listEvalGraderLabelsForEval" | "listEvalResults">;
14
+ /** Rebuilds the in-memory case output from the stored result JSON. */
15
+ export declare function replayOutput(stored: unknown): CaseOutput | null;
16
+ /**
17
+ * The labeled samples for one grader across every run of the eval: the newest label per identical
18
+ * (case, output), grouped per case with a fresh repeat index so the engine can replay them.
19
+ */
20
+ export declare function collectCalibrationSamples(store: CalibrationStore, projectRoot: string, evalId: string, graderId: string, split: EvalCalibrationSplit): Promise<CalibrationSampleSource[]>;
21
+ export type { EvalGraderLabelRow };
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ import { calibrationSplitOf } from "./stats.js";
3
+ import { renderTranscript } from "./graders/transcript.js";
4
+ function outputKey(caseId, output) {
5
+ return createHash("sha256")
6
+ .update(caseId)
7
+ .update("\u0000")
8
+ .update(JSON.stringify(output ?? null))
9
+ .digest("hex");
10
+ }
11
+ /** Rebuilds the in-memory case output from the stored result JSON. */
12
+ export function replayOutput(stored) {
13
+ if (stored === null || typeof stored !== "object")
14
+ return null;
15
+ const o = stored;
16
+ if (o.kind === "conversation") {
17
+ const out = { ...o };
18
+ out.transcript = renderTranscript(out);
19
+ return out;
20
+ }
21
+ if (typeof o.status !== "string" || typeof o.summary !== "string")
22
+ return null;
23
+ return {
24
+ kind: "envelope",
25
+ status: o.status === "failure" ? "failure" : "success",
26
+ summary: o.summary,
27
+ ...(o.payload !== undefined && typeof o.payload === "object" ? { payload: o.payload } : {}),
28
+ artifacts: Array.isArray(o.artifacts) ? o.artifacts : [],
29
+ };
30
+ }
31
+ /**
32
+ * The labeled samples for one grader across every run of the eval: the newest label per identical
33
+ * (case, output), grouped per case with a fresh repeat index so the engine can replay them.
34
+ */
35
+ export async function collectCalibrationSamples(store, projectRoot, evalId, graderId, split) {
36
+ const labels = await store.listEvalGraderLabelsForEval(projectRoot, evalId, graderId);
37
+ const resultsByRun = new Map();
38
+ const seen = new Set();
39
+ const perCase = new Map();
40
+ const out = [];
41
+ for (const label of labels) {
42
+ if (split !== "all" && calibrationSplitOf(label.caseId) !== split)
43
+ continue;
44
+ let results = resultsByRun.get(label.evalRunId);
45
+ if (!results) {
46
+ results = new Map((await store.listEvalResults(label.evalRunId)).map((r) => [`${r.caseId}#${r.repeatIndex}`, r]));
47
+ resultsByRun.set(label.evalRunId, results);
48
+ }
49
+ const result = results.get(`${label.caseId}#${label.repeatIndex}`);
50
+ const output = result ? replayOutput(result.output) : null;
51
+ if (!output)
52
+ continue;
53
+ const key = outputKey(label.caseId, result?.output);
54
+ if (seen.has(key))
55
+ continue;
56
+ seen.add(key);
57
+ const repeatIndex = perCase.get(label.caseId) ?? 0;
58
+ perCase.set(label.caseId, repeatIndex + 1);
59
+ out.push({
60
+ caseId: label.caseId,
61
+ repeatIndex,
62
+ label: label.label,
63
+ output,
64
+ originalRunId: label.evalRunId,
65
+ originalRepeatIndex: label.repeatIndex,
66
+ });
67
+ }
68
+ return out;
69
+ }
@@ -0,0 +1,29 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import type { EvalCalibrationRow } from "./storeTypes.js";
3
+ import type { CaseOutput, RubricExample } from "./types.js";
4
+ export declare const EXAMPLE_SUGGESTION_LIMIT = 3;
5
+ export type SuggestedExample = {
6
+ eval_run_id: string;
7
+ case_id: string;
8
+ repeat_index: number;
9
+ /** What the judge said, so the author can see why this example would help. */
10
+ judge: "pass" | "fail";
11
+ example: RubricExample;
12
+ };
13
+ export type SuggestStore = Pick<RunStore, "listEvalResults" | "listEvalGraderLabelsForEval">;
14
+ /** The text a judge example shows for one stored output. */
15
+ export declare function exampleOutputText(output: CaseOutput): string;
16
+ /**
17
+ * Up to three few-shot examples built from where the judge disagreed with the author on the dev split (a held-out row
18
+ * is never offered, so the held-out check stays a fair test): one of each kind of
19
+ * disagreement first (judge too lenient, judge too strict), distinct outputs only. The author's label note,
20
+ * when there is one, becomes the example's reason. Nothing is written.
21
+ */
22
+ export declare function suggestExamples(store: SuggestStore, input: {
23
+ projectRoot: string;
24
+ evalId: string;
25
+ graderId: string;
26
+ scale: "pass_fail" | "1_to_5";
27
+ calibration: Pick<EvalCalibrationRow, "disagreements">;
28
+ limit?: number;
29
+ }): Promise<SuggestedExample[]>;
@@ -0,0 +1,72 @@
1
+ import { replayOutput } from "./calibration.js";
2
+ import { calibrationSplitOf } from "./stats.js";
3
+ export const EXAMPLE_SUGGESTION_LIMIT = 3;
4
+ const EXAMPLE_OUTPUT_MAX_CHARS = 1500;
5
+ function cut(text) {
6
+ return text.length <= EXAMPLE_OUTPUT_MAX_CHARS ? text : `${text.slice(0, EXAMPLE_OUTPUT_MAX_CHARS)}…`;
7
+ }
8
+ /** The text a judge example shows for one stored output. */
9
+ export function exampleOutputText(output) {
10
+ if (output.kind === "conversation")
11
+ return cut(output.transcript ?? output.final_reply);
12
+ const payload = output.payload === undefined ? "" : `\n${JSON.stringify(output.payload)}`;
13
+ return cut(`${output.summary}${payload}`);
14
+ }
15
+ function exampleFor(scale, label, output, reason) {
16
+ const base = { output, ...(reason !== undefined && reason !== "" ? { reason } : {}) };
17
+ if (scale === "pass_fail")
18
+ return { verdict: label, ...base };
19
+ return { score: label === "pass" ? 5 : 1, ...base };
20
+ }
21
+ /**
22
+ * Up to three few-shot examples built from where the judge disagreed with the author on the dev split (a held-out row
23
+ * is never offered, so the held-out check stays a fair test): one of each kind of
24
+ * disagreement first (judge too lenient, judge too strict), distinct outputs only. The author's label note,
25
+ * when there is one, becomes the example's reason. Nothing is written.
26
+ */
27
+ export async function suggestExamples(store, input) {
28
+ const limit = input.limit ?? EXAMPLE_SUGGESTION_LIMIT;
29
+ const labels = await store.listEvalGraderLabelsForEval(input.projectRoot, input.evalId, input.graderId);
30
+ const notes = new Map(labels.map((l) => [`${l.evalRunId}#${l.caseId}#${l.repeatIndex}`, l.note]));
31
+ const dev = input.calibration.disagreements.filter((d) => calibrationSplitOf(d.caseId) === "dev");
32
+ const lenient = dev.filter((d) => d.judge === "pass");
33
+ const strict = dev.filter((d) => d.judge === "fail");
34
+ const ordered = [];
35
+ for (let i = 0; ordered.length < lenient.length + strict.length; i++) {
36
+ if (lenient[i])
37
+ ordered.push(lenient[i]);
38
+ if (strict[i])
39
+ ordered.push(strict[i]);
40
+ }
41
+ const runs = new Map();
42
+ const seen = new Set();
43
+ const out = [];
44
+ for (const d of ordered) {
45
+ if (out.length >= limit)
46
+ break;
47
+ if (d.evalRunId === undefined)
48
+ continue;
49
+ let results = runs.get(d.evalRunId);
50
+ if (!results) {
51
+ results = await store.listEvalResults(d.evalRunId);
52
+ runs.set(d.evalRunId, results);
53
+ }
54
+ const result = results.find((r) => r.caseId === d.caseId && r.repeatIndex === d.repeatIndex);
55
+ const output = result ? replayOutput(result.output) : null;
56
+ if (!output)
57
+ continue;
58
+ const text = exampleOutputText(output);
59
+ if (seen.has(text))
60
+ continue;
61
+ seen.add(text);
62
+ const note = notes.get(`${d.evalRunId}#${d.caseId}#${d.repeatIndex}`) ?? undefined;
63
+ out.push({
64
+ eval_run_id: d.evalRunId,
65
+ case_id: d.caseId,
66
+ repeat_index: d.repeatIndex,
67
+ judge: d.judge,
68
+ example: exampleFor(input.scale, d.label, text, note),
69
+ });
70
+ }
71
+ return out;
72
+ }
@@ -0,0 +1,46 @@
1
+ import { type CalibrationStore } from "./calibration.js";
2
+ import type { JudgedGrader } from "./judged.js";
3
+ import { type CalibrationStats, type StabilityStats } from "./stats.js";
4
+ import type { EvalCalibrationDisagreement, EvalCalibrationRow, EvalCalibrationSplit } from "./storeTypes.js";
5
+ import type { EvalEngine, EvalPlan, JudgeSettings, RubricExample } from "./types.js";
6
+ import type { RunStore } from "../runstore/port.js";
7
+ export type CalibrateInput = {
8
+ projectRoot: string;
9
+ evalPath: string;
10
+ graderId: string;
11
+ split?: EvalCalibrationSplit;
12
+ /** A rubric to measure instead of the saved one; the result is stored as a candidate. */
13
+ candidate?: {
14
+ rubric?: string;
15
+ examples?: RubricExample[];
16
+ judge?: string;
17
+ judgeSettings?: JudgeSettings;
18
+ };
19
+ maxCostUsd?: number;
20
+ /** Judge each output 3 times (or the grader's own larger vote count) and report how often the verdict splits. */
21
+ stability?: boolean;
22
+ /** Cancels the check (for example when the request closes); nothing is stored when it fires. */
23
+ signal?: AbortSignal;
24
+ };
25
+ export type CalibrateResult = {
26
+ calibration: EvalCalibrationRow;
27
+ stats: CalibrationStats;
28
+ disagreements: EvalCalibrationDisagreement[];
29
+ errored: number;
30
+ stability?: StabilityStats;
31
+ costUsd: number;
32
+ };
33
+ export type CalibrationDeps = {
34
+ store: CalibrationStore & Pick<RunStore, "insertEvalCalibration">;
35
+ engine: EvalEngine;
36
+ log: (level: "info" | "warn" | "error", message: string) => void;
37
+ };
38
+ export declare const CALIBRATION_DEFAULT_MAX_COST_USD = 1;
39
+ /**
40
+ * Re-judges the stored outputs of results the author labeled for one judged grader and compares the verdicts to the
41
+ * labels. No stage or agent runs: the engine's case callback replays the stored output. `base` is the eval's built
42
+ * plan and `found` is the saved grader; the caller owns the one-at-a-time claim and the work directory.
43
+ */
44
+ export declare function measureCalibration({ store, engine, log }: CalibrationDeps, input: CalibrateInput, base: EvalPlan, found: JudgedGrader, abort: AbortController,
45
+ /** Fires when the caller cancels; the budget limit uses `abort` instead and still stores the partial reading. */
46
+ cancel: AbortSignal): Promise<CalibrateResult>;