@bastani/atomic 0.9.14-alpha.3 → 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 (150) hide show
  1. package/CHANGELOG.md +23 -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/CHANGELOG.md +10 -0
  12. package/dist/builtin/web-access/README.md +3 -6
  13. package/dist/builtin/web-access/index.ts +1 -2
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/web-access/web-search-config.ts +2 -8
  16. package/dist/builtin/web-access/web-search-tool.ts +2 -7
  17. package/dist/builtin/web-access/web-search-workflow.ts +10 -0
  18. package/dist/builtin/workflows/CHANGELOG.md +46 -0
  19. package/dist/builtin/workflows/README.md +6 -5
  20. package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
  21. package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
  22. package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
  23. package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
  24. package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
  25. package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
  26. package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
  27. package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
  28. package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
  29. package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
  30. package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
  31. package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
  32. package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
  33. package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
  34. package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
  35. package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
  36. package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
  37. package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
  38. package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
  39. package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
  40. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
  41. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
  42. package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
  43. package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
  44. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
  45. package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
  46. package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
  47. package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
  48. package/dist/builtin/workflows/builtin/tournament.ts +66 -32
  49. package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
  50. package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
  51. package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
  52. package/dist/builtin/workflows/package.json +1 -1
  53. package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
  54. package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
  55. package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
  56. package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
  57. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  58. package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
  59. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
  60. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
  61. package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
  62. package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
  63. package/dist/builtin/workflows/src/engine/options.ts +1 -0
  64. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
  65. package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
  66. package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
  67. package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
  68. package/dist/builtin/workflows/src/engine/run.ts +124 -2
  69. package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
  70. package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
  71. package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
  72. package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
  73. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
  74. package/dist/builtin/workflows/src/extension/index.bundle.mjs +3033 -872
  75. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
  76. package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
  77. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
  78. package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
  79. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
  80. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
  81. package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
  82. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
  83. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
  84. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
  85. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
  86. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
  87. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
  88. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
  89. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
  90. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
  91. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
  92. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
  93. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
  94. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
  95. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
  96. package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
  97. package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
  98. package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
  99. package/dist/builtin/workflows/src/shared/budget.ts +127 -0
  100. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
  101. package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
  102. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
  103. package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
  104. package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
  105. package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
  106. package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
  107. package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
  108. package/dist/builtin/workflows/src/shared/types.ts +3 -0
  109. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
  110. package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
  111. package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
  112. package/dist/builtin/workflows/src/tui/graph-theme.ts +11 -0
  113. package/dist/builtin/workflows/src/tui/graph-view-render.ts +19 -10
  114. package/dist/builtin/workflows/src/tui/tool-detail.ts +45 -26
  115. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  116. package/dist/core/atomic-guide-command.js +1 -0
  117. package/dist/core/atomic-guide-command.js.map +1 -1
  118. package/dist/core/extensions/ui-types.d.ts +13 -3
  119. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  120. package/dist/core/extensions/ui-types.js +15 -3
  121. package/dist/core/extensions/ui-types.js.map +1 -1
  122. package/dist/core/slash-commands.d.ts.map +1 -1
  123. package/dist/core/slash-commands.js +33 -3
  124. package/dist/core/slash-commands.js.map +1 -1
  125. package/dist/main-deferred-startup.d.ts.map +1 -1
  126. package/dist/main-deferred-startup.js +6 -2
  127. package/dist/main-deferred-startup.js.map +1 -1
  128. package/dist/modes/interactive/interactive-startup.js +4 -0
  129. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  130. package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
  131. package/dist/modes/interactive/interactive-tui.js +19 -1
  132. package/dist/modes/interactive/interactive-tui.js.map +1 -1
  133. package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
  134. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  135. package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
  136. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  137. package/dist/modes/rpc/rpc-client.d.ts +1 -0
  138. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  139. package/dist/modes/rpc/rpc-client.js +15 -2
  140. package/dist/modes/rpc/rpc-client.js.map +1 -1
  141. package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
  142. package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
  143. package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
  144. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  145. package/docs/extensions.md +1 -1
  146. package/docs/quickstart.md +1 -0
  147. package/docs/skills.md +4 -0
  148. package/docs/workflows.md +75 -11
  149. package/npm-shrinkwrap.json +29 -29
  150. package/package.json +2 -2
@@ -4,36 +4,70 @@ import { withSteeringPropagationContext } from "./steering-context.js";
4
4
  import { runTournament } from "./tournament-runner.js";
5
5
 
6
6
  export default workflow({
7
- name: "tournament",
8
- description: "Run several independent whole-task attempts through a balanced pairwise judging bracket and return an auditable winner.",
9
- // The 15-minute default, stated rather than inherited: this is a per-workflow
10
- // product decision, so a future change to the global default must not silently
11
- // re-cadence a long autonomous run.
12
- heartbeatIntervalMinutes: 15,
13
- inputs: {
14
- prompt: Type.String({ description: "Task every competing agent must attempt independently." }),
15
- num_attempts: Type.Integer({
16
- minimum: 2,
17
- maximum: 8,
18
- default: 4,
19
- description: "Number of independent whole-task attempts (2-8).",
20
- }),
21
- max_concurrency: Type.Integer({
22
- minimum: 1,
23
- maximum: 8,
24
- default: 4,
25
- description: "Maximum simultaneously active attempts or pairwise judges (1-8).",
26
- }),
27
- },
28
- outputs: {
29
- result: Type.String({ description: "Compact reference to the final reducer report holding the winning solution and decision trail; read `result_path` for the full report." }),
30
- winner: Type.String({ description: "Stable attempt label selected as the tournament winner." }),
31
- winner_artifact_path: Type.String({ description: "Path to the original winning attempt artifact." }),
32
- result_path: Type.String({ description: "Path to the final reducer report artifact." }),
33
- attempt_artifact_paths: Type.Array(Type.String(), { description: "Paths to every independent attempt artifact." }),
34
- judge_artifact_paths: Type.Array(Type.String(), { description: "Paths to all structured pairwise judge artifacts." }),
35
- bracket_path: Type.String({ description: "Path to the durable JSON bracket with matches, byes, rationales, and winner." }),
36
- artifact_dir: Type.String({ description: "Run-specific directory containing tournament artifacts." }),
37
- },
38
- run: async (ctx) => await runTournament(withSteeringPropagationContext(ctx)),
7
+ name: "tournament",
8
+ description: "Run independent whole-task attempts through a soft-scored pivot-pairing schedule and return an auditable ranking.",
9
+ // The 15-minute default, stated rather than inherited: this is a per-workflow
10
+ // product decision, so a future change to the global default must not silently
11
+ // re-cadence a long autonomous run.
12
+ heartbeatIntervalMinutes: 15,
13
+ inputs: {
14
+ prompt: Type.String({ description: "Task every competing agent must attempt independently." }),
15
+ num_attempts: Type.Integer({
16
+ minimum: 2,
17
+ maximum: 8,
18
+ default: 4,
19
+ description: "Number of independent whole-task attempts (2-8).",
20
+ }),
21
+ max_concurrency: Type.Integer({
22
+ minimum: 1,
23
+ maximum: 8,
24
+ default: 4,
25
+ description: "Maximum simultaneously active attempts or pairwise judges (1-8).",
26
+ }),
27
+ n_evaluations: Type.Integer({
28
+ minimum: 1,
29
+ default: 2,
30
+ description: "Number of repeated evaluations per criterion and directed pair.",
31
+ }),
32
+ pivots: Type.Integer({
33
+ minimum: 1,
34
+ default: 1,
35
+ description: "Number of pivot candidates used for the second comparison phase.",
36
+ }),
37
+ seed: Type.Integer({
38
+ default: 0,
39
+ description: "Seed for the deterministic comparison schedule.",
40
+ }),
41
+ criteria: Type.Optional(Type.Union([
42
+ Type.String(),
43
+ Type.Record(Type.String(), Type.String()),
44
+ Type.Array(Type.String()),
45
+ Type.Array(Type.Object({
46
+ id: Type.Optional(Type.String()),
47
+ name: Type.Optional(Type.String()),
48
+ description: Type.String(),
49
+ }, { additionalProperties: true })),
50
+ ], {
51
+ description: "Optional V1 judge criteria; accepts a markdown rubric, record, string list, or CriterionInput list; omitted uses the default three-criterion rubric.",
52
+ })),
53
+ models: Type.Optional(Type.Array(Type.String(), {
54
+ description: "Optional ordered model ids assigned round-robin to attempt slots.",
55
+ })),
56
+ },
57
+ outputs: {
58
+ result: Type.String({ description: "Compact reference to the final reducer report; read `result_path` for the full report." }),
59
+ winner: Type.String({ description: "Stable attempt label selected as the top-ranked tournament winner." }),
60
+ winner_artifact_path: Type.String({ description: "Path to the original winning attempt artifact." }),
61
+ result_path: Type.String({ description: "Path to the final reducer report artifact." }),
62
+ attempt_artifact_paths: Type.Array(Type.String(), { description: "Paths to every independent attempt artifact." }),
63
+ judge_artifact_paths: Type.Array(Type.String(), { description: "Paths to all per-job structured judge artifacts." }),
64
+ comparisons_path: Type.String({ description: "Path to the durable JSON soft-scored comparisons ledger." }),
65
+ ranking: Type.Array(Type.Object({
66
+ label: Type.String(),
67
+ meanPreference: Type.Number(),
68
+ }, { additionalProperties: false }), { description: "All attempts ordered by mean preference." }),
69
+ seed: Type.Integer({ description: "Seed used for the deterministic comparison schedule." }),
70
+ artifact_dir: Type.String({ description: "Run-specific directory containing tournament artifacts." }),
71
+ },
72
+ run: async (ctx) => await runTournament(withSteeringPropagationContext(ctx)),
39
73
  });
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Shared verification rubric primitive.
3
+ *
4
+ * Doors: `parse_rubric`, `normalize_criteria`, `select_criteria`,
5
+ * `decide_verification`. A CriterionScore can only exist as a schema-valid
6
+ * structured report — an unparseable verifier output is unrepresentable as a
7
+ * vote, so it cannot shift the mean.
8
+ */
9
+ import { Type, type TSchema } from "typebox";
10
+
11
+ export interface Criterion {
12
+ readonly id: string;
13
+ readonly name: string;
14
+ readonly description: string;
15
+ }
16
+
17
+ export interface Criteria {
18
+ readonly groundTruthNote: string;
19
+ readonly criteria: readonly Criterion[];
20
+ }
21
+
22
+ export interface CriterionInput {
23
+ readonly id?: string;
24
+ readonly name?: string;
25
+ readonly description?: string;
26
+ }
27
+
28
+ export interface Finding {
29
+ readonly finding: string;
30
+ readonly severity: "veto" | "blocking" | "note";
31
+ }
32
+
33
+ export interface CriterionScore {
34
+ readonly criterionId: string;
35
+ readonly score: number;
36
+ readonly evidence: readonly string[];
37
+ readonly findings: readonly Finding[];
38
+ }
39
+
40
+ export type RubricErrorCode = "NoCriteria" | "EmptyCriterion";
41
+
42
+ export class RubricError extends Error {
43
+ readonly code: RubricErrorCode;
44
+
45
+ constructor(message: string, code: RubricErrorCode) {
46
+ super(message);
47
+ this.name = code;
48
+ this.code = code;
49
+ }
50
+ }
51
+
52
+ export class NoCriteria extends RubricError {
53
+ constructor(message = "no criteria found — check the `## Criteria` section and its `### Name {#id}` headings") {
54
+ super(message, "NoCriteria");
55
+ this.name = "NoCriteria";
56
+ }
57
+ }
58
+
59
+ export class EmptyCriterion extends RubricError {
60
+ readonly ids: readonly string[];
61
+
62
+ constructor(ids: readonly string[]) {
63
+ super(`criteria have empty instructions: ${ids.join(", ")}`, "EmptyCriterion");
64
+ this.name = "EmptyCriterion";
65
+ this.ids = ids;
66
+ }
67
+ }
68
+
69
+ const CRITERION_ID_ANCHOR = /^(.*?)\s*\{#([A-Za-z0-9_-]+)\}\s*$/;
70
+ const SLUG_MAX_LENGTH = 40;
71
+
72
+ function slug_id(text: string): string {
73
+ const slug = text
74
+ .toLowerCase()
75
+ .replace(/[^a-z0-9]+/g, "_")
76
+ .replace(/^_+|_+$/g, "");
77
+ return slug.slice(0, SLUG_MAX_LENGTH).replace(/_+$/g, "") || "criterion";
78
+ }
79
+
80
+ function dedup_id(cid: string, seen: Set<string>): string {
81
+ let out = cid;
82
+ let n = 1;
83
+ while (seen.has(out)) {
84
+ n += 1;
85
+ out = `${cid}_${n}`;
86
+ }
87
+ seen.add(out);
88
+ return out;
89
+ }
90
+
91
+ function criterion(id: string, name: string, description: string): Criterion {
92
+ return { id, name, description };
93
+ }
94
+
95
+ function strip_html_comments(markdown: string): string {
96
+ let sanitized = markdown;
97
+ while (true) {
98
+ const opener = sanitized.indexOf("<!--");
99
+ if (opener === -1) return sanitized;
100
+ const closer = sanitized.indexOf("-->", opener + 4);
101
+ if (closer === -1) return sanitized.slice(0, opener);
102
+ sanitized = sanitized.slice(0, opener) + sanitized.slice(closer + 3);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Parse a `criteria.md` document into normalized criteria.
108
+ *
109
+ * `# title` is ignored. `## Ground Truth Note` is optional; the first section
110
+ * wins. `## Criteria` owns `### Name {#id}` headings. HTML comments are stripped
111
+ * before parse. Ids slug to lowercase alnum/underscore, ≤40 chars, fallback
112
+ * `"criterion"`, with encounter-order `_2` / `_3` dedup.
113
+ *
114
+ * @throws {NoCriteria} when the Criteria section yields no headings
115
+ * @throws {EmptyCriterion} when any criterion has an empty body
116
+ */
117
+ export function parse_rubric(markdown: string): Criteria {
118
+ const lines = strip_html_comments(markdown).split(/\r?\n/);
119
+
120
+ let groundTruthNote = "";
121
+ let seenGroundTruth = false;
122
+ const criteria: Criterion[] = [];
123
+ const seen = new Set<string>();
124
+
125
+ let section: "ground_truth" | "criteria" | null = null;
126
+ let currentName: string | null = null;
127
+ let currentId: string | null = null;
128
+ let buf: string[] = [];
129
+
130
+ const flush = (): void => {
131
+ const text = buf.join("\n").trim();
132
+ buf = [];
133
+ if (section === "ground_truth") {
134
+ if (!seenGroundTruth) {
135
+ groundTruthNote = text;
136
+ seenGroundTruth = true;
137
+ }
138
+ return;
139
+ }
140
+ if (currentName === null || currentId === null) return;
141
+ criteria.push(criterion(currentId, currentName, text));
142
+ currentName = null;
143
+ currentId = null;
144
+ };
145
+
146
+ for (const line of lines) {
147
+ if (line.startsWith("## ") && !line.startsWith("### ")) {
148
+ flush();
149
+ const heading = line.slice(3).trim().toLowerCase();
150
+ if (heading.includes("ground truth")) {
151
+ section = "ground_truth";
152
+ } else if (heading.includes("criteri")) {
153
+ section = "criteria";
154
+ } else {
155
+ section = null;
156
+ }
157
+ continue;
158
+ }
159
+ if (line.startsWith("### ") && section === "criteria") {
160
+ flush();
161
+ const heading = line.slice(4).trim();
162
+ const anchored = CRITERION_ID_ANCHOR.exec(heading);
163
+ const name = anchored ? anchored[1]!.trim() : heading;
164
+ const rawId = anchored ? anchored[2]!.trim() : slug_id(heading);
165
+ currentName = name;
166
+ currentId = dedup_id(rawId, seen);
167
+ continue;
168
+ }
169
+ if (line.startsWith("# ")) {
170
+ continue;
171
+ }
172
+ buf.push(line);
173
+ }
174
+ flush();
175
+
176
+ if (criteria.length === 0) {
177
+ throw new NoCriteria();
178
+ }
179
+ const empty = criteria.filter((item) => item.description.length === 0).map((item) => item.id);
180
+ if (empty.length > 0) {
181
+ throw new EmptyCriterion(empty);
182
+ }
183
+ return { groundTruthNote, criteria };
184
+ }
185
+
186
+ function isRecord(value: object): value is Record<string, string> {
187
+ return Object.values(value).every((entry) => typeof entry === "string");
188
+ }
189
+
190
+ function isOptionalString(value: object, key: "id" | "name" | "description"): boolean {
191
+ if (!(key in value)) return true;
192
+ return typeof (value as CriterionInput)[key] === "string";
193
+ }
194
+
195
+ function isCriterionInput(value: object): value is CriterionInput {
196
+ return isOptionalString(value, "id") && isOptionalString(value, "name") && isOptionalString(value, "description");
197
+ }
198
+
199
+ function normalize_one(raw: string | CriterionInput, index: number, seen: Set<string>): Criterion {
200
+ const cidRaw = typeof raw === "string" ? "" : String(raw.id ?? "");
201
+ const nameRaw = typeof raw === "string" ? raw : String(raw.name ?? "");
202
+ const desc = typeof raw === "string" ? raw : String(raw.description ?? "");
203
+ if (desc.length === 0) {
204
+ throw new EmptyCriterion([cidRaw || nameRaw || String(index)]);
205
+ }
206
+ const name = nameRaw || cidRaw || slug_id(desc);
207
+ const id = dedup_id(cidRaw || slug_id(name), seen);
208
+ return criterion(id, name, desc);
209
+ }
210
+
211
+ /**
212
+ * Canonicalize any accepted criteria shape into `{id,name,description}[]`.
213
+ *
214
+ * Same slug/dedup rules as `parse_rubric`. Empty descriptions throw
215
+ * `EmptyCriterion`; an empty collection throws `NoCriteria`.
216
+ */
217
+ export function normalize_criteria(
218
+ input: Record<string, string> | readonly string[] | readonly CriterionInput[],
219
+ ): readonly Criterion[] {
220
+ if (input === null || typeof input !== "object") {
221
+ throw new TypeError(`criteria must be a record, string[], or CriterionInput[], got ${typeof input}`);
222
+ }
223
+
224
+ const rawItems: Array<string | CriterionInput> = [];
225
+ if (Array.isArray(input)) {
226
+ for (const [index, raw] of input.entries()) {
227
+ if (typeof raw === "string") {
228
+ rawItems.push(raw);
229
+ continue;
230
+ }
231
+ if (isCriterionInput(raw)) {
232
+ rawItems.push(raw);
233
+ continue;
234
+ }
235
+ throw new TypeError(`criteria[${index}] must be a string or CriterionInput`);
236
+ }
237
+ } else if (isRecord(input)) {
238
+ for (const [name, description] of Object.entries(input)) {
239
+ rawItems.push({ name, description });
240
+ }
241
+ } else {
242
+ throw new TypeError("criteria record values must be strings");
243
+ }
244
+
245
+ const seen = new Set<string>();
246
+ const out = rawItems.map((raw, index) => normalize_one(raw, index, seen));
247
+ if (out.length === 0) {
248
+ throw new NoCriteria("criteria is empty");
249
+ }
250
+ return out;
251
+ }
252
+
253
+ /**
254
+ * Return the subset of `criteria` in `ids` order.
255
+ *
256
+ * Omitted `ids` returns every criterion in the original order. An unknown id
257
+ * throws — nothing is silently dropped.
258
+ */
259
+ export function select_criteria(criteria: readonly Criterion[], ids?: readonly string[]): readonly Criterion[] {
260
+ if (ids === undefined) return criteria;
261
+ const byId = new Map(criteria.map((item) => [item.id, item]));
262
+ const missing = ids.filter((id) => !byId.has(id));
263
+ if (missing.length > 0) {
264
+ throw new Error(`criteria not found: ${missing.join(", ")}`);
265
+ }
266
+ return ids.map((id) => byId.get(id)!);
267
+ }
268
+
269
+ export const VERIFICATION_SCALE: {
270
+ readonly min: 1;
271
+ readonly max: 20;
272
+ readonly anchors: string;
273
+ readonly schema: TSchema;
274
+ } = {
275
+ min: 1,
276
+ max: 20,
277
+ anchors: "1 = certainly fails … 10 = borderline … 20 = verified correct",
278
+ schema: Type.Integer({ minimum: 1, maximum: 20 }),
279
+ };
280
+
281
+ export type Accept = { readonly kind: "accept"; readonly mean: number };
282
+ export type Repair = { readonly kind: "repair"; readonly mean: number; readonly findings: readonly Finding[] };
283
+ export type Indeterminate = { readonly kind: "indeterminate"; readonly missing: number };
284
+ export type VerificationDecision = Accept | Repair | Indeterminate;
285
+
286
+ export type VerificationRound = {
287
+ readonly scores: readonly CriterionScore[];
288
+ readonly invalidCount: number;
289
+ readonly expectedCount: number;
290
+ };
291
+
292
+ export type VerificationPolicy = {
293
+ readonly acceptMean: number;
294
+ readonly quorumFraction: number;
295
+ };
296
+
297
+ function mean_score(scores: readonly CriterionScore[]): number {
298
+ if (scores.length === 0) return 0;
299
+ let total = 0;
300
+ for (const item of scores) total += item.score;
301
+ return total / scores.length;
302
+ }
303
+
304
+ /**
305
+ * Map one round of schema-valid scores to accept / repair / indeterminate.
306
+ *
307
+ * Accept iff quorum holds AND mean ≥ acceptMean AND no `severity: "veto"`
308
+ * finding exists. Any veto forces Repair regardless of mean. Below quorum
309
+ * (`valid < ceil(expected × quorumFraction)`) is Indeterminate.
310
+ *
311
+ * `invalidCount` is metadata only: there is no constructor that turns an
312
+ * unparseable report into a CriterionScore, so a parse failure cannot shift
313
+ * the mean.
314
+ */
315
+ export function decide_verification(round: VerificationRound, policy: VerificationPolicy): VerificationDecision {
316
+ // `invalidCount` is intentionally unread. It cannot enter the mean.
317
+ const { scores, expectedCount } = round;
318
+ const valid = scores.length;
319
+ const quorumNeeded = Math.ceil(expectedCount * policy.quorumFraction);
320
+ if (valid < quorumNeeded) {
321
+ return { kind: "indeterminate", missing: Math.max(0, expectedCount - valid) };
322
+ }
323
+
324
+ const mean = mean_score(scores);
325
+ const findings = scores.flatMap((item) => item.findings);
326
+ if (findings.some((item) => item.severity === "veto") || mean < policy.acceptMean) {
327
+ return { kind: "repair", mean, findings };
328
+ }
329
+ return { kind: "accept", mean };
330
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Prefix-cache-aware verification prompts and warm-first fan-out.
3
+ *
4
+ * A scoring-family prompt is always `SHARED HEAD ‖ VARYING TAIL`: the head
5
+ * contains the task statement, ground-truth note, candidate bodies (or read
6
+ * references when the family is too large), and scale anchors, in that order.
7
+ * The tail contains only the criterion name and description plus the output
8
+ * format instruction. Keeping this ordering invariant means sibling criteria
9
+ * can share the provider's cached prefix byte-for-byte.
10
+ *
11
+ * A pathless candidate is valid while the whole family is inline. If any body
12
+ * exceeds the inline bound, every candidate must provide its caller-bound path;
13
+ * this layer refuses the family rather than inventing a read path.
14
+ */
15
+ import type {
16
+ WorkflowParallelOptions,
17
+ WorkflowRunContext,
18
+ WorkflowTaskResult,
19
+ WorkflowTaskStep,
20
+ } from "../src/shared/types.js";
21
+ import type { Criterion } from "./verification-criteria.js";
22
+ import { VERIFICATION_SCALE } from "./verification-criteria.js";
23
+
24
+ /** Maximum UTF-8 byte length of one candidate body eligible for inlining. */
25
+ export const MAX_INLINE_CANDIDATE_BYTES = 32 * 1024;
26
+
27
+ export interface ScoringCandidate {
28
+ /**
29
+ * Optional only while every candidate body is inline. An oversized family
30
+ * requires a caller-bound path for every candidate; paths are preserved
31
+ * verbatim and are never synthesized.
32
+ */
33
+ readonly path?: string;
34
+ readonly body: string;
35
+ }
36
+
37
+ export interface SharedHead {
38
+ readonly task: string;
39
+ readonly groundTruthNote: string;
40
+ readonly candidates: readonly ScoringCandidate[];
41
+ readonly scaleAnchors?: string;
42
+ readonly outputFormat?: string;
43
+ }
44
+
45
+ export interface WarmFirstFanOutOptions extends WorkflowParallelOptions {
46
+ /** Maximum number of distinct prefixes warmed at once. */
47
+ readonly warmConcurrency?: number;
48
+ }
49
+
50
+ export type WarmFirstFanOutContext = Pick<WorkflowRunContext, "parallel">;
51
+
52
+ const DEFAULT_WARM_CONCURRENCY = 4;
53
+ const DEFAULT_OUTPUT_FORMAT =
54
+ "Call structured_output with criterion_id, score (1–20), evidence, and findings containing finding and severity.";
55
+
56
+ function inlineCandidates(head: SharedHead): boolean {
57
+ return head.candidates.every(
58
+ (candidate) => Buffer.byteLength(candidate.body, "utf8") <= MAX_INLINE_CANDIDATE_BYTES,
59
+ );
60
+ }
61
+
62
+ function fallbackReadPaths(head: SharedHead): readonly string[] | undefined {
63
+ if (inlineCandidates(head)) return undefined;
64
+ const paths: string[] = [];
65
+ for (const [index, candidate] of head.candidates.entries()) {
66
+ if (candidate.path === undefined) {
67
+ throw new TypeError(
68
+ `Oversized candidate family requires a caller-bound path for every candidate; candidate ${index + 1} has no path.`,
69
+ );
70
+ }
71
+ paths.push(candidate.path);
72
+ }
73
+ return paths;
74
+ }
75
+
76
+ function candidateSection(head: SharedHead, readPaths: readonly string[] | undefined): string {
77
+ return head.candidates
78
+ .map((candidate, index) => {
79
+ if (readPaths === undefined) {
80
+ return `<candidate index="${index + 1}">\n${candidate.body}\n</candidate>`;
81
+ }
82
+ return `<candidate index="${index + 1}" source="read">\nRead candidate from ${readPaths[index]!}.\n</candidate>`;
83
+ })
84
+ .join("\n");
85
+ }
86
+
87
+ /**
88
+ * Return candidate paths required by the read fallback.
89
+ *
90
+ * The whole family switches together: either every body is in the shared head,
91
+ * or every candidate is named as a caller-provided read. Order and duplicate
92
+ * paths are kept; an oversized pathless family throws instead of guessing.
93
+ */
94
+ export function scoring_prompt_reads(head: SharedHead): readonly string[] {
95
+ return fallbackReadPaths(head) ?? [];
96
+ }
97
+
98
+ /**
99
+ * Build one scoring prompt with a byte-identical shared head and criterion tail.
100
+ * Candidate bodies are measured in UTF-8 bytes, not JavaScript code units. If
101
+ * any body exceeds `MAX_INLINE_CANDIDATE_BYTES`, no body is inlined for the
102
+ * family, preserving the same head for every sibling criterion.
103
+ */
104
+ export function build_scoring_prompt(head: SharedHead, criterion: Criterion): string {
105
+ const readPaths = fallbackReadPaths(head);
106
+ const sharedHead = [
107
+ "<scoring_head>",
108
+ "<task_statement>",
109
+ head.task,
110
+ "</task_statement>",
111
+ "<ground_truth_note>",
112
+ head.groundTruthNote,
113
+ "</ground_truth_note>",
114
+ "<candidates>",
115
+ candidateSection(head, readPaths),
116
+ "</candidates>",
117
+ "<scale_anchors>",
118
+ head.scaleAnchors ?? VERIFICATION_SCALE.anchors,
119
+ "</scale_anchors>",
120
+ ].join("\n");
121
+ const varyingTail = [
122
+ "<criterion>",
123
+ `<name>${criterion.name}</name>`,
124
+ `<description>${criterion.description}</description>`,
125
+ "</criterion>",
126
+ "<output_format>",
127
+ head.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
128
+ "</output_format>",
129
+ ].join("\n");
130
+ return `${sharedHead}\n\n${varyingTail}`;
131
+ }
132
+
133
+ function boundedConcurrency(
134
+ stepCount: number,
135
+ phaseCap: number | undefined,
136
+ inheritedCap: number | undefined,
137
+ ): number {
138
+ return Math.min(stepCount, phaseCap ?? stepCount, inheritedCap ?? stepCount);
139
+ }
140
+
141
+ /**
142
+ * Run one warm step per prefix, then release the remaining steps. Prefix keys
143
+ * are caller-owned; first-seen order is deterministic. Warm failures are
144
+ * observed without fail-fast, and the second phase is always attempted before
145
+ * the warm error is rethrown. Successful results are returned in input order.
146
+ */
147
+ export async function warm_first_fan_out<K>(
148
+ ctx: WarmFirstFanOutContext,
149
+ steps: readonly WorkflowTaskStep[],
150
+ prefixKeyOf: (step: WorkflowTaskStep, index: number) => K,
151
+ options: WarmFirstFanOutOptions = {},
152
+ ): Promise<WorkflowTaskResult[]> {
153
+ const warmIndices: number[] = [];
154
+ const restIndices: number[] = [];
155
+ const seen = new Set<K>();
156
+ for (const [index, step] of steps.entries()) {
157
+ const prefixKey = prefixKeyOf(step, index);
158
+ if (seen.has(prefixKey)) restIndices.push(index);
159
+ else {
160
+ seen.add(prefixKey);
161
+ warmIndices.push(index);
162
+ }
163
+ }
164
+
165
+ const { warmConcurrency, ...parallelOptions } = options;
166
+ const inheritedConcurrency = parallelOptions.concurrency;
167
+ const resultsByIndex = new Map<number, WorkflowTaskResult>();
168
+ let warmFailure: Error | undefined;
169
+ if (warmIndices.length > 0) {
170
+ const warmSteps = warmIndices.map((index) => steps[index]!);
171
+ try {
172
+ const warmResults = await ctx.parallel(warmSteps, {
173
+ ...parallelOptions,
174
+ concurrency: boundedConcurrency(
175
+ warmSteps.length,
176
+ warmConcurrency ?? DEFAULT_WARM_CONCURRENCY,
177
+ inheritedConcurrency,
178
+ ),
179
+ failFast: false,
180
+ });
181
+ for (const [resultIndex, result] of warmResults.entries()) {
182
+ const originalIndex = warmIndices[resultIndex];
183
+ if (originalIndex !== undefined) resultsByIndex.set(originalIndex, result);
184
+ }
185
+ } catch (error) {
186
+ warmFailure = error instanceof Error ? error : new Error(String(error));
187
+ }
188
+ }
189
+
190
+ if (restIndices.length > 0) {
191
+ const restSteps = restIndices.map((index) => steps[index]!);
192
+ const restResults = await ctx.parallel(restSteps, {
193
+ ...parallelOptions,
194
+ concurrency: boundedConcurrency(restSteps.length, undefined, inheritedConcurrency),
195
+ });
196
+ for (const [resultIndex, result] of restResults.entries()) {
197
+ const originalIndex = restIndices[resultIndex];
198
+ if (originalIndex !== undefined) resultsByIndex.set(originalIndex, result);
199
+ }
200
+ }
201
+
202
+ if (warmFailure !== undefined) throw warmFailure;
203
+ return steps
204
+ .map((_, index) => resultsByIndex.get(index))
205
+ .filter((result): result is WorkflowTaskResult => result !== undefined);
206
+ }
@@ -0,0 +1,44 @@
1
+ import type { WorkflowTaskResult } from "../src/shared/types.js";
2
+
3
+ export type UsageTotals = {
4
+ calls: number;
5
+ input: number;
6
+ output: number;
7
+ cacheRead: number;
8
+ cacheWrite: number;
9
+ cost: number;
10
+ turns: number;
11
+ cacheHitRate: number;
12
+ };
13
+
14
+ export function fold_usage(results: readonly WorkflowTaskResult[]): UsageTotals {
15
+ const totals = {
16
+ calls: 0,
17
+ input: 0,
18
+ output: 0,
19
+ cacheRead: 0,
20
+ cacheWrite: 0,
21
+ cost: 0,
22
+ turns: 0,
23
+ };
24
+
25
+ for (const result of results) {
26
+ for (const attempt of result.modelAttempts ?? []) {
27
+ totals.calls += 1;
28
+ const usage = attempt.usage;
29
+ if (usage === undefined) continue;
30
+ totals.input += usage.input ?? 0;
31
+ totals.output += usage.output ?? 0;
32
+ totals.cacheRead += usage.cacheRead ?? 0;
33
+ totals.cacheWrite += usage.cacheWrite ?? 0;
34
+ totals.cost += usage.cost ?? 0;
35
+ totals.turns += usage.turns ?? 0;
36
+ }
37
+ }
38
+
39
+ const cacheDenominator = totals.input + totals.cacheRead;
40
+ return {
41
+ ...totals,
42
+ cacheHitRate: cacheDenominator === 0 ? 0 : totals.cacheRead / cacheDenominator,
43
+ };
44
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.14-alpha.3",
3
+ "version": "0.9.14-alpha.5",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [