@bastani/atomic 0.9.14-alpha.4 → 0.9.14-alpha.5

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 (141) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/subagents/src/extension/schemas.ts +5 -0
  7. package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +3 -1
  8. package/dist/builtin/subagents/src/runs/shared/progress-trend.ts +69 -0
  9. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +12 -1
  10. package/dist/builtin/subagents/src/shared/types-results.ts +2 -0
  11. package/dist/builtin/web-access/package.json +1 -1
  12. package/dist/builtin/workflows/CHANGELOG.md +41 -1
  13. package/dist/builtin/workflows/README.md +6 -5
  14. package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
  15. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
  16. package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
  17. package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
  18. package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
  19. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
  20. package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
  21. package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
  22. package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
  23. package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
  24. package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
  25. package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
  26. package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
  27. package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
  28. package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
  29. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
  30. package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
  31. package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
  32. package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
  33. package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
  34. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
  35. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
  36. package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
  37. package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
  38. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
  39. package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
  40. package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
  41. package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
  42. package/dist/builtin/workflows/builtin/tournament.ts +66 -32
  43. package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
  44. package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
  45. package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
  46. package/dist/builtin/workflows/package.json +1 -1
  47. package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
  48. package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
  49. package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
  50. package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
  51. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  52. package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
  53. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
  54. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
  55. package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
  56. package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
  57. package/dist/builtin/workflows/src/engine/options.ts +1 -0
  58. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
  59. package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
  60. package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
  62. package/dist/builtin/workflows/src/engine/run.ts +124 -2
  63. package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
  64. package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
  65. package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
  66. package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
  68. package/dist/builtin/workflows/src/extension/index.bundle.mjs +2975 -843
  69. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
  70. package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
  71. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
  72. package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
  73. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
  74. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  75. package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
  76. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
  77. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
  78. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
  79. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
  80. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
  81. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
  82. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
  83. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
  84. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
  85. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
  86. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
  87. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
  88. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
  89. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
  90. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
  91. package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
  92. package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
  93. package/dist/builtin/workflows/src/shared/budget.ts +127 -0
  94. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
  95. package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
  96. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
  97. package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
  98. package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
  99. package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
  100. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
  101. package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
  102. package/dist/builtin/workflows/src/shared/types.ts +3 -0
  103. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
  104. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
  105. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
  106. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  107. package/dist/core/atomic-guide-command.js +1 -0
  108. package/dist/core/atomic-guide-command.js.map +1 -1
  109. package/dist/core/extensions/ui-types.d.ts +13 -3
  110. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  111. package/dist/core/extensions/ui-types.js +15 -3
  112. package/dist/core/extensions/ui-types.js.map +1 -1
  113. package/dist/core/slash-commands.d.ts.map +1 -1
  114. package/dist/core/slash-commands.js +33 -3
  115. package/dist/core/slash-commands.js.map +1 -1
  116. package/dist/main-deferred-startup.d.ts.map +1 -1
  117. package/dist/main-deferred-startup.js +6 -2
  118. package/dist/main-deferred-startup.js.map +1 -1
  119. package/dist/modes/interactive/interactive-startup.js +4 -0
  120. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  121. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  122. package/dist/modes/interactive/interactive-tui.js +19 -1
  123. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  124. package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
  125. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
  127. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  128. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  129. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  130. package/dist/modes/rpc/rpc-client.js +15 -2
  131. package/dist/modes/rpc/rpc-client.js.map +1 -1
  132. package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
  133. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  134. package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
  135. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  136. package/docs/extensions.md +1 -1
  137. package/docs/quickstart.md +1 -0
  138. package/docs/skills.md +4 -0
  139. package/docs/workflows.md +74 -10
  140. package/npm-shrinkwrap.json +29 -29
  141. package/package.json +2 -2
@@ -1,194 +1,400 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join, isAbsolute, relative, resolve } from "node:path";
3
3
  import { Type } from "typebox";
4
- import type {
5
- WorkflowRunContext,
6
- WorkflowSerializableObject,
7
- WorkflowSerializableValue,
8
- WorkflowTaskResult,
9
- } from "../src/shared/types.js";
10
- import {
11
- renderBracketReducerPrompt,
12
- renderPairwiseJudgePrompt,
13
- renderTournamentAttemptPrompt,
14
- } from "./tournament-prompts.js";
4
+ import type { WorkflowInputValues, WorkflowRunContext, WorkflowSerializableObject, WorkflowSerializableValue, WorkflowTaskResult, WorkflowTaskStep } from "../src/shared/types.js";
5
+ import { accumulate, plan_comparisons, rank_candidates, select_pivots, soft_win, type Preference, type ScoringJob } from "./selection-math.js";
6
+ import { build_scoring_prompt, scoring_prompt_reads, warm_first_fan_out, type ScoringCandidate, type SharedHead } from "./verification-prompts.js";
7
+ import { normalize_criteria, parse_rubric, VERIFICATION_SCALE, type Criterion, type CriterionInput } from "./verification-criteria.js";
8
+ import { DEFAULT_TOURNAMENT_CRITERIA, renderComparisonsReducerPrompt, renderTournamentAttemptPrompt } from "./tournament-prompts.js";
15
9
  import { stableArtifactRoot } from "./pattern-artifact-root.js";
10
+ import { fold_usage, type UsageTotals } from "./verification-usage.js";
16
11
 
17
- const judgeDecisionSchema = Type.Object({
18
- winner: Type.Union([Type.Literal("first"), Type.Literal("second")]),
19
- rationale: Type.String(),
20
- evidence: Type.Array(Type.String(), { minItems: 1 }),
12
+ const judgeScoreSchema = Type.Object({
13
+ criterion_id: Type.String(),
14
+ score_a: Type.Integer({ minimum: VERIFICATION_SCALE.min, maximum: VERIFICATION_SCALE.max }),
15
+ score_b: Type.Integer({ minimum: VERIFICATION_SCALE.min, maximum: VERIFICATION_SCALE.max }),
16
+ evidence: Type.Array(Type.String()),
21
17
  }, { additionalProperties: false });
22
18
 
23
- type TournamentInputs = {
24
- readonly prompt: string;
25
- readonly num_attempts: number;
26
- readonly max_concurrency: number;
27
- } & Record<string, WorkflowSerializableValue>;
28
-
29
- type Entrant = { readonly label: string; readonly path: string };
30
- type MatchRecord = {
31
- readonly round: number;
32
- readonly match: number;
33
- readonly first: string;
34
- readonly second: string;
35
- readonly winner: string;
36
- readonly rationale: string;
37
- readonly evidence: readonly string[];
38
- readonly judge_artifact_path: string;
39
- };
40
- type ByeRecord = { readonly round: number; readonly entrant: string };
19
+ const JUDGE_GROUND_TRUTH_NOTE = "No external ground truth is supplied; score each presented solution against the task and criterion.";
20
+ const JUDGE_OUTPUT_FORMAT =
21
+ "Return only JSON with criterion_id (string), score_a (integer 1–20 for presented slot 1), score_b (integer 1–20 for presented slot 2), and evidence (array of strings).";
22
+
23
+ type TournamentCriteriaInput = string | Record<string, string> | readonly string[] | readonly CriterionInput[];
24
+ type TournamentInputs = WorkflowInputValues & { readonly prompt: string; readonly num_attempts: number; readonly max_concurrency: number; readonly n_evaluations: number; readonly pivots: number; readonly seed: number; readonly criteria?: TournamentCriteriaInput; readonly models?: readonly string[] };
25
+
26
+ type Entrant = { readonly label: string; readonly path: string; readonly body: string };
27
+ type JudgeReport = { readonly criterion_id: string; readonly score_a: number; readonly score_b: number; readonly evidence: readonly string[] };
28
+ type ComparisonRecord = { a: number; b: number; phase: "ring" | "pivot"; criterion_id: string; rep: number; swapped: boolean; score_a?: number; score_b?: number; p_ab?: number; invalid?: true; judge_artifact_path: string };
29
+ type ScoredComparisonRecord = ComparisonRecord & { score_a: number; score_b: number };
30
+ type PairRecord = { a: number; b: number; phase: "ring" | "pivot"; valid_reports: number; mean_score_a?: number; mean_score_b?: number; p_ab: number; invalid?: true };
31
+ type JudgeStage = { readonly job: ScoringJob; readonly name: string; readonly path: string; readonly slot1: number; readonly slot2: number; readonly step: WorkflowTaskStep };
41
32
 
42
33
  function serializableObject(
43
- value: WorkflowSerializableValue | undefined,
34
+ value: WorkflowSerializableValue | undefined,
44
35
  ): WorkflowSerializableObject | undefined {
45
- if (value === null || Array.isArray(value) || typeof value !== "object") return undefined;
46
- return value as WorkflowSerializableObject;
36
+ if (value === null || Array.isArray(value) || typeof value !== "object") return undefined;
37
+ return value as WorkflowSerializableObject;
38
+ }
39
+
40
+ function stringArray(value: WorkflowSerializableValue | undefined): value is readonly string[] {
41
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
47
42
  }
48
43
 
49
- function decisionFrom(result: WorkflowTaskResult): {
50
- readonly winner: "first" | "second";
51
- readonly rationale: string;
52
- readonly evidence: readonly string[];
53
- } {
54
- const value = serializableObject(result.structured);
55
- if (value === undefined) {
56
- throw new Error(`tournament: judge ${result.stageName} did not return a structured decision`);
57
- }
58
- const winner = value.winner;
59
- const rationale = value.rationale;
60
- const evidence = value.evidence;
61
- if ((winner !== "first" && winner !== "second") || typeof rationale !== "string" ||
62
- !Array.isArray(evidence) || !evidence.every((item) => typeof item === "string")) {
63
- throw new Error(`tournament: judge ${result.stageName} returned an invalid decision`);
64
- }
65
- return { winner, rationale, evidence };
44
+ function judgeReport(value: WorkflowSerializableValue | undefined): JudgeReport | undefined {
45
+ const object = serializableObject(value);
46
+ if (object === undefined) return undefined;
47
+ const criterionId = object.criterion_id;
48
+ const scoreA = object.score_a;
49
+ const scoreB = object.score_b;
50
+ const evidence = object.evidence;
51
+ if (
52
+ typeof criterionId !== "string" ||
53
+ typeof scoreA !== "number" ||
54
+ !Number.isInteger(scoreA) ||
55
+ scoreA < VERIFICATION_SCALE.min ||
56
+ scoreA > VERIFICATION_SCALE.max ||
57
+ typeof scoreB !== "number" ||
58
+ !Number.isInteger(scoreB) ||
59
+ scoreB < VERIFICATION_SCALE.min ||
60
+ scoreB > VERIFICATION_SCALE.max ||
61
+ !stringArray(evidence)
62
+ ) return undefined;
63
+ return { criterion_id: criterionId, score_a: scoreA, score_b: scoreB, evidence };
66
64
  }
67
65
 
68
- function resultByName(results: readonly WorkflowTaskResult[], name: string): WorkflowTaskResult {
69
- const result = results.find((candidate) => candidate.stageName === name || candidate.name === name);
70
- if (result === undefined) throw new Error(`tournament: missing result for ${name}`);
71
- return result;
66
+ function resultFor(
67
+ results: readonly WorkflowTaskResult[],
68
+ stageName: string,
69
+ ): WorkflowTaskResult | undefined {
70
+ return results.find((result) => result.stageName === stageName || result.name === stageName);
71
+ }
72
+
73
+ function scoredComparison(record: ComparisonRecord): record is ScoredComparisonRecord {
74
+ return typeof record.score_a === "number" && typeof record.score_b === "number" && record.invalid !== true;
75
+ }
76
+
77
+ function stageName(job: ScoringJob): string {
78
+ return `judge-${job.a}-${job.b}-${job.criterionId}-r${job.rep}`;
79
+ }
80
+
81
+ function criterionSlug(id: string): string {
82
+ const slug = id
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9]+/g, "_")
85
+ .replace(/^_+|_+$/g, "")
86
+ .slice(0, 40)
87
+ .replace(/_+$/g, "");
88
+ return slug || "criterion";
89
+ }
90
+
91
+ function judgeArtifactPath(judgesDir: string, job: ScoringJob, criterionIndex: number): string {
92
+ const filename = `judge-${job.a}-${job.b}-c${criterionIndex}-${criterionSlug(job.criterionId)}-r${job.rep}.json`;
93
+ const root = resolve(judgesDir);
94
+ const path = resolve(judgesDir, filename);
95
+ const fromRoot = relative(root, path);
96
+ if (fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
97
+ throw new Error(`tournament: judge artifact escaped its directory: ${path}`);
98
+ }
99
+ return path;
100
+ }
101
+
102
+ function tournamentCriteria(input: TournamentCriteriaInput | undefined): readonly Criterion[] {
103
+ if (input === undefined) return normalize_criteria(DEFAULT_TOURNAMENT_CRITERIA);
104
+ if (typeof input === "string") return parse_rubric(input).criteria;
105
+ return normalize_criteria(input);
106
+ }
107
+
108
+ function modelAssignment(
109
+ entrants: readonly Entrant[],
110
+ models: readonly string[] | undefined,
111
+ ): Record<string, string> | undefined {
112
+ if (models === undefined) return undefined;
113
+ const assignment: Record<string, string> = {};
114
+ if (models.length === 0) return assignment;
115
+ for (const [index, entrant] of entrants.entries()) assignment[entrant.label] = models[index % models.length]!;
116
+ return assignment;
117
+ }
118
+
119
+ function combineUsage(totals: readonly UsageTotals[]): UsageTotals {
120
+ const combined = {
121
+ calls: 0,
122
+ input: 0,
123
+ output: 0,
124
+ cacheRead: 0,
125
+ cacheWrite: 0,
126
+ cost: 0,
127
+ turns: 0,
128
+ };
129
+ for (const total of totals) {
130
+ combined.calls += total.calls;
131
+ combined.input += total.input;
132
+ combined.output += total.output;
133
+ combined.cacheRead += total.cacheRead;
134
+ combined.cacheWrite += total.cacheWrite;
135
+ combined.cost += total.cost;
136
+ combined.turns += total.turns;
137
+ }
138
+ const cacheDenominator = combined.input + combined.cacheRead;
139
+ return {
140
+ ...combined,
141
+ cacheHitRate: cacheDenominator === 0 ? 0 : combined.cacheRead / cacheDenominator,
142
+ };
72
143
  }
73
144
 
74
145
  export async function runTournament(ctx: WorkflowRunContext<TournamentInputs>) {
75
- const artifactDir = await stableArtifactRoot(ctx, "tournament");
76
- const attemptsDir = join(artifactDir, "attempts");
77
- const judgesDir = join(artifactDir, "judges");
78
- await mkdir(attemptsDir, { recursive: true });
79
- await mkdir(judgesDir, { recursive: true });
80
-
81
- const attemptArtifactPaths = Array.from({ length: ctx.inputs.num_attempts }, (_, index) =>
82
- join(attemptsDir, `attempt-${index + 1}.md`));
83
- await ctx.parallel(
84
- attemptArtifactPaths.map((path, index) => ({
85
- name: `attempt-${index + 1}`,
86
- prompt: renderTournamentAttemptPrompt(ctx.inputs.prompt, index + 1),
87
- context: "fresh" as const,
88
- output: path,
89
- outputMode: "file-only" as const,
90
- })),
91
- { concurrency: ctx.inputs.max_concurrency, failFast: true },
92
- );
93
-
94
- let entrants: Entrant[] = attemptArtifactPaths.map((path, index) => ({
95
- label: `attempt-${index + 1}`,
96
- path,
97
- }));
98
- const matches: MatchRecord[] = [];
99
- const byes: ByeRecord[] = [];
100
- const judgeArtifactPaths: string[] = [];
101
- let round = 1;
102
-
103
- while (entrants.length > 1) {
104
- const pairs: Array<readonly [Entrant, Entrant]> = [];
105
- const bye = entrants.length % 2 === 1 ? entrants.at(-1) : undefined;
106
- if (bye !== undefined) byes.push({ round, entrant: bye.label });
107
- for (let index = 0; index + 1 < entrants.length; index += 2) {
108
- pairs.push([entrants[index]!, entrants[index + 1]!]);
109
- }
110
- const judgeSteps = pairs.map(([left, right], index) => {
111
- const reverse = (round + index) % 2 === 0;
112
- const first = reverse ? right : left;
113
- const second = reverse ? left : right;
114
- const path = join(judgesDir, `round-${round}-match-${index + 1}.json`);
115
- judgeArtifactPaths.push(path);
116
- return {
117
- name: `judge-round-${round}-match-${index + 1}`,
118
- prompt: renderPairwiseJudgePrompt({
119
- task: ctx.inputs.prompt,
120
- firstLabel: first.label,
121
- secondLabel: second.label,
122
- firstPath: first.path,
123
- secondPath: second.path,
124
- }),
125
- context: "fresh" as const,
126
- reads: [first.path, second.path],
127
- schema: judgeDecisionSchema,
128
- };
129
- });
130
- const judgeResults = await ctx.parallel(judgeSteps, {
131
- concurrency: Math.min(ctx.inputs.max_concurrency, Math.max(1, judgeSteps.length)),
132
- failFast: true,
133
- });
134
- const next: Entrant[] = [];
135
- for (let index = 0; index < pairs.length; index += 1) {
136
- const [left, right] = pairs[index]!;
137
- const reverse = (round + index) % 2 === 0;
138
- const first = reverse ? right : left;
139
- const second = reverse ? left : right;
140
- const name = `judge-round-${round}-match-${index + 1}`;
141
- const decision = decisionFrom(resultByName(judgeResults, name));
142
- // Judge decisions are inter-stage data: the bracket reducer reads
143
- // schema-shaped JSON from these paths, so the runner persists the
144
- // structured decisions itself rather than the stage artifact channel.
145
- const judgeArtifactPath = judgeArtifactPaths.at(-pairs.length + index)!;
146
- await writeFile(judgeArtifactPath, `${JSON.stringify(decision, null, 2)}\n`);
147
- const winner = decision.winner === "first" ? first : second;
148
- next.push(winner);
149
- matches.push({
150
- round,
151
- match: index + 1,
152
- first: first.label,
153
- second: second.label,
154
- winner: winner.label,
155
- rationale: decision.rationale,
156
- evidence: decision.evidence,
157
- judge_artifact_path: judgeArtifactPath,
158
- });
159
- }
160
- if (bye !== undefined) next.push(bye);
161
- entrants = next;
162
- round += 1;
163
- }
164
-
165
- const winner = entrants[0]!;
166
- const bracketPath = join(artifactDir, "bracket.json");
167
- await writeFile(bracketPath, `${JSON.stringify({ task: ctx.inputs.prompt, matches, byes, winner }, null, 2)}\n`);
168
- const resultPath = join(artifactDir, "winner.md");
169
- const reducer = await ctx.task("bracket-reducer", {
170
- prompt: renderBracketReducerPrompt({
171
- task: ctx.inputs.prompt,
172
- bracketPath,
173
- winnerLabel: winner.label,
174
- winnerPath: winner.path,
175
- }),
176
- context: "fresh",
177
- reads: [bracketPath, winner.path, ...judgeArtifactPaths],
178
- output: resultPath,
179
- // Keep the reducer report out of the caller's context window; `result_path`
180
- // below carries it for callers that want the contents.
181
- outputMode: "file-only",
182
- });
183
-
184
- return {
185
- result: reducer.text,
186
- winner: winner.label,
187
- winner_artifact_path: winner.path,
188
- result_path: resultPath,
189
- attempt_artifact_paths: attemptArtifactPaths,
190
- judge_artifact_paths: judgeArtifactPaths,
191
- bracket_path: bracketPath,
192
- artifact_dir: artifactDir,
193
- };
146
+ const artifactDir = await stableArtifactRoot(ctx, "tournament");
147
+ const attemptsDir = join(artifactDir, "attempts");
148
+ const numAttempts = ctx.inputs.num_attempts ?? 4;
149
+ const maxConcurrency = ctx.inputs.max_concurrency ?? 4;
150
+ const nEvaluations = ctx.inputs.n_evaluations ?? 2;
151
+ const pivotCount = ctx.inputs.pivots ?? 1;
152
+ const seed = ctx.inputs.seed ?? 0;
153
+ const judgesDir = join(artifactDir, "judges");
154
+ await mkdir(attemptsDir, { recursive: true });
155
+ await mkdir(judgesDir, { recursive: true });
156
+
157
+ const attemptArtifactPaths = Array.from({ length: numAttempts }, (_, index) =>
158
+ join(attemptsDir, `attempt-${index + 1}.md`));
159
+ const models = ctx.inputs.models;
160
+ const attemptResults = await ctx.parallel(
161
+ attemptArtifactPaths.map((path, index) => ({
162
+ name: `attempt-${index + 1}`,
163
+ prompt: renderTournamentAttemptPrompt(ctx.inputs.prompt, index + 1),
164
+ context: "fresh" as const,
165
+ output: path,
166
+ outputMode: "file-only" as const,
167
+ ...(models !== undefined && models.length > 0 ? { model: models[index % models.length] } : {}),
168
+ })),
169
+ { concurrency: maxConcurrency, failFast: true },
170
+ );
171
+
172
+ const entrants: Entrant[] = await Promise.all(attemptArtifactPaths.map(async (path, index) => ({
173
+ label: `attempt-${index + 1}`,
174
+ path,
175
+ body: await readFile(path, "utf8"),
176
+ })));
177
+ const criteria = tournamentCriteria(ctx.inputs.criteria);
178
+ const criterionIds = criteria.map((criterion) => criterion.id);
179
+ const plan = plan_comparisons({
180
+ n: entrants.length,
181
+ pivots: pivotCount,
182
+ repeats: nEvaluations,
183
+ seed,
184
+ });
185
+ const weights = Array.from({ length: entrants.length }, () => 0);
186
+ const counts = Array.from({ length: entrants.length }, () => 0);
187
+ const comparisons: ComparisonRecord[] = [];
188
+ const pairs: PairRecord[] = [];
189
+ const judgeArtifactPaths: string[] = [];
190
+ let executed = 0;
191
+
192
+ const runPhase = async (
193
+ phase: "ring" | "pivot",
194
+ jobs: readonly ScoringJob[],
195
+ ): Promise<{ readonly preferences: readonly Preference[]; readonly results: readonly WorkflowTaskResult[] }> => {
196
+ const stages: JudgeStage[] = jobs.map((job) => {
197
+ const slot1 = job.swapped ? job.b : job.a;
198
+ const slot2 = job.swapped ? job.a : job.b;
199
+ const first = entrants[slot1]!;
200
+ const second = entrants[slot2]!;
201
+ const head: SharedHead = {
202
+ task: ctx.inputs.prompt,
203
+ groundTruthNote: JUDGE_GROUND_TRUTH_NOTE,
204
+ candidates: [
205
+ { path: first.path, body: first.body },
206
+ { path: second.path, body: second.body },
207
+ ] satisfies readonly ScoringCandidate[],
208
+ scaleAnchors: VERIFICATION_SCALE.anchors,
209
+ outputFormat: JUDGE_OUTPUT_FORMAT,
210
+ };
211
+ const name = stageName(job);
212
+ const criterionIndex = criterionIds.indexOf(job.criterionId);
213
+ const path = judgeArtifactPath(judgesDir, job, criterionIndex);
214
+ judgeArtifactPaths.push(path);
215
+ return {
216
+ job,
217
+ name,
218
+ path,
219
+ slot1,
220
+ slot2,
221
+ step: {
222
+ name,
223
+ prompt: build_scoring_prompt(head, criteria.find((criterion) => criterion.id === job.criterionId)!),
224
+ context: "fresh" as const,
225
+ reads: scoring_prompt_reads(head),
226
+ schema: judgeScoreSchema,
227
+ },
228
+ };
229
+ });
230
+ if (stages.length === 0) return { preferences: [], results: [] };
231
+ executed += stages.length;
232
+ const results = await warm_first_fan_out(
233
+ ctx,
234
+ stages.map((stage) => stage.step),
235
+ (_step, index) => `${stages[index]!.slot1}:${stages[index]!.slot2}`,
236
+ { concurrency: maxConcurrency, failFast: false },
237
+ );
238
+ const phaseResults = [...results];
239
+ for (const stage of stages) {
240
+ let report = judgeReport(resultFor(results, stage.name)?.structured);
241
+ if (report === undefined) {
242
+ executed += 1;
243
+ const retried = await ctx.task(`${stage.name}-reask`, {
244
+ prompt: `${stage.step.prompt}\nThe previous report was invalid. Return the required JSON schema exactly once.`,
245
+ context: "fresh",
246
+ reads: stage.step.reads,
247
+ schema: judgeScoreSchema,
248
+ });
249
+ phaseResults.push(retried);
250
+ report = judgeReport(retried.structured);
251
+ }
252
+ if (report === undefined) {
253
+ await writeFile(stage.path, `${JSON.stringify({ invalid: true }, null, 2)}\n`);
254
+ comparisons.push({
255
+ a: stage.job.a,
256
+ b: stage.job.b,
257
+ phase,
258
+ criterion_id: stage.job.criterionId,
259
+ rep: stage.job.rep,
260
+ swapped: stage.job.swapped,
261
+ invalid: true,
262
+ judge_artifact_path: stage.path,
263
+ });
264
+ continue;
265
+ }
266
+ await writeFile(stage.path, `${JSON.stringify(report, null, 2)}\n`);
267
+ const scoreA = stage.job.swapped ? report.score_b : report.score_a;
268
+ const scoreB = stage.job.swapped ? report.score_a : report.score_b;
269
+ comparisons.push({
270
+ a: stage.job.a,
271
+ b: stage.job.b,
272
+ phase,
273
+ criterion_id: stage.job.criterionId,
274
+ rep: stage.job.rep,
275
+ swapped: stage.job.swapped,
276
+ score_a: scoreA,
277
+ score_b: scoreB,
278
+ p_ab: soft_win(scoreA, scoreB),
279
+ judge_artifact_path: stage.path,
280
+ });
281
+ }
282
+
283
+ const phaseRecords = comparisons.filter((record) => record.phase === phase);
284
+ const grouped = new Map<string, ComparisonRecord[]>();
285
+ for (const record of phaseRecords) {
286
+ const key = `${record.a}:${record.b}`;
287
+ const group = grouped.get(key);
288
+ if (group === undefined) grouped.set(key, [record]);
289
+ else group.push(record);
290
+ }
291
+ const preferences: Preference[] = [];
292
+ for (const records of grouped.values()) {
293
+ const first = records[0]!;
294
+ const valid = records.filter(scoredComparison);
295
+ if (valid.length === 0) {
296
+ pairs.push({ a: first.a, b: first.b, phase, valid_reports: 0, p_ab: 0.5, invalid: true });
297
+ preferences.push({ a: first.a, b: first.b, p: 0.5 });
298
+ continue;
299
+ }
300
+ const meanScoreA = valid.reduce((sum, record) => sum + record.score_a, 0) / valid.length;
301
+ const meanScoreB = valid.reduce((sum, record) => sum + record.score_b, 0) / valid.length;
302
+ const p = soft_win(meanScoreA, meanScoreB);
303
+ pairs.push({
304
+ a: first.a,
305
+ b: first.b,
306
+ phase,
307
+ valid_reports: valid.length,
308
+ mean_score_a: meanScoreA,
309
+ mean_score_b: meanScoreB,
310
+ p_ab: p,
311
+ });
312
+ preferences.push({ a: first.a, b: first.b, p });
313
+ }
314
+ return { preferences, results: phaseResults };
315
+ };
316
+
317
+ const ringJobs = plan.jobs(plan.ring, criterionIds);
318
+ const ringPhase = await runPhase("ring", ringJobs);
319
+ accumulate(ringPhase.preferences, weights, counts);
320
+ const pivotIndices = select_pivots(weights, counts, pivotCount);
321
+ const pivotJobs = plan.jobs(plan.pivotRounds(pivotIndices), criterionIds);
322
+ const pivotPhase = await runPhase("pivot", pivotJobs);
323
+ accumulate(pivotPhase.preferences, weights, counts);
324
+ const ranking = rank_candidates(weights, counts).map((entry) => ({
325
+ label: entrants[entry.index]!.label,
326
+ index: entry.index,
327
+ meanPreference: entry.meanPreference,
328
+ }));
329
+ const outputRanking = ranking.map(({ label, meanPreference }) => ({ label, meanPreference }));
330
+ const budgetPivots = Math.min(pivotCount, entrants.length);
331
+ // Planned units are judge jobs: scheduled directed pairs × criteria × evaluations.
332
+ const comparisonBudget =
333
+ (entrants.length + budgetPivots * (entrants.length - budgetPivots) + (budgetPivots * (budgetPivots - 1)) / 2) *
334
+ criteria.length * nEvaluations;
335
+ const comparisonPath = join(artifactDir, "comparisons.json");
336
+ const assignment = modelAssignment(entrants, models);
337
+ const phaseUsage = {
338
+ attempts: fold_usage(attemptResults),
339
+ ring: fold_usage(ringPhase.results),
340
+ pivots: fold_usage(pivotPhase.results),
341
+ };
342
+ const baseLedger = {
343
+ task: ctx.inputs.prompt,
344
+ seed,
345
+ params: {
346
+ n: entrants.length,
347
+ pivots: pivotCount,
348
+ n_evaluations: nEvaluations,
349
+ criteria,
350
+ },
351
+ comparisons,
352
+ pairs,
353
+ w: weights,
354
+ c: counts,
355
+ ranking,
356
+ // Budget units are judge stages: planned jobs versus dispatched jobs, including re-asks in executed.
357
+ budget: { planned: comparisonBudget, executed },
358
+ ...(assignment === undefined ? {} : { model_assignment: assignment }),
359
+ };
360
+ const writeLedger = async (reducerUsage: UsageTotals): Promise<void> => {
361
+ const usage = {
362
+ ...phaseUsage,
363
+ reducer: reducerUsage,
364
+ total: combineUsage([phaseUsage.attempts, phaseUsage.ring, phaseUsage.pivots, reducerUsage]),
365
+ };
366
+ await writeFile(comparisonPath, `${JSON.stringify({ ...baseLedger, usage }, null, 2)}\n`);
367
+ };
368
+ await writeLedger(fold_usage([]));
369
+
370
+ const winner = ranking[0]!;
371
+ const winnerEntrant = entrants[winner.index]!;
372
+ const resultPath = join(artifactDir, "winner.md");
373
+ const reducer = await ctx.task("comparisons-reducer", {
374
+ prompt: renderComparisonsReducerPrompt({
375
+ task: ctx.inputs.prompt,
376
+ comparisonsPath: comparisonPath,
377
+ ranking: outputRanking,
378
+ winnerLabel: winner.label,
379
+ winnerPath: winnerEntrant.path,
380
+ }),
381
+ context: "fresh",
382
+ reads: [comparisonPath, winnerEntrant.path, ...judgeArtifactPaths],
383
+ output: resultPath,
384
+ outputMode: "file-only",
385
+ });
386
+ await writeLedger(fold_usage([reducer]));
387
+
388
+ return {
389
+ result: reducer.text,
390
+ winner: winner.label,
391
+ winner_artifact_path: winnerEntrant.path,
392
+ result_path: resultPath,
393
+ attempt_artifact_paths: attemptArtifactPaths,
394
+ judge_artifact_paths: judgeArtifactPaths,
395
+ comparisons_path: comparisonPath,
396
+ ranking: outputRanking,
397
+ seed,
398
+ artifact_dir: artifactDir,
399
+ };
194
400
  }
@@ -1,32 +1,61 @@
1
1
  import type { WorkflowDefinition, WorkflowInputValues, WorkflowOutputValues } from "../src/authoring.js";
2
2
 
3
+ export type TournamentCriterionInput = {
4
+ readonly id?: string;
5
+ readonly name?: string;
6
+ readonly description: string;
7
+ };
8
+
9
+ export type TournamentCriteriaInput =
10
+ | string
11
+ | Record<string, string>
12
+ | string[]
13
+ | TournamentCriterionInput[];
14
+
15
+ export type TournamentRankingEntry = {
16
+ readonly label: string;
17
+ readonly meanPreference: number;
18
+ };
19
+
3
20
  export type TournamentWorkflowInputs = WorkflowInputValues & {
4
- readonly prompt: string;
5
- readonly num_attempts: number;
6
- readonly max_concurrency: number;
21
+ readonly prompt: string;
22
+ readonly num_attempts: number;
23
+ readonly max_concurrency: number;
24
+ readonly n_evaluations: number;
25
+ readonly pivots: number;
26
+ readonly seed: number;
27
+ readonly criteria?: TournamentCriteriaInput;
28
+ readonly models?: string[];
7
29
  };
8
30
 
9
31
  export type TournamentWorkflowRunInputs = WorkflowInputValues & {
10
- readonly prompt: string;
11
- readonly num_attempts?: number;
12
- readonly max_concurrency?: number;
32
+ readonly prompt: string;
33
+ readonly num_attempts?: number;
34
+ readonly max_concurrency?: number;
35
+ readonly n_evaluations?: number;
36
+ readonly pivots?: number;
37
+ readonly seed?: number;
38
+ readonly criteria?: TournamentCriteriaInput;
39
+ readonly models?: string[];
13
40
  };
14
41
 
15
42
  export type TournamentWorkflowOutputs = WorkflowOutputValues & {
16
- readonly result: string;
17
- readonly winner: string;
18
- readonly winner_artifact_path: string;
19
- readonly result_path: string;
20
- readonly attempt_artifact_paths: string[];
21
- readonly judge_artifact_paths: string[];
22
- readonly bracket_path: string;
23
- readonly artifact_dir: string;
43
+ readonly result: string;
44
+ readonly winner: string;
45
+ readonly winner_artifact_path: string;
46
+ readonly result_path: string;
47
+ readonly attempt_artifact_paths: string[];
48
+ readonly judge_artifact_paths: string[];
49
+ readonly comparisons_path: string;
50
+ readonly ranking: TournamentRankingEntry[];
51
+ readonly seed: number;
52
+ readonly artifact_dir: string;
24
53
  };
25
54
 
26
55
  export type TournamentWorkflowDefinition = WorkflowDefinition<
27
- TournamentWorkflowInputs,
28
- TournamentWorkflowOutputs,
29
- TournamentWorkflowRunInputs
56
+ TournamentWorkflowInputs,
57
+ TournamentWorkflowOutputs,
58
+ TournamentWorkflowRunInputs
30
59
  >;
31
60
 
32
61
  declare const workflow: TournamentWorkflowDefinition;