@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.
- package/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/schemas.ts +5 -0
- package/dist/builtin/subagents/src/runs/shared/long-running-guard.ts +3 -1
- package/dist/builtin/subagents/src/runs/shared/progress-trend.ts +69 -0
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +12 -1
- package/dist/builtin/subagents/src/shared/types-results.ts +2 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +41 -1
- package/dist/builtin/workflows/README.md +6 -5
- package/dist/builtin/workflows/builtin/adversarial-verification-prompts.ts +13 -5
- package/dist/builtin/workflows/builtin/adversarial-verification-runner.ts +376 -89
- package/dist/builtin/workflows/builtin/adversarial-verification.d.ts +30 -6
- package/dist/builtin/workflows/builtin/adversarial-verification.ts +14 -9
- package/dist/builtin/workflows/builtin/generate-and-filter-prompts.ts +26 -3
- package/dist/builtin/workflows/builtin/generate-and-filter-runner.ts +18 -14
- package/dist/builtin/workflows/builtin/goal-artifacts.ts +9 -8
- package/dist/builtin/workflows/builtin/goal-convergence.ts +87 -0
- package/dist/builtin/workflows/builtin/goal-ledger.ts +4 -0
- package/dist/builtin/workflows/builtin/goal-prompts.ts +2 -0
- package/dist/builtin/workflows/builtin/goal-reducer.ts +6 -1
- package/dist/builtin/workflows/builtin/goal-reverify.ts +305 -0
- package/dist/builtin/workflows/builtin/goal-runner.ts +75 -10
- package/dist/builtin/workflows/builtin/goal-schemas.ts +7 -0
- package/dist/builtin/workflows/builtin/goal-types.ts +6 -0
- package/dist/builtin/workflows/builtin/loop-until-done-runner.ts +94 -6
- package/dist/builtin/workflows/builtin/loop-until-done.d.ts +8 -0
- package/dist/builtin/workflows/builtin/loop-until-done.ts +15 -0
- package/dist/builtin/workflows/builtin/progress-scoring.ts +230 -0
- package/dist/builtin/workflows/builtin/ralph-core.ts +11 -0
- package/dist/builtin/workflows/builtin/ralph-review-gate.ts +1 -0
- package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +2 -0
- package/dist/builtin/workflows/builtin/ralph-runner.ts +60 -10
- package/dist/builtin/workflows/builtin/selection-math.ts +156 -0
- package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -0
- package/dist/builtin/workflows/builtin/tournament-prompts.ts +57 -75
- package/dist/builtin/workflows/builtin/tournament-runner.ts +384 -178
- package/dist/builtin/workflows/builtin/tournament.d.ts +46 -17
- package/dist/builtin/workflows/builtin/tournament.ts +66 -32
- package/dist/builtin/workflows/builtin/verification-criteria.ts +330 -0
- package/dist/builtin/workflows/builtin/verification-prompts.ts +206 -0
- package/dist/builtin/workflows/builtin/verification-usage.ts +44 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +90 -30
- package/dist/builtin/workflows/skills/show-me/LICENSE.txt +21 -0
- package/dist/builtin/workflows/skills/show-me/SKILL.md +143 -0
- package/dist/builtin/workflows/src/authoring/workflow.ts +8 -0
- package/dist/builtin/workflows/src/authoring.d.ts +1 -1
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +5 -2
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +1 -1
- package/dist/builtin/workflows/src/durable/resume-eligibility.ts +5 -3
- package/dist/builtin/workflows/src/durable/run-timing.ts +41 -10
- package/dist/builtin/workflows/src/durable/tool-primitive.ts +24 -2
- package/dist/builtin/workflows/src/engine/options.ts +1 -0
- package/dist/builtin/workflows/src/engine/primitives/workflow.ts +12 -3
- package/dist/builtin/workflows/src/engine/run-budget.ts +308 -0
- package/dist/builtin/workflows/src/engine/run-returned-status.ts +8 -0
- package/dist/builtin/workflows/src/engine/run-tool-node-lifecycle.ts +6 -0
- package/dist/builtin/workflows/src/engine/run.ts +124 -2
- package/dist/builtin/workflows/src/engine/runtime.ts +9 -0
- package/dist/builtin/workflows/src/extension/config-file-loader.ts +6 -0
- package/dist/builtin/workflows/src/extension/config-loader.ts +24 -1
- package/dist/builtin/workflows/src/extension/dispatcher.ts +6 -5
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +2 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +2975 -843
- package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +51 -4
- package/dist/builtin/workflows/src/extension/public-types.ts +3 -1
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +7 -1
- package/dist/builtin/workflows/src/extension/runtime.ts +22 -10
- package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +5 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +16 -0
- package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +44 -1
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +10 -1
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +14 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +15 -4
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +62 -5
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +2 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -1
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +1 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +7 -0
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +1 -0
- package/dist/builtin/workflows/src/shared/budget-meter.ts +34 -0
- package/dist/builtin/workflows/src/shared/budget.d.ts +67 -0
- package/dist/builtin/workflows/src/shared/budget.ts +127 -0
- package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +92 -8
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +11 -1
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +15 -3
- package/dist/builtin/workflows/src/shared/returned-run-status.ts +35 -2
- package/dist/builtin/workflows/src/shared/store-public-types.ts +4 -1
- package/dist/builtin/workflows/src/shared/store-run-methods.ts +8 -1
- package/dist/builtin/workflows/src/shared/store-stage-methods.ts +1 -0
- package/dist/builtin/workflows/src/shared/store-types.ts +24 -0
- package/dist/builtin/workflows/src/shared/types.ts +3 -0
- package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +1 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.d.ts +3 -0
- package/dist/builtin/workflows/src/shared/workflow-authoring-types.ts +3 -0
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +1 -0
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/extensions/ui-types.d.ts +13 -3
- package/dist/core/extensions/ui-types.d.ts.map +1 -1
- package/dist/core/extensions/ui-types.js +15 -3
- package/dist/core/extensions/ui-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +33 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/main-deferred-startup.d.ts.map +1 -1
- package/dist/main-deferred-startup.js +6 -2
- package/dist/main-deferred-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +4 -0
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive/interactive-tui.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-tui.js +19 -1
- package/dist/modes/interactive/interactive-tui.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +7 -0
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +94 -37
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +1 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +15 -2
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.d.ts +3 -2
- package/dist/modes/rpc/rpc-input-scheduler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-input-scheduler.js +5 -2
- package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
- package/docs/extensions.md +1 -1
- package/docs/quickstart.md +1 -0
- package/docs/skills.md +4 -0
- package/docs/workflows.md +74 -10
- package/npm-shrinkwrap.json +29 -29
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|