@yycholla/pi-dynamic-workflows 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +325 -0
- package/assets/readme/hero.png +0 -0
- package/assets/readme/hero.svg +70 -0
- package/assets/readme/package-cover.png +0 -0
- package/assets/readme/package-cover.svg +66 -0
- package/assets/readme/workflow.png +0 -0
- package/assets/readme/workflow.svg +77 -0
- package/dist/accept-workflow-guidance.d.ts +14 -0
- package/dist/accept-workflow-guidance.js +53 -0
- package/dist/adversarial-review.d.ts +30 -0
- package/dist/adversarial-review.js +107 -0
- package/dist/agent-history.d.ts +20 -0
- package/dist/agent-history.js +122 -0
- package/dist/agent-registry.d.ts +83 -0
- package/dist/agent-registry.js +190 -0
- package/dist/agent.d.ts +364 -0
- package/dist/agent.js +714 -0
- package/dist/builtin-commands.d.ts +19 -0
- package/dist/builtin-commands.js +251 -0
- package/dist/builtin-workflows.d.ts +45 -0
- package/dist/builtin-workflows.js +121 -0
- package/dist/code-review.d.ts +26 -0
- package/dist/code-review.js +181 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +44 -0
- package/dist/deep-research.d.ts +30 -0
- package/dist/deep-research.js +124 -0
- package/dist/display.d.ts +134 -0
- package/dist/display.js +248 -0
- package/dist/effort-command.d.ts +28 -0
- package/dist/effort-command.js +68 -0
- package/dist/enums.d.ts +69 -0
- package/dist/enums.js +78 -0
- package/dist/errors.d.ts +113 -0
- package/dist/errors.js +140 -0
- package/dist/extension-reload.d.ts +37 -0
- package/dist/extension-reload.js +78 -0
- package/dist/fs-persistence.d.ts +63 -0
- package/dist/fs-persistence.js +102 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +35 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.js +66 -0
- package/dist/model-routing.d.ts +30 -0
- package/dist/model-routing.js +50 -0
- package/dist/model-spec.d.ts +29 -0
- package/dist/model-spec.js +252 -0
- package/dist/model-tier-config.d.ts +133 -0
- package/dist/model-tier-config.js +249 -0
- package/dist/run-persistence.d.ts +180 -0
- package/dist/run-persistence.js +294 -0
- package/dist/saved-commands.d.ts +28 -0
- package/dist/saved-commands.js +100 -0
- package/dist/shared-store.d.ts +98 -0
- package/dist/shared-store.js +212 -0
- package/dist/structured-output.d.ts +19 -0
- package/dist/structured-output.js +30 -0
- package/dist/task-panel.d.ts +61 -0
- package/dist/task-panel.js +422 -0
- package/dist/usage-limit-scheduler.d.ts +145 -0
- package/dist/usage-limit-scheduler.js +368 -0
- package/dist/web-tools.d.ts +20 -0
- package/dist/web-tools.js +120 -0
- package/dist/workflow-authoring-coverage.d.ts +70 -0
- package/dist/workflow-authoring-coverage.js +421 -0
- package/dist/workflow-authoring-reference.d.ts +20 -0
- package/dist/workflow-authoring-reference.js +156 -0
- package/dist/workflow-capability-contract.d.ts +131 -0
- package/dist/workflow-capability-contract.js +604 -0
- package/dist/workflow-commands.d.ts +18 -0
- package/dist/workflow-commands.js +260 -0
- package/dist/workflow-comprehension.d.ts +133 -0
- package/dist/workflow-comprehension.js +1321 -0
- package/dist/workflow-context-measurement.d.ts +72 -0
- package/dist/workflow-context-measurement.js +213 -0
- package/dist/workflow-control-tool.d.ts +30 -0
- package/dist/workflow-control-tool.js +176 -0
- package/dist/workflow-delivery-choice.d.ts +20 -0
- package/dist/workflow-delivery-choice.js +48 -0
- package/dist/workflow-editor.d.ts +93 -0
- package/dist/workflow-editor.js +363 -0
- package/dist/workflow-manager.d.ts +492 -0
- package/dist/workflow-manager.js +1124 -0
- package/dist/workflow-paths.d.ts +22 -0
- package/dist/workflow-paths.js +46 -0
- package/dist/workflow-release-gate.d.ts +39 -0
- package/dist/workflow-release-gate.js +309 -0
- package/dist/workflow-saved.d.ts +38 -0
- package/dist/workflow-saved.js +126 -0
- package/dist/workflow-settings.d.ts +70 -0
- package/dist/workflow-settings.js +131 -0
- package/dist/workflow-tool.d.ts +71 -0
- package/dist/workflow-tool.js +367 -0
- package/dist/workflow-ui.d.ts +182 -0
- package/dist/workflow-ui.js +1587 -0
- package/dist/workflow.d.ts +333 -0
- package/dist/workflow.js +1151 -0
- package/dist/workflows-models-command.d.ts +31 -0
- package/dist/workflows-models-command.js +156 -0
- package/dist/worktree.d.ts +25 -0
- package/dist/worktree.js +61 -0
- package/extensions/workflow.ts +151 -0
- package/package.json +104 -0
- package/skills/workflow-authoring/SKILL.md +30 -0
- package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
- package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
- package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
- package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
- package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
- package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
- package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
- package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
- package/skills/workflow-authoring/examples/structured-output.js +37 -0
- package/skills/workflow-authoring/examples/tournament.js +81 -0
- package/skills/workflow-authoring/examples/validated-gate.js +63 -0
- package/skills/workflow-authoring/references/capabilities.md +41 -0
- package/skills/workflow-authoring/references/capability-details.md +357 -0
- package/skills/workflow-authoring/references/common-helpers.md +4 -0
- package/skills/workflow-authoring/references/debugging.md +27 -0
- package/skills/workflow-authoring/references/focused-recipes.md +11 -0
- package/skills/workflow-authoring/references/helpers.md +9 -0
- package/skills/workflow-authoring/references/lifecycle.md +33 -0
- package/skills/workflow-authoring/references/pattern-selection.md +15 -0
- package/skills/workflow-authoring/references/quality-helpers.md +8 -0
- package/skills/workflow-authoring/references/registry-ownership.md +15 -0
- package/skills/workflow-authoring/references/retry-helper.md +5 -0
- package/skills/workflow-authoring/references/review.md +43 -0
- package/skills/workflow-authoring/references/runtime.md +27 -0
- package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
- package/skills/workflow-authoring/references/versions.md +13 -0
- package/skills/workflow-patterns/SKILL.md +51 -0
- package/src/accept-workflow-guidance.ts +71 -0
- package/src/adversarial-review.ts +120 -0
- package/src/agent-history.ts +157 -0
- package/src/agent-registry.ts +221 -0
- package/src/agent.ts +929 -0
- package/src/builtin-commands.ts +286 -0
- package/src/builtin-workflows.ts +155 -0
- package/src/code-review.ts +183 -0
- package/src/config.ts +55 -0
- package/src/deep-research.ts +135 -0
- package/src/display.ts +367 -0
- package/src/effort-command.ts +87 -0
- package/src/enums.ts +77 -0
- package/src/errors.ts +199 -0
- package/src/extension-reload.ts +100 -0
- package/src/fs-persistence.ts +124 -0
- package/src/index.ts +176 -0
- package/src/logger.ts +88 -0
- package/src/model-routing.ts +73 -0
- package/src/model-spec.ts +309 -0
- package/src/model-tier-config.ts +296 -0
- package/src/run-persistence.ts +484 -0
- package/src/saved-commands.ts +115 -0
- package/src/shared-store.ts +228 -0
- package/src/structured-output.ts +47 -0
- package/src/task-panel.ts +490 -0
- package/src/usage-limit-scheduler.ts +432 -0
- package/src/web-tools.ts +124 -0
- package/src/workflow-authoring-coverage.ts +486 -0
- package/src/workflow-authoring-reference.ts +186 -0
- package/src/workflow-capability-contract.ts +806 -0
- package/src/workflow-commands.ts +287 -0
- package/src/workflow-comprehension.ts +1673 -0
- package/src/workflow-context-measurement.ts +262 -0
- package/src/workflow-control-tool.ts +238 -0
- package/src/workflow-delivery-choice.ts +69 -0
- package/src/workflow-editor.ts +444 -0
- package/src/workflow-manager.ts +1405 -0
- package/src/workflow-paths.ts +63 -0
- package/src/workflow-release-gate.ts +529 -0
- package/src/workflow-saved.ts +180 -0
- package/src/workflow-settings.ts +194 -0
- package/src/workflow-tool.ts +464 -0
- package/src/workflow-ui.ts +1789 -0
- package/src/workflow.ts +1615 -0
- package/src/workflows-models-command.ts +211 -0
- package/src/worktree.ts +76 -0
|
@@ -0,0 +1,1673 @@
|
|
|
1
|
+
import { setImmediate as pause } from "node:timers/promises";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { TSchema } from "typebox";
|
|
5
|
+
import type { AgentRunOptions } from "./agent.js";
|
|
6
|
+
import { ComprehensionSuite, ComprehensionTaskKind } from "./enums.js";
|
|
7
|
+
import { ModelGenerationError, WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
8
|
+
import { parseWorkflowScript, runWorkflow, type WorkflowRuntimeEvent } from "./workflow.js";
|
|
9
|
+
|
|
10
|
+
/** Re-exported scenario groups and authoring operations used by the optional comprehension CLI. */
|
|
11
|
+
export { ComprehensionSuite, ComprehensionTaskKind } from "./enums.js";
|
|
12
|
+
|
|
13
|
+
/** Prompt and expected authoring branch for one optional model scenario. */
|
|
14
|
+
export interface ComprehensionScenario {
|
|
15
|
+
id: string;
|
|
16
|
+
suite: ComprehensionSuite;
|
|
17
|
+
kind: ComprehensionTaskKind;
|
|
18
|
+
prompt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ENVELOPE =
|
|
22
|
+
"Return one complete plain-JavaScript workflow. It must start with export const meta, call agent() at least once, use unique labels, and explicitly return JSON-serializable data. Do not use imports.";
|
|
23
|
+
|
|
24
|
+
/** Stable quick, core, and coverage scenarios available to provider and replay runs. */
|
|
25
|
+
export const COMPREHENSION_SCENARIOS: readonly ComprehensionScenario[] = [
|
|
26
|
+
{
|
|
27
|
+
id: "quick-write",
|
|
28
|
+
suite: ComprehensionSuite.QUICK,
|
|
29
|
+
kind: ComprehensionTaskKind.WRITE,
|
|
30
|
+
prompt: `${ENVELOPE}\nWrite a small workflow that asks two independent agents to summarize alpha and beta concurrently, then returns both labeled results. Consult any installed skill that applies before authoring.`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "full-write",
|
|
34
|
+
suite: ComprehensionSuite.FULL,
|
|
35
|
+
kind: ComprehensionTaskKind.WRITE,
|
|
36
|
+
prompt: `${ENVELOPE}\nWrite a fan-out workflow for work units alpha and beta. Use common authoring with parallel calls and require structured output from each agent before JavaScript reads fields. The deterministic beta agent may fail, so return completed data and a missing-work ledger that preserves beta by identity. Consult any installed skill that applies.`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "full-edit",
|
|
40
|
+
suite: ComprehensionSuite.FULL,
|
|
41
|
+
kind: ComprehensionTaskKind.EDIT,
|
|
42
|
+
prompt: `${ENVELOPE}\nEdit the workflow below. It must declare and enter a phase with a positive phase budget, ask one preparation agent, then invoke the saved workflow named child-workflow sequentially for alpha and beta with workflow( and return both child results. Consult any installed skill that applies.\n\nCurrent workflow:\nexport const meta = { name: "nested", description: "broken" }\nreturn await workflow("child-workflow", { id: "alpha" })`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "full-review",
|
|
46
|
+
suite: ComprehensionSuite.FULL,
|
|
47
|
+
kind: ComprehensionTaskKind.REVIEW,
|
|
48
|
+
prompt: `${ENVELOPE}\nReview and correct the workflow below. The corrected workflow must adversarially verify an agent-produced claim with exactly three reviewers, an inclusive threshold of 0.6, and source/logic lenses cycled across reviewers. One deterministic reviewer may fail. Return the claim plus the helper's complete verdict unchanged so successful-vote counts and missing review coverage remain visible. Consult any installed skill that applies.\n\nCurrent workflow:\nexport const meta = { name: "review", description: "broken" }\nconst claim = await agent("claim")\nreturn claim`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "full-debug",
|
|
52
|
+
suite: ComprehensionSuite.FULL,
|
|
53
|
+
kind: ComprehensionTaskKind.DEBUG,
|
|
54
|
+
prompt: `${ENVELOPE}\nDebug and correct the workflow below. Use a bounded control helper (retry or gate), with at most three attempts. Require structured agent output with the fields acceptable (boolean) and answer (string), retry when acceptable is false, and return the accepted agent result plus attempt outcome. The deterministic first result is unacceptable and the next is acceptable. Consult any installed skill that applies.\n\nCurrent workflow:\nexport const meta = { name: "debug", description: "broken" }\nwhile (true) await agent("try")`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "full-loop",
|
|
58
|
+
suite: ComprehensionSuite.FULL,
|
|
59
|
+
kind: ComprehensionTaskKind.WRITE,
|
|
60
|
+
prompt: `${ENVELOPE}\nWrite a bounded discovery workflow that stops after two consecutive successful dry rounds, with at most five rounds. Each round must call one uniquely labeled agent with structured output containing a findings array of { id, evidence }. The deterministic agent responses provide one alpha finding, then a recoverable failure, then two empty successes. Derive findings, failures, and stopping state from the actual agent return values; do not hard-code the announced sequence. A failed round must not count as dry. Return deduplicated findings, failed-round identity, a truthful termination reason, and complete: false whenever any round failed. Consult any installed skill that applies.`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "full-retry",
|
|
64
|
+
suite: ComprehensionSuite.FULL,
|
|
65
|
+
kind: ComprehensionTaskKind.DEBUG,
|
|
66
|
+
prompt: `${ENVELOPE}\nWrite a workflow using retry() with at most three total attempts. The thunk receives a zero-based attempt index and must call one uniquely labeled structured agent per attempt for { acceptable: boolean, answer: string }. Use a synchronous null-safe acceptance predicate. The deterministic first result is unacceptable and the second is acceptable. Return the accepted agent result, an explicit exhausted boolean, and a ledger containing every attempt result. Consult any installed skill that applies.`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "coverage-fan-out-synthesize",
|
|
70
|
+
suite: ComprehensionSuite.COVERAGE,
|
|
71
|
+
kind: ComprehensionTaskKind.WRITE,
|
|
72
|
+
prompt: `${ENVELOPE}\nWrite a fan-out-and-synthesize workflow for two independent research briefs identified as climate and transport. Run both structured research agents concurrently and wait for the complete result set before synthesis. The deterministic transport agent may fail recoverably. Then call exactly one structured synthesis agent with both intended brief identities, the actual climate result, and transport represented as missing rather than omitted. Return the contributor coverage ledger and the actual synthesis result. Consult any installed skill that applies.`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "coverage-generate-filter",
|
|
76
|
+
suite: ComprehensionSuite.COVERAGE,
|
|
77
|
+
kind: ComprehensionTaskKind.WRITE,
|
|
78
|
+
prompt: `${ENVELOPE}\nWrite a generate-and-filter workflow. Run exactly two structured generator agents concurrently; each returns candidate objects with id and proposal fields, and the deterministic outputs contain one duplicate id. After all generation finishes, use JavaScript to deduplicate by id and cap the retained set at three before calling one uniquely labeled structured filter agent per retained candidate. Return the actual generator outputs, retained candidate identities, accepted filter results, and any filter failures. Consult any installed skill that applies.`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "coverage-judge-panel",
|
|
82
|
+
suite: ComprehensionSuite.COVERAGE,
|
|
83
|
+
kind: ComprehensionTaskKind.REVIEW,
|
|
84
|
+
prompt: `${ENVELOPE}\nWrite a workflow that produces exactly two structured candidate attempts concurrently, then passes the actual attempts to judgePanel() with exactly three judges and a concrete correctness rubric. One deterministic judgment may fail recoverably. Return both produced attempts and the helper's winning result unchanged, including its index, attempt, mean score, and surviving judgments. Consult any installed skill that applies.`,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/** Skill discovery and read calls observed while the parent model authored a workflow. */
|
|
89
|
+
export interface SkillLoadingEvidence {
|
|
90
|
+
discovered: boolean;
|
|
91
|
+
loaded: boolean;
|
|
92
|
+
toolCalls: Array<{ tool: string; path?: string }>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Provider-reported usage attached to one generated workflow. */
|
|
96
|
+
export interface ComprehensionTokenUsage {
|
|
97
|
+
input: number;
|
|
98
|
+
output: number;
|
|
99
|
+
total: number;
|
|
100
|
+
cost: number;
|
|
101
|
+
cacheRead: number;
|
|
102
|
+
cacheWrite: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Workflow source and generation evidence supplied to the deterministic replay seam. */
|
|
106
|
+
export interface ModelGeneration {
|
|
107
|
+
workflow: string;
|
|
108
|
+
skillLoadingEvidence: SkillLoadingEvidence;
|
|
109
|
+
tokenUsage: ComprehensionTokenUsage;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Re-exported generation failure that retains loading and usage evidence. */
|
|
113
|
+
export { ModelGenerationError } from "./errors.js";
|
|
114
|
+
|
|
115
|
+
interface RuntimeCall {
|
|
116
|
+
index: number;
|
|
117
|
+
completedIndex: number | null;
|
|
118
|
+
label: string;
|
|
119
|
+
prompt: string;
|
|
120
|
+
phase: string | null;
|
|
121
|
+
structured: boolean;
|
|
122
|
+
scenarioRole: "generator" | "filter" | null;
|
|
123
|
+
status: "returned" | "null";
|
|
124
|
+
result: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type RuntimeEvent = WorkflowRuntimeEvent & { index: number };
|
|
128
|
+
|
|
129
|
+
interface ScenarioFixtureState {
|
|
130
|
+
attempt: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface GeneratorFixtureValue {
|
|
134
|
+
value: unknown;
|
|
135
|
+
consumed: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ScenarioAgentRequest {
|
|
139
|
+
prompt: string;
|
|
140
|
+
label: string;
|
|
141
|
+
schema: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type ScenarioAgentResponse = { status: "returned"; result: unknown } | { status: "null"; message: string };
|
|
145
|
+
|
|
146
|
+
type ScenarioAgentFixture = (request: ScenarioAgentRequest, state: ScenarioFixtureState) => ScenarioAgentResponse;
|
|
147
|
+
|
|
148
|
+
const ordinaryAgentFixture: ScenarioAgentFixture = ({ prompt, label, schema }) => ({
|
|
149
|
+
status: "returned",
|
|
150
|
+
result: schema ? sampleSchema(schema, label || "agent") : `result:${label || prompt}`,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function resolveWriteTaskIdentity(label: string, prompt: string): string | undefined {
|
|
154
|
+
return resolveIdentity(label, prompt, ["alpha", "beta"]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveIdentity(label: string, prompt: string, identities: readonly string[]): string | undefined {
|
|
158
|
+
const labelIdentities = identities.filter((identity) => new RegExp(`\\b${identity}\\b`, "i").test(label));
|
|
159
|
+
if (labelIdentities.length === 1) {
|
|
160
|
+
return labelIdentities[0];
|
|
161
|
+
}
|
|
162
|
+
return identities
|
|
163
|
+
.map((identity) => ({ identity, position: prompt.search(new RegExp(`\\b${identity}\\b`, "i")) }))
|
|
164
|
+
.filter(({ position }) => position >= 0)
|
|
165
|
+
.sort((left, right) => left.position - right.position)[0]?.identity;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveExclusiveIdentity(label: string, prompt: string, identities: readonly string[]): string | undefined {
|
|
169
|
+
const labelIdentities = identities.filter((identity) => new RegExp(`\\b${identity}\\b`, "i").test(label));
|
|
170
|
+
if (labelIdentities.length === 1) return labelIdentities[0];
|
|
171
|
+
const promptIdentities = identities.filter((identity) => new RegExp(`\\b${identity}\\b`, "i").test(prompt));
|
|
172
|
+
return promptIdentities.length === 1 ? promptIdentities[0] : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveGenerateFilterIdentity(
|
|
176
|
+
label: string,
|
|
177
|
+
prompt: string,
|
|
178
|
+
identities: readonly string[],
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const labelIdentity = resolveExclusiveIdentity(label, "", identities);
|
|
181
|
+
if (labelIdentity) {
|
|
182
|
+
return labelIdentity;
|
|
183
|
+
}
|
|
184
|
+
const candidateIdentities = jsonValuesInText(prompt)
|
|
185
|
+
.flatMap((value) => generatedCandidatesFromResult(value) ?? [])
|
|
186
|
+
.flatMap(({ id }) => (typeof id === "string" && identities.includes(id) ? [id] : []));
|
|
187
|
+
if (new Set(candidateIdentities).size === 1) {
|
|
188
|
+
return candidateIdentities[0];
|
|
189
|
+
}
|
|
190
|
+
return resolveIdentity(label, prompt, identities);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const fullWriteAgentFixture: ScenarioAgentFixture = (request) => {
|
|
194
|
+
const taskIdentity = resolveWriteTaskIdentity(request.label, request.prompt);
|
|
195
|
+
if (taskIdentity === "beta") {
|
|
196
|
+
return { status: "null", message: "deterministic beta failure" };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
status: "returned",
|
|
200
|
+
result: request.schema
|
|
201
|
+
? sampleSchema(request.schema, request.label || "agent", taskIdentity)
|
|
202
|
+
: `result:${request.label || request.prompt}`,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const SCENARIO_AGENT_FIXTURES: Readonly<Record<string, ScenarioAgentFixture>> = {
|
|
207
|
+
"quick-write": ordinaryAgentFixture,
|
|
208
|
+
"full-write": fullWriteAgentFixture,
|
|
209
|
+
"full-edit": ordinaryAgentFixture,
|
|
210
|
+
"full-review": (request) => {
|
|
211
|
+
const reviewerMatch = /^verify (\d+)$/.exec(request.label);
|
|
212
|
+
if (!reviewerMatch) {
|
|
213
|
+
return ordinaryAgentFixture(request, { attempt: 0 });
|
|
214
|
+
}
|
|
215
|
+
const reviewer = Number(reviewerMatch[1]);
|
|
216
|
+
if (reviewer === 3) {
|
|
217
|
+
return { status: "null", message: "deterministic reviewer failure" };
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
status: "returned",
|
|
221
|
+
result: { real: reviewer === 1, reason: `reviewer-${reviewer}` },
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
"full-debug": (request, state) => {
|
|
225
|
+
state.attempt++;
|
|
226
|
+
const attemptIdentity = request.label || `attempt-${state.attempt}`;
|
|
227
|
+
return {
|
|
228
|
+
status: "returned",
|
|
229
|
+
result: {
|
|
230
|
+
acceptable: state.attempt > 1,
|
|
231
|
+
answer: state.attempt > 1 ? `accepted:${attemptIdentity}` : `invalid:${attemptIdentity}`,
|
|
232
|
+
feedback: state.attempt > 1 ? "" : "add concrete detail",
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
"full-loop": (request, state) => {
|
|
237
|
+
state.attempt++;
|
|
238
|
+
if (state.attempt === 2) {
|
|
239
|
+
return { status: "null", message: "deterministic discovery round failure" };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
status: "returned",
|
|
243
|
+
result: {
|
|
244
|
+
findings: state.attempt === 1 ? [{ id: "alpha", evidence: `evidence:${request.label}` }] : [],
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
"full-retry": (request, state) => {
|
|
249
|
+
state.attempt++;
|
|
250
|
+
return {
|
|
251
|
+
status: "returned",
|
|
252
|
+
result: {
|
|
253
|
+
acceptable: state.attempt > 1,
|
|
254
|
+
answer: state.attempt > 1 ? `accepted:${request.label}` : `invalid:${request.label}`,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
"coverage-fan-out-synthesize": (request) => {
|
|
259
|
+
const identity = resolveExclusiveIdentity(request.label, request.prompt, ["climate", "transport"]);
|
|
260
|
+
if (identity === undefined) {
|
|
261
|
+
return {
|
|
262
|
+
status: "returned",
|
|
263
|
+
result: {
|
|
264
|
+
summary: "synthesis:climate-with-transport-missing",
|
|
265
|
+
coveredIds: ["climate"],
|
|
266
|
+
missingIds: ["transport"],
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (identity === "transport") {
|
|
271
|
+
return { status: "null", message: "deterministic transport failure" };
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
status: "returned",
|
|
275
|
+
result: request.schema
|
|
276
|
+
? sampleSchema(request.schema, request.label || "research", identity)
|
|
277
|
+
: { id: identity ?? "climate", finding: `finding:${request.label}` },
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
"coverage-generate-filter": (request, state) => {
|
|
281
|
+
if (isGenerateFilterGeneratorSchema(request.schema)) {
|
|
282
|
+
state.attempt++;
|
|
283
|
+
const candidates =
|
|
284
|
+
state.attempt === 1
|
|
285
|
+
? [
|
|
286
|
+
{ id: "signal-a", proposal: "proposal:signal-a" },
|
|
287
|
+
{ id: "shared", proposal: "proposal:shared:first" },
|
|
288
|
+
]
|
|
289
|
+
: [
|
|
290
|
+
{ id: "shared", proposal: "proposal:shared:duplicate" },
|
|
291
|
+
{ id: "signal-b", proposal: "proposal:signal-b" },
|
|
292
|
+
{ id: "signal-c", proposal: "proposal:signal-c" },
|
|
293
|
+
];
|
|
294
|
+
return {
|
|
295
|
+
status: "returned",
|
|
296
|
+
result: createGeneratorResult(request.schema, candidates, request.label),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const identity = resolveGenerateFilterIdentity(request.label, request.prompt, [
|
|
300
|
+
"signal-a",
|
|
301
|
+
"shared",
|
|
302
|
+
"signal-b",
|
|
303
|
+
"signal-c",
|
|
304
|
+
]);
|
|
305
|
+
if (identity === "signal-b") {
|
|
306
|
+
return { status: "null", message: "deterministic filter failure for signal-b" };
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
status: "returned",
|
|
310
|
+
result: createFilterDecisionResult(request.schema, identity ?? "unknown", identity === "signal-a"),
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
"coverage-judge-panel": (request, state) => {
|
|
314
|
+
const judgeMatch = /^judge (\d+)\.(\d+)$/.exec(request.label);
|
|
315
|
+
if (judgeMatch) {
|
|
316
|
+
const attempt = Number(judgeMatch[1]);
|
|
317
|
+
const judge = Number(judgeMatch[2]);
|
|
318
|
+
if (attempt === 2 && judge === 3) {
|
|
319
|
+
return { status: "null", message: "deterministic judge 2.3 failure" };
|
|
320
|
+
}
|
|
321
|
+
const scores = attempt === 1 ? [0.4, 0.5, 0.3] : [0.9, 0.8, 0.7];
|
|
322
|
+
return {
|
|
323
|
+
status: "returned",
|
|
324
|
+
result: { score: scores[judge - 1], reason: `judge:${attempt}.${judge}` },
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
state.attempt++;
|
|
328
|
+
const identity = state.attempt === 1 ? "draft-a" : "draft-b";
|
|
329
|
+
return {
|
|
330
|
+
status: "returned",
|
|
331
|
+
result: { id: identity, answer: `answer:${identity}` },
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** Exact requested and resolved model settings recorded for reproducible comparison. */
|
|
337
|
+
export interface ComprehensionModelSelection {
|
|
338
|
+
requested: string;
|
|
339
|
+
resolved: string;
|
|
340
|
+
thinkingLevel: ModelThinkingLevel | null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
type ComprehensionFailureStage = "generation" | "parse" | "runtime" | "assertion";
|
|
344
|
+
|
|
345
|
+
/** Versioned evidence from one generated workflow executed against its scenario contract. */
|
|
346
|
+
export interface ComprehensionEvidence {
|
|
347
|
+
formatVersion: 2;
|
|
348
|
+
provider: string;
|
|
349
|
+
modelSelection: ComprehensionModelSelection;
|
|
350
|
+
extensionVersion: string;
|
|
351
|
+
contractVersions: { format: string; content: string };
|
|
352
|
+
skillVersion: string;
|
|
353
|
+
task: { id: string; suite: ComprehensionSuite; kind: ComprehensionTaskKind; prompt: string };
|
|
354
|
+
generatedWorkflow: string | null;
|
|
355
|
+
skillLoadingEvidence: SkillLoadingEvidence;
|
|
356
|
+
tokenUsage: ComprehensionTokenUsage | null;
|
|
357
|
+
runtime: {
|
|
358
|
+
calls: RuntimeCall[];
|
|
359
|
+
events: RuntimeEvent[];
|
|
360
|
+
topology: { maxConcurrent: number; phases: string[] };
|
|
361
|
+
failures: Array<{
|
|
362
|
+
callIndex: number;
|
|
363
|
+
label: string;
|
|
364
|
+
message: string;
|
|
365
|
+
errorCode: string | null;
|
|
366
|
+
recoverable: boolean | null;
|
|
367
|
+
}>;
|
|
368
|
+
result: unknown;
|
|
369
|
+
assertions: Array<{ name: string; passed: boolean; details: string }>;
|
|
370
|
+
};
|
|
371
|
+
passed: boolean;
|
|
372
|
+
failure: { stage: ComprehensionFailureStage; message: string; stack?: string } | null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
interface RunComprehensionScenarioBaseOptions {
|
|
376
|
+
scenario: ComprehensionScenario;
|
|
377
|
+
provider: string;
|
|
378
|
+
extensionVersion: string;
|
|
379
|
+
contractVersions: { format: string; content: string };
|
|
380
|
+
skillVersion: string;
|
|
381
|
+
generate: (scenario: ComprehensionScenario) => Promise<ModelGeneration>;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Dependencies and version facts needed to generate and execute one comprehension scenario. */
|
|
385
|
+
export type RunComprehensionScenarioOptions = RunComprehensionScenarioBaseOptions &
|
|
386
|
+
(
|
|
387
|
+
| { modelSelection: ComprehensionModelSelection; model?: never }
|
|
388
|
+
| {
|
|
389
|
+
/** @deprecated Use modelSelection. Retained for provider-free callers created before evidence format 2. */
|
|
390
|
+
model: string;
|
|
391
|
+
modelSelection?: never;
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
/** Select scenarios in stable declaration order for one suite. */
|
|
396
|
+
export function selectComprehensionScenarios(
|
|
397
|
+
suite: ComprehensionSuite | `${ComprehensionSuite}`,
|
|
398
|
+
): readonly ComprehensionScenario[] {
|
|
399
|
+
return COMPREHENSION_SCENARIOS.filter((scenario) => scenario.suite === suite);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Generate, parse, execute, and behaviorally score one scenario without making scoring-stage provider calls. */
|
|
403
|
+
export async function runComprehensionScenario(
|
|
404
|
+
options: RunComprehensionScenarioOptions,
|
|
405
|
+
): Promise<ComprehensionEvidence> {
|
|
406
|
+
const evidence = emptyEvidence(options);
|
|
407
|
+
let generation: ModelGeneration;
|
|
408
|
+
try {
|
|
409
|
+
generation = await options.generate(options.scenario);
|
|
410
|
+
evidence.generatedWorkflow = generation.workflow;
|
|
411
|
+
evidence.skillLoadingEvidence = generation.skillLoadingEvidence;
|
|
412
|
+
evidence.tokenUsage = generation.tokenUsage;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error instanceof ModelGenerationError) {
|
|
415
|
+
evidence.skillLoadingEvidence = error.skillLoadingEvidence;
|
|
416
|
+
evidence.tokenUsage = error.tokenUsage;
|
|
417
|
+
}
|
|
418
|
+
evidence.failure = failure("generation", error);
|
|
419
|
+
return evidence;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
parseWorkflowScript(generation.workflow);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
evidence.failure = failure("parse", error);
|
|
426
|
+
return evidence;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const calls: RuntimeCall[] = [];
|
|
430
|
+
const events: RuntimeEvent[] = [];
|
|
431
|
+
const runtimeFailures: ComprehensionEvidence["runtime"]["failures"] = [];
|
|
432
|
+
let timelineIndex = 0;
|
|
433
|
+
let active = 0;
|
|
434
|
+
let maxConcurrent = 0;
|
|
435
|
+
const fixtureState: ScenarioFixtureState = { attempt: 0 };
|
|
436
|
+
try {
|
|
437
|
+
const result = await runWorkflow(generation.workflow, {
|
|
438
|
+
persistLogs: false,
|
|
439
|
+
args: { work: [{ id: "alpha" }, { id: "beta" }], phaseBudget: 100 },
|
|
440
|
+
loadSavedWorkflow: () =>
|
|
441
|
+
`export const meta = { name: "comprehension_child", description: "deterministic child" }\nconst value = await agent("child:" + args.id, { label: "child:" + args.id })\nreturn { id: args.id, value }`,
|
|
442
|
+
onRuntimeEvent: (event) => events.push({ ...event, index: timelineIndex++ }),
|
|
443
|
+
onAgentStart: ({ label, phase, prompt }) => {
|
|
444
|
+
calls.push({
|
|
445
|
+
index: timelineIndex++,
|
|
446
|
+
completedIndex: null,
|
|
447
|
+
label,
|
|
448
|
+
prompt,
|
|
449
|
+
phase: phase ?? null,
|
|
450
|
+
structured: false,
|
|
451
|
+
scenarioRole: null,
|
|
452
|
+
status: "returned",
|
|
453
|
+
result: null,
|
|
454
|
+
});
|
|
455
|
+
},
|
|
456
|
+
onAgentEnd: ({ label, error, errorCode, recoverable }) => {
|
|
457
|
+
const call = [...calls].reverse().find((candidate) => candidate.label === label);
|
|
458
|
+
const completedIndex = timelineIndex++;
|
|
459
|
+
if (call) {
|
|
460
|
+
call.completedIndex = completedIndex;
|
|
461
|
+
}
|
|
462
|
+
if (!error) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
runtimeFailures.push({
|
|
466
|
+
callIndex: call?.index ?? -1,
|
|
467
|
+
label,
|
|
468
|
+
message: error,
|
|
469
|
+
errorCode: errorCode ?? null,
|
|
470
|
+
recoverable: recoverable ?? null,
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
agent: {
|
|
474
|
+
async run(prompt: string, agentOptions: AgentRunOptions<TSchema> = {}): Promise<unknown> {
|
|
475
|
+
const call = [...calls].reverse().find(({ label }) => label === agentOptions.label && label !== "");
|
|
476
|
+
if (call) {
|
|
477
|
+
call.structured = agentOptions.schema !== undefined;
|
|
478
|
+
if (options.scenario.id === "coverage-generate-filter") {
|
|
479
|
+
call.scenarioRole = isGenerateFilterGeneratorSchema(agentOptions.schema) ? "generator" : "filter";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
active++;
|
|
483
|
+
maxConcurrent = Math.max(maxConcurrent, active);
|
|
484
|
+
await pause();
|
|
485
|
+
active--;
|
|
486
|
+
const fixture = SCENARIO_AGENT_FIXTURES[options.scenario.id];
|
|
487
|
+
if (!fixture) {
|
|
488
|
+
throw new Error(`No deterministic fixture for scenario ${options.scenario.id}`);
|
|
489
|
+
}
|
|
490
|
+
const response = fixture(
|
|
491
|
+
{
|
|
492
|
+
prompt,
|
|
493
|
+
label: agentOptions.label ?? "",
|
|
494
|
+
schema: agentOptions.schema,
|
|
495
|
+
},
|
|
496
|
+
fixtureState,
|
|
497
|
+
);
|
|
498
|
+
if (response.status === "null") {
|
|
499
|
+
if (call) {
|
|
500
|
+
call.status = "null";
|
|
501
|
+
}
|
|
502
|
+
throw new WorkflowError(response.message, WorkflowErrorCode.AGENT_EXECUTION_ERROR, {
|
|
503
|
+
recoverable: true,
|
|
504
|
+
agentLabel: agentOptions.label,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (call) {
|
|
508
|
+
call.result = serializable(response.result);
|
|
509
|
+
}
|
|
510
|
+
return response.result;
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
evidence.runtime.calls = calls;
|
|
515
|
+
evidence.runtime.events = events;
|
|
516
|
+
evidence.runtime.topology = { maxConcurrent, phases: [...result.phases] };
|
|
517
|
+
evidence.runtime.failures = runtimeFailures;
|
|
518
|
+
evidence.runtime.result = serializable(result.result);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
evidence.runtime.calls = calls;
|
|
521
|
+
evidence.runtime.events = events;
|
|
522
|
+
evidence.runtime.topology.maxConcurrent = maxConcurrent;
|
|
523
|
+
evidence.failure = failure("runtime", error);
|
|
524
|
+
return evidence;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const assertions = assertScenario(options.scenario, generation.skillLoadingEvidence, evidence.runtime);
|
|
528
|
+
evidence.runtime.assertions = assertions;
|
|
529
|
+
evidence.passed = assertions.every(({ passed }) => passed);
|
|
530
|
+
if (!evidence.passed) {
|
|
531
|
+
evidence.failure = {
|
|
532
|
+
stage: "assertion",
|
|
533
|
+
message: assertions
|
|
534
|
+
.filter(({ passed }) => !passed)
|
|
535
|
+
.map(({ details }) => details)
|
|
536
|
+
.join("; "),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return evidence;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function emptyEvidence(options: RunComprehensionScenarioOptions): ComprehensionEvidence {
|
|
543
|
+
return {
|
|
544
|
+
formatVersion: 2,
|
|
545
|
+
provider: options.provider,
|
|
546
|
+
modelSelection: normalizeModelSelection(options),
|
|
547
|
+
extensionVersion: options.extensionVersion,
|
|
548
|
+
contractVersions: options.contractVersions,
|
|
549
|
+
skillVersion: options.skillVersion,
|
|
550
|
+
task: {
|
|
551
|
+
id: options.scenario.id,
|
|
552
|
+
suite: options.scenario.suite,
|
|
553
|
+
kind: options.scenario.kind,
|
|
554
|
+
prompt: options.scenario.prompt,
|
|
555
|
+
},
|
|
556
|
+
generatedWorkflow: null,
|
|
557
|
+
skillLoadingEvidence: { discovered: false, loaded: false, toolCalls: [] },
|
|
558
|
+
tokenUsage: null,
|
|
559
|
+
runtime: {
|
|
560
|
+
calls: [],
|
|
561
|
+
events: [],
|
|
562
|
+
topology: { maxConcurrent: 0, phases: [] },
|
|
563
|
+
failures: [],
|
|
564
|
+
result: null,
|
|
565
|
+
assertions: [],
|
|
566
|
+
},
|
|
567
|
+
passed: false,
|
|
568
|
+
failure: null,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function normalizeModelSelection(options: RunComprehensionScenarioOptions): ComprehensionModelSelection {
|
|
573
|
+
if (options.modelSelection) {
|
|
574
|
+
return options.modelSelection;
|
|
575
|
+
}
|
|
576
|
+
if (options.model) {
|
|
577
|
+
return { requested: options.model, resolved: options.model, thinkingLevel: null };
|
|
578
|
+
}
|
|
579
|
+
throw new Error("modelSelection is required");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function assertScenario(
|
|
583
|
+
scenario: ComprehensionScenario,
|
|
584
|
+
skillLoadingEvidence: SkillLoadingEvidence,
|
|
585
|
+
runtime: ComprehensionEvidence["runtime"],
|
|
586
|
+
): Array<{ name: string; passed: boolean; details: string }> {
|
|
587
|
+
const assertions: Array<{ name: string; passed: boolean; details: string }> = [];
|
|
588
|
+
assertions.push({
|
|
589
|
+
name: "skill:loaded",
|
|
590
|
+
passed: skillLoadingEvidence.discovered && skillLoadingEvidence.loaded,
|
|
591
|
+
details: "model must discover and load the applicable installed skill",
|
|
592
|
+
});
|
|
593
|
+
assertions.push({
|
|
594
|
+
name: "runtime:calls",
|
|
595
|
+
passed: runtime.calls.length > 0,
|
|
596
|
+
details: "real runtime must observe at least one deterministic agent call",
|
|
597
|
+
});
|
|
598
|
+
const labels = runtime.calls.map(({ label }) => label);
|
|
599
|
+
assertions.push({
|
|
600
|
+
name: "labels:unique",
|
|
601
|
+
passed: labels.every((label) => label.trim().length > 0) && new Set(labels).size === labels.length,
|
|
602
|
+
details: "every agent call must use a unique nonblank label",
|
|
603
|
+
});
|
|
604
|
+
if (scenario.id === "quick-write" || scenario.id === "full-write") {
|
|
605
|
+
assertions.push({
|
|
606
|
+
name: "topology:parallel",
|
|
607
|
+
passed: runtime.topology.maxConcurrent > 1,
|
|
608
|
+
details: "write scenario must execute independent calls concurrently",
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (scenario.id === "quick-write") {
|
|
612
|
+
const taskCalls = ["alpha", "beta"].map((workId) => findTaskCall(runtime.calls, workId));
|
|
613
|
+
assertions.push({
|
|
614
|
+
name: "labels:unique-task-calls",
|
|
615
|
+
passed:
|
|
616
|
+
taskCalls.every((call) => call !== undefined && call.label.trim() !== "") &&
|
|
617
|
+
new Set(taskCalls.map((call) => call?.label)).size === taskCalls.length,
|
|
618
|
+
details: "quick-write task calls must use distinct nonblank labels",
|
|
619
|
+
});
|
|
620
|
+
for (const [index, workId] of ["alpha", "beta"].entries()) {
|
|
621
|
+
const call = taskCalls[index];
|
|
622
|
+
assertions.push({
|
|
623
|
+
name: `result:${workId}-returned`,
|
|
624
|
+
passed: call !== undefined && containsTaskResult(runtime.result, workId, call.result),
|
|
625
|
+
details: `quick-write must return the ${workId} agent result associated with ${workId}`,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (scenario.id === "full-write") {
|
|
630
|
+
const alphaCall = findTaskCall(runtime.calls, "alpha");
|
|
631
|
+
const betaCall = findTaskCall(runtime.calls, "beta");
|
|
632
|
+
assertions.push({
|
|
633
|
+
name: "structured-output:task-calls",
|
|
634
|
+
passed: alphaCall?.structured === true && betaCall?.structured === true,
|
|
635
|
+
details: "full-write must execute both alpha and beta with schema-validated output",
|
|
636
|
+
});
|
|
637
|
+
assertions.push({
|
|
638
|
+
name: "coverage:completed-alpha",
|
|
639
|
+
passed:
|
|
640
|
+
alphaCall !== undefined &&
|
|
641
|
+
containsInNamedField(runtime.result, /complet|success|done/i, (value) =>
|
|
642
|
+
containsTaskResultEvidence(value, "alpha", alphaCall.result),
|
|
643
|
+
),
|
|
644
|
+
details: "full-write must preserve the successful alpha result in completed data",
|
|
645
|
+
});
|
|
646
|
+
assertions.push({
|
|
647
|
+
name: "coverage:missing-beta",
|
|
648
|
+
passed:
|
|
649
|
+
betaCall !== undefined &&
|
|
650
|
+
betaCall.status === "null" &&
|
|
651
|
+
runtime.failures.some(({ callIndex }) => callIndex === betaCall.index) &&
|
|
652
|
+
containsInNamedField(runtime.result, /miss|fail|error|unavailable/i, (value) =>
|
|
653
|
+
containsIdentity(value, "beta"),
|
|
654
|
+
),
|
|
655
|
+
details: "full-write must preserve failed beta by identity in a missing-work collection",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
if (scenario.id === "coverage-fan-out-synthesize") {
|
|
659
|
+
const contributorCalls = runtime.calls.filter(
|
|
660
|
+
({ label, prompt }) => resolveExclusiveIdentity(label, prompt, ["climate", "transport"]) !== undefined,
|
|
661
|
+
);
|
|
662
|
+
const synthesisCalls = runtime.calls.filter((call) => !contributorCalls.includes(call));
|
|
663
|
+
const synthesisCall = synthesisCalls[0];
|
|
664
|
+
const climateCall = contributorCalls.find(
|
|
665
|
+
({ label, prompt }) => resolveExclusiveIdentity(label, prompt, ["climate", "transport"]) === "climate",
|
|
666
|
+
);
|
|
667
|
+
const transportCall = contributorCalls.find(
|
|
668
|
+
({ label, prompt }) => resolveExclusiveIdentity(label, prompt, ["climate", "transport"]) === "transport",
|
|
669
|
+
);
|
|
670
|
+
assertions.push({
|
|
671
|
+
name: "synthesis:complete-fan-out",
|
|
672
|
+
passed:
|
|
673
|
+
contributorCalls.length === 2 &&
|
|
674
|
+
runtime.topology.maxConcurrent > 1 &&
|
|
675
|
+
climateCall?.structured === true &&
|
|
676
|
+
transportCall?.structured === true &&
|
|
677
|
+
transportCall.status === "null" &&
|
|
678
|
+
runtime.failures.some(({ callIndex }) => callIndex === transportCall.index),
|
|
679
|
+
details:
|
|
680
|
+
"fan-out-and-synthesize must run both structured contributors concurrently and preserve failure evidence",
|
|
681
|
+
});
|
|
682
|
+
assertions.push({
|
|
683
|
+
name: "synthesis:complete-input",
|
|
684
|
+
passed:
|
|
685
|
+
synthesisCalls.length === 1 &&
|
|
686
|
+
synthesisCall?.structured === true &&
|
|
687
|
+
climateCall !== undefined &&
|
|
688
|
+
promptContainsResultOrSingleFieldProjection(synthesisCall.prompt, climateCall.result) &&
|
|
689
|
+
/\btransport\b/i.test(synthesisCall.prompt) &&
|
|
690
|
+
/miss|null|fail|unavailable/i.test(synthesisCall.prompt),
|
|
691
|
+
details:
|
|
692
|
+
"synthesis must start from the complete intended coverage, including the climate result and missing transport identity",
|
|
693
|
+
});
|
|
694
|
+
assertions.push({
|
|
695
|
+
name: "synthesis:returned-provenance",
|
|
696
|
+
passed:
|
|
697
|
+
synthesisCall !== undefined &&
|
|
698
|
+
climateCall !== undefined &&
|
|
699
|
+
containsValue(runtime.result, synthesisCall.result) &&
|
|
700
|
+
containsInNamedField(
|
|
701
|
+
runtime.result,
|
|
702
|
+
/contribut|coverage|ledger/i,
|
|
703
|
+
(value) =>
|
|
704
|
+
containsIdentity(value, "climate") &&
|
|
705
|
+
containsIdentity(value, "transport") &&
|
|
706
|
+
containsMissingCoverage(value, "transport"),
|
|
707
|
+
),
|
|
708
|
+
details:
|
|
709
|
+
"fan-out-and-synthesize must return the actual synthesis and a contributor ledger with missing transport coverage",
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
if (scenario.id === "coverage-generate-filter") {
|
|
713
|
+
const generatorCalls = runtime.calls.filter(({ scenarioRole }) => scenarioRole === "generator");
|
|
714
|
+
const filterCalls = runtime.calls.filter(({ scenarioRole }) => scenarioRole === "filter");
|
|
715
|
+
const generatedCandidates = generatorCalls.flatMap(({ result }) => generatedCandidatesFromResult(result) ?? []);
|
|
716
|
+
const retainedCandidates: Array<Record<string, unknown>> = [];
|
|
717
|
+
const seenCandidateIds = new Set<string>();
|
|
718
|
+
for (const candidate of generatedCandidates) {
|
|
719
|
+
if (typeof candidate.id !== "string" || seenCandidateIds.has(candidate.id)) continue;
|
|
720
|
+
seenCandidateIds.add(candidate.id);
|
|
721
|
+
if (retainedCandidates.length < 3) retainedCandidates.push(candidate);
|
|
722
|
+
}
|
|
723
|
+
const filterCallsById = new Map(
|
|
724
|
+
retainedCandidates.map((candidate) => [
|
|
725
|
+
candidate.id,
|
|
726
|
+
filterCalls.filter(({ prompt }) => promptContainsCandidate(prompt, candidate)),
|
|
727
|
+
]),
|
|
728
|
+
);
|
|
729
|
+
assertions.push({
|
|
730
|
+
name: "filter:generation-barrier",
|
|
731
|
+
passed:
|
|
732
|
+
generatorCalls.length === 2 &&
|
|
733
|
+
generatorCalls.every(({ structured }) => structured) &&
|
|
734
|
+
runtime.topology.maxConcurrent > 1 &&
|
|
735
|
+
generatorCalls.every(({ completedIndex }) => completedIndex !== null) &&
|
|
736
|
+
filterCalls.every(
|
|
737
|
+
({ index }) =>
|
|
738
|
+
index > Math.max(...generatorCalls.map(({ completedIndex }) => completedIndex ?? Number.POSITIVE_INFINITY)),
|
|
739
|
+
),
|
|
740
|
+
details: "generate-and-filter must finish exactly two concurrent structured generators before filtering",
|
|
741
|
+
});
|
|
742
|
+
assertions.push({
|
|
743
|
+
name: "filter:deduplicated-bounded-input",
|
|
744
|
+
passed:
|
|
745
|
+
retainedCandidates.length > 0 &&
|
|
746
|
+
retainedCandidates.length <= 3 &&
|
|
747
|
+
generatedCandidates.length > seenCandidateIds.size &&
|
|
748
|
+
filterCalls.length === retainedCandidates.length &&
|
|
749
|
+
filterCalls.every(({ structured }) => structured) &&
|
|
750
|
+
retainedCandidates.every((candidate) => {
|
|
751
|
+
const calls = filterCallsById.get(candidate.id) ?? [];
|
|
752
|
+
return calls.length === 1 && promptContainsCandidate(calls[0]?.prompt ?? "", candidate);
|
|
753
|
+
}),
|
|
754
|
+
details:
|
|
755
|
+
"generate-and-filter must deduplicate by candidate id, cap at three, and filter each retained candidate once",
|
|
756
|
+
});
|
|
757
|
+
const acceptedCalls = filterCalls.filter(({ result }) => filterDecisionIsAccepted(result));
|
|
758
|
+
const failedFilters = filterCalls.filter(({ status }) => status === "null");
|
|
759
|
+
const generatorOutputsReturned = containsInNamedField(runtime.result, /generat|output|batch/i, (value) =>
|
|
760
|
+
generatedCandidates.every((candidate) => containsCandidateProjection(value, candidate)),
|
|
761
|
+
);
|
|
762
|
+
assertions.push({
|
|
763
|
+
name: "filter:returned-provenance",
|
|
764
|
+
passed:
|
|
765
|
+
generatorOutputsReturned &&
|
|
766
|
+
retainedCandidates.every(
|
|
767
|
+
({ id }) =>
|
|
768
|
+
typeof id === "string" &&
|
|
769
|
+
containsInNamedField(runtime.result, /retain|candidate.?id/i, (value) => containsIdentity(value, id)),
|
|
770
|
+
) &&
|
|
771
|
+
acceptedCalls.every((call) => {
|
|
772
|
+
const identity = resolveGenerateFilterIdentity(
|
|
773
|
+
call.label,
|
|
774
|
+
call.prompt,
|
|
775
|
+
retainedCandidates.flatMap(({ id }) => (typeof id === "string" ? [id] : [])),
|
|
776
|
+
);
|
|
777
|
+
return (
|
|
778
|
+
identity !== undefined &&
|
|
779
|
+
containsInNamedField(runtime.result, /accept|pass|selected/i, (value) =>
|
|
780
|
+
containsIdentity(value, identity),
|
|
781
|
+
) &&
|
|
782
|
+
containsFilterDecisionEvidence(runtime.result, call.result, identity)
|
|
783
|
+
);
|
|
784
|
+
}) &&
|
|
785
|
+
failedFilters.every((call) => {
|
|
786
|
+
const identity = resolveGenerateFilterIdentity(
|
|
787
|
+
call.label,
|
|
788
|
+
call.prompt,
|
|
789
|
+
retainedCandidates.flatMap(({ id }) => (typeof id === "string" ? [id] : [])),
|
|
790
|
+
);
|
|
791
|
+
return (
|
|
792
|
+
identity !== undefined &&
|
|
793
|
+
runtime.failures.some(({ callIndex }) => callIndex === call.index) &&
|
|
794
|
+
containsInNamedField(runtime.result, /fail|miss|error/i, (value) => containsIdentity(value, identity))
|
|
795
|
+
);
|
|
796
|
+
}) &&
|
|
797
|
+
(failedFilters.length > 0 ||
|
|
798
|
+
containsInNamedField(runtime.result, /fail|miss|error/i, (value) => Array.isArray(value) || isRecord(value))),
|
|
799
|
+
details:
|
|
800
|
+
"generate-and-filter must return actual generation, retained identities, accepted decisions, and failed filter identity",
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (scenario.id === "coverage-judge-panel") {
|
|
804
|
+
const qualityStart = runtime.events.find(
|
|
805
|
+
(event) => event.type === "quality" && event.stage === "start" && event.helper === "judgePanel",
|
|
806
|
+
);
|
|
807
|
+
const qualityEnd = runtime.events.find(
|
|
808
|
+
(event) => event.type === "quality" && event.stage === "end" && event.helper === "judgePanel",
|
|
809
|
+
);
|
|
810
|
+
const judgmentCalls =
|
|
811
|
+
qualityStart && qualityEnd
|
|
812
|
+
? runtime.calls.filter(({ index }) => qualityStart.index < index && index < qualityEnd.index)
|
|
813
|
+
: [];
|
|
814
|
+
const candidateCalls = runtime.calls.filter((call) => !judgmentCalls.includes(call));
|
|
815
|
+
const judgedAttempts = candidateCalls.map((candidate, index) =>
|
|
816
|
+
judgedAttemptFromPrompt(
|
|
817
|
+
judgmentCalls.find(({ label }) => label.startsWith(`judge ${index + 1}.`))?.prompt ?? "",
|
|
818
|
+
candidate.result,
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
const judgmentsByCandidate = candidateCalls.map((_candidate, index) =>
|
|
822
|
+
judgmentCalls.filter(({ label }) => label.startsWith(`judge ${index + 1}.`)),
|
|
823
|
+
);
|
|
824
|
+
assertions.push({
|
|
825
|
+
name: "judge-panel:executed",
|
|
826
|
+
passed:
|
|
827
|
+
qualityStart !== undefined &&
|
|
828
|
+
qualityEnd !== undefined &&
|
|
829
|
+
candidateCalls.length === 2 &&
|
|
830
|
+
candidateCalls.every(({ structured }) => structured) &&
|
|
831
|
+
runtime.topology.maxConcurrent > 1,
|
|
832
|
+
details:
|
|
833
|
+
"judge-panel scenario must produce exactly two structured candidates concurrently and execute judgePanel",
|
|
834
|
+
});
|
|
835
|
+
assertions.push({
|
|
836
|
+
name: "judge-panel:numeric-judges",
|
|
837
|
+
passed:
|
|
838
|
+
judgmentCalls.length === 6 &&
|
|
839
|
+
judgmentCalls.every(({ structured, prompt }) => structured && /correct|valid|accur/i.test(prompt)) &&
|
|
840
|
+
judgmentsByCandidate.every(
|
|
841
|
+
(calls, index) =>
|
|
842
|
+
calls.length === 3 &&
|
|
843
|
+
judgedAttempts[index] !== undefined &&
|
|
844
|
+
containsResultProjection(judgedAttempts[index], candidateCalls[index]?.result),
|
|
845
|
+
) &&
|
|
846
|
+
judgmentCalls.filter(({ status }) => status === "null").length === 1,
|
|
847
|
+
details:
|
|
848
|
+
"judgePanel must run exactly three structured correctness judges per produced candidate and retain one recoverable failure",
|
|
849
|
+
});
|
|
850
|
+
const scoredCandidates = judgmentsByCandidate.map((calls, index) => {
|
|
851
|
+
const surviving = calls.filter(({ status }) => status === "returned");
|
|
852
|
+
const scores = surviving.map(({ result }) =>
|
|
853
|
+
isRecord(result) && typeof result.score === "number" ? result.score : 0,
|
|
854
|
+
);
|
|
855
|
+
return {
|
|
856
|
+
index,
|
|
857
|
+
attempt: judgedAttempts[index],
|
|
858
|
+
score: scores.length > 0 ? scores.reduce((sum, score) => sum + score, 0) / scores.length : 0,
|
|
859
|
+
judgments: surviving.map(({ result }) => result),
|
|
860
|
+
};
|
|
861
|
+
});
|
|
862
|
+
const expectedWinner = scoredCandidates.reduce<(typeof scoredCandidates)[number] | undefined>(
|
|
863
|
+
(best, candidate) =>
|
|
864
|
+
!best || candidate.score > best.score || (candidate.score === best.score && candidate.index < best.index)
|
|
865
|
+
? candidate
|
|
866
|
+
: best,
|
|
867
|
+
undefined,
|
|
868
|
+
);
|
|
869
|
+
assertions.push({
|
|
870
|
+
name: "judge-panel:returned-winner",
|
|
871
|
+
passed:
|
|
872
|
+
expectedWinner !== undefined &&
|
|
873
|
+
candidateCalls.every(({ result }) => containsResultProjection(runtime.result, result)) &&
|
|
874
|
+
containsValue(runtime.result, expectedWinner),
|
|
875
|
+
details: "judge-panel scenario must return both produced candidates and the helper's unchanged winning result",
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
if (scenario.id === "full-edit") {
|
|
879
|
+
const childStarts = runtime.events.filter(
|
|
880
|
+
(event): event is RuntimeEvent & { type: "workflow"; stage: "start" } =>
|
|
881
|
+
event.type === "workflow" && event.stage === "start",
|
|
882
|
+
);
|
|
883
|
+
const childEnds = runtime.events.filter(
|
|
884
|
+
(event): event is RuntimeEvent & { type: "workflow"; stage: "end" } =>
|
|
885
|
+
event.type === "workflow" && event.stage === "end",
|
|
886
|
+
);
|
|
887
|
+
const alphaStart = childStarts.find((event) => isRecord(event.args) && event.args.id === "alpha");
|
|
888
|
+
const alphaEnd = childEnds.find((event) => isRecord(event.args) && event.args.id === "alpha");
|
|
889
|
+
const betaStart = childStarts.find((event) => isRecord(event.args) && event.args.id === "beta");
|
|
890
|
+
const betaEnd = childEnds.find((event) => isRecord(event.args) && event.args.id === "beta");
|
|
891
|
+
const preparationCalls = alphaStart ? runtime.calls.filter(({ index }) => index < alphaStart.index) : [];
|
|
892
|
+
const preparation = preparationCalls[0];
|
|
893
|
+
const alphaCall = callWithin(runtime.calls, alphaStart, alphaEnd);
|
|
894
|
+
const betaCall = callWithin(runtime.calls, betaStart, betaEnd);
|
|
895
|
+
assertions.push({
|
|
896
|
+
name: "preparation:executed",
|
|
897
|
+
passed: preparationCalls.length === 1,
|
|
898
|
+
details: "edit scenario must execute exactly one preparation agent before saved children",
|
|
899
|
+
});
|
|
900
|
+
assertions.push({
|
|
901
|
+
name: "nesting:executed",
|
|
902
|
+
passed: alphaStart !== undefined && betaStart !== undefined,
|
|
903
|
+
details: "saved child workflow must execute for alpha and beta",
|
|
904
|
+
});
|
|
905
|
+
assertions.push({
|
|
906
|
+
name: "nesting:sequential",
|
|
907
|
+
passed: alphaEnd !== undefined && betaStart !== undefined && alphaEnd.index < betaStart.index,
|
|
908
|
+
details: "saved child workflows must execute sequentially in alpha then beta order",
|
|
909
|
+
});
|
|
910
|
+
assertions.push({
|
|
911
|
+
name: "nesting:returned-results",
|
|
912
|
+
passed:
|
|
913
|
+
alphaCall !== undefined &&
|
|
914
|
+
betaCall !== undefined &&
|
|
915
|
+
containsValue(runtime.result, alphaCall.result) &&
|
|
916
|
+
containsValue(runtime.result, betaCall.result),
|
|
917
|
+
details: "edit scenario must return data from both saved child workflows",
|
|
918
|
+
});
|
|
919
|
+
assertions.push({
|
|
920
|
+
name: "phase:entered",
|
|
921
|
+
passed:
|
|
922
|
+
preparation !== undefined &&
|
|
923
|
+
runtime.events.some(
|
|
924
|
+
(event) =>
|
|
925
|
+
event.type === "phase" && event.budget !== null && event.budget > 0 && event.index < preparation.index,
|
|
926
|
+
),
|
|
927
|
+
details: "edit scenario must enter a positive-budget phase before preparation",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
if (scenario.id === "full-review") {
|
|
931
|
+
const qualityStart = runtime.events.find((event) => event.type === "quality" && event.stage === "start");
|
|
932
|
+
const qualityEnd = runtime.events.find((event) => event.type === "quality" && event.stage === "end");
|
|
933
|
+
const claimCall = qualityStart ? runtime.calls.filter(({ index }) => index < qualityStart.index).at(-1) : undefined;
|
|
934
|
+
const qualityCalls =
|
|
935
|
+
qualityStart && qualityEnd
|
|
936
|
+
? runtime.calls.filter(({ index }) => qualityStart.index < index && index < qualityEnd.index)
|
|
937
|
+
: [];
|
|
938
|
+
const successfulQualityCalls = qualityCalls.filter(({ status }) => status === "returned");
|
|
939
|
+
const expectedVerdict = {
|
|
940
|
+
real: false,
|
|
941
|
+
realCount: 1,
|
|
942
|
+
total: 2,
|
|
943
|
+
votes: successfulQualityCalls.map(({ result }) => result),
|
|
944
|
+
};
|
|
945
|
+
assertions.push({
|
|
946
|
+
name: "quality:executed",
|
|
947
|
+
passed:
|
|
948
|
+
claimCall !== undefined &&
|
|
949
|
+
qualityCalls.length === 3 &&
|
|
950
|
+
qualityCalls.every(({ prompt }) => promptContainsResultOrSingleFieldProjection(prompt, claimCall.result)),
|
|
951
|
+
details: "quality helper must run exactly three adversarial reviewers over the produced claim",
|
|
952
|
+
});
|
|
953
|
+
assertions.push({
|
|
954
|
+
name: "quality:verify-contract",
|
|
955
|
+
passed:
|
|
956
|
+
qualityCalls.length === 3 &&
|
|
957
|
+
/\bFocus lens:[^\n]*\bsource\b/i.test(qualityCalls[0]?.prompt ?? "") &&
|
|
958
|
+
/\bFocus lens:[^\n]*\blogic(?:al)?\b/i.test(qualityCalls[1]?.prompt ?? "") &&
|
|
959
|
+
/\bFocus lens:[^\n]*\bsource\b/i.test(qualityCalls[2]?.prompt ?? "") &&
|
|
960
|
+
qualityCalls[2]?.status === "null" &&
|
|
961
|
+
containsValue(runtime.result, expectedVerdict),
|
|
962
|
+
details: "review scenario must use the documented verify options and return its survivor-based verdict unchanged",
|
|
963
|
+
});
|
|
964
|
+
assertions.push({
|
|
965
|
+
name: "review:returned-outcome",
|
|
966
|
+
passed:
|
|
967
|
+
claimCall !== undefined &&
|
|
968
|
+
containsResultOrSingleFieldProjection(runtime.result, claimCall.result) &&
|
|
969
|
+
successfulQualityCalls.length === 2 &&
|
|
970
|
+
successfulQualityCalls.every(({ result }) => containsValue(runtime.result, result)),
|
|
971
|
+
details: "review scenario must return the produced claim and successful quality-helper votes",
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
if (scenario.id === "full-debug") {
|
|
975
|
+
const attempts = runtime.events.filter((event) => event.type === "control-attempt");
|
|
976
|
+
const firstAttemptCall = attempts[0]
|
|
977
|
+
? runtime.calls.filter(({ index }) => index < attempts[0].index).at(-1)
|
|
978
|
+
: undefined;
|
|
979
|
+
const acceptedEvent = attempts.at(-1);
|
|
980
|
+
const acceptedCall = acceptedEvent
|
|
981
|
+
? runtime.calls.filter(({ index }) => index < acceptedEvent.index).at(-1)
|
|
982
|
+
: undefined;
|
|
983
|
+
assertions.push({
|
|
984
|
+
name: "control:retried",
|
|
985
|
+
passed:
|
|
986
|
+
attempts.length >= 2 &&
|
|
987
|
+
attempts.length <= 3 &&
|
|
988
|
+
attempts[0]?.accepted === false &&
|
|
989
|
+
acceptedEvent?.accepted === true,
|
|
990
|
+
details: "bounded control helper must reject the first result and accept within two or three attempts",
|
|
991
|
+
});
|
|
992
|
+
assertions.push({
|
|
993
|
+
name: "control:structured-validity",
|
|
994
|
+
passed:
|
|
995
|
+
firstAttemptCall?.structured === true &&
|
|
996
|
+
acceptedCall?.structured === true &&
|
|
997
|
+
isRecord(firstAttemptCall.result) &&
|
|
998
|
+
firstAttemptCall.result.acceptable === false &&
|
|
999
|
+
isRecord(acceptedCall.result) &&
|
|
1000
|
+
acceptedCall.result.acceptable === true,
|
|
1001
|
+
details: "debug scenario must gate the scenario-owned structured validity signal",
|
|
1002
|
+
});
|
|
1003
|
+
assertions.push({
|
|
1004
|
+
name: "debug:returned-outcome",
|
|
1005
|
+
passed:
|
|
1006
|
+
acceptedCall !== undefined &&
|
|
1007
|
+
containsNamedResultProjection(runtime.result, acceptedCall.result, ["acceptable", "answer"]),
|
|
1008
|
+
details: "debug scenario must return the accepted agent result rather than fabricate success",
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
if (scenario.id === "full-loop") {
|
|
1012
|
+
const firstCall = runtime.calls[0];
|
|
1013
|
+
const failedCall = runtime.calls.find(({ status }) => status === "null");
|
|
1014
|
+
const firstFinding =
|
|
1015
|
+
isRecord(firstCall?.result) && Array.isArray(firstCall.result.findings)
|
|
1016
|
+
? firstCall.result.findings[0]
|
|
1017
|
+
: undefined;
|
|
1018
|
+
assertions.push({
|
|
1019
|
+
name: "loop:successful-dry-stopping",
|
|
1020
|
+
passed:
|
|
1021
|
+
runtime.calls.length === 4 &&
|
|
1022
|
+
runtime.calls.every(({ structured }) => structured) &&
|
|
1023
|
+
failedCall === runtime.calls[1],
|
|
1024
|
+
details: "loop scenario must ignore the failed round for dryness and stop after two later successful dry rounds",
|
|
1025
|
+
});
|
|
1026
|
+
assertions.push({
|
|
1027
|
+
name: "loop:coverage-ledger",
|
|
1028
|
+
passed:
|
|
1029
|
+
firstFinding !== undefined &&
|
|
1030
|
+
containsValue(runtime.result, firstFinding) &&
|
|
1031
|
+
failedCall !== undefined &&
|
|
1032
|
+
containsInNamedField(runtime.result, /fail|miss|error/i, (value) =>
|
|
1033
|
+
containsRoundIdentity(value, failedCall.label, runtime.calls.indexOf(failedCall)),
|
|
1034
|
+
),
|
|
1035
|
+
details: "loop scenario must return the alpha finding and preserve the failed round by identity",
|
|
1036
|
+
});
|
|
1037
|
+
assertions.push({
|
|
1038
|
+
name: "loop:truthful-termination",
|
|
1039
|
+
passed:
|
|
1040
|
+
containsInNamedField(runtime.result, /complete/i, (value) => value === false) &&
|
|
1041
|
+
containsInNamedField(runtime.result, /termin|reason|status/i, (value) => typeof value === "string"),
|
|
1042
|
+
details: "loop scenario must return a termination reason and must not claim complete after a failed round",
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (scenario.id === "full-retry") {
|
|
1046
|
+
const controlAttempts = runtime.events.filter(
|
|
1047
|
+
(event): event is RuntimeEvent & { type: "control-attempt" } =>
|
|
1048
|
+
event.type === "control-attempt" && event.helper === "retry",
|
|
1049
|
+
);
|
|
1050
|
+
const firstCall = runtime.calls[0];
|
|
1051
|
+
const acceptedCall = runtime.calls[1];
|
|
1052
|
+
assertions.push({
|
|
1053
|
+
name: "retry:exact-control-contract",
|
|
1054
|
+
passed:
|
|
1055
|
+
runtime.calls.length === 2 &&
|
|
1056
|
+
controlAttempts.length === 2 &&
|
|
1057
|
+
controlAttempts[0]?.accepted === false &&
|
|
1058
|
+
controlAttempts[1]?.accepted === true &&
|
|
1059
|
+
firstCall?.structured === true &&
|
|
1060
|
+
acceptedCall?.structured === true,
|
|
1061
|
+
details: "retry scenario must use synchronous false-then-true acceptance over exactly two structured attempts",
|
|
1062
|
+
});
|
|
1063
|
+
assertions.push({
|
|
1064
|
+
name: "retry:returned-ledger",
|
|
1065
|
+
passed:
|
|
1066
|
+
firstCall !== undefined &&
|
|
1067
|
+
acceptedCall !== undefined &&
|
|
1068
|
+
containsInNamedField(
|
|
1069
|
+
runtime.result,
|
|
1070
|
+
/attempt|ledger|history/i,
|
|
1071
|
+
(value) =>
|
|
1072
|
+
containsResultProjection(value, firstCall.result) && containsResultProjection(value, acceptedCall.result),
|
|
1073
|
+
) &&
|
|
1074
|
+
containsValue(runtime.result, acceptedCall.result) &&
|
|
1075
|
+
containsInNamedField(runtime.result, /exhaust/i, (value) => value === false),
|
|
1076
|
+
details: "retry scenario must return every attempt, the accepted result, and explicit non-exhaustion",
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return assertions;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function sampleSchema(schema: unknown, label: string, taskIdentity?: string): unknown {
|
|
1083
|
+
if (isRecord(schema) && schema.type === "object") {
|
|
1084
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1085
|
+
return Object.fromEntries(
|
|
1086
|
+
Object.entries(properties).map(([key, value]) => [key, sampleProperty(key, value, label, taskIdentity)]),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
return sampleProperty("value", schema, label, taskIdentity);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function sampleProperty(key: string, schema: unknown, label: string, taskIdentity?: string): unknown {
|
|
1093
|
+
if (isRecord(schema) && Object.hasOwn(schema, "const")) {
|
|
1094
|
+
return schema.const;
|
|
1095
|
+
}
|
|
1096
|
+
if (isRecord(schema) && Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
1097
|
+
return schema.enum[0];
|
|
1098
|
+
}
|
|
1099
|
+
const type = isRecord(schema) ? schema.type : undefined;
|
|
1100
|
+
if (type === "string" && taskIdentity && /(?:^id$|id$|identity|work.?unit|^unit$)/i.test(key)) {
|
|
1101
|
+
return taskIdentity;
|
|
1102
|
+
}
|
|
1103
|
+
if (type === "boolean") {
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
if (type === "number" || type === "integer") {
|
|
1107
|
+
return 1;
|
|
1108
|
+
}
|
|
1109
|
+
if (type === "array") {
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
if (type === "object") {
|
|
1113
|
+
return sampleSchema(schema, label, taskIdentity);
|
|
1114
|
+
}
|
|
1115
|
+
return `${key}:${label}`;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function callWithin(calls: RuntimeCall[], start?: RuntimeEvent, end?: RuntimeEvent): RuntimeCall | undefined {
|
|
1119
|
+
if (!start || !end) {
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
return calls.find(({ index }) => start.index < index && index < end.index);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function findTaskCall(calls: RuntimeCall[], workId: string): RuntimeCall | undefined {
|
|
1126
|
+
return calls.find(({ label, prompt }) => resolveWriteTaskIdentity(label, prompt) === workId);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function containsInNamedField(
|
|
1130
|
+
container: unknown,
|
|
1131
|
+
fieldPattern: RegExp,
|
|
1132
|
+
predicate: (value: unknown) => boolean,
|
|
1133
|
+
): boolean {
|
|
1134
|
+
if (Array.isArray(container)) {
|
|
1135
|
+
return container.some((value) => containsInNamedField(value, fieldPattern, predicate));
|
|
1136
|
+
}
|
|
1137
|
+
if (!isRecord(container)) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
for (const [key, value] of Object.entries(container)) {
|
|
1141
|
+
if (fieldPattern.test(key) && predicate(value)) {
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
if (containsInNamedField(value, fieldPattern, predicate)) {
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function containsIdentity(container: unknown, identity: string): boolean {
|
|
1152
|
+
if (typeof container === "string") {
|
|
1153
|
+
return container.toLowerCase() === identity.toLowerCase();
|
|
1154
|
+
}
|
|
1155
|
+
if (Array.isArray(container)) {
|
|
1156
|
+
return container.some((value) => containsIdentity(value, identity));
|
|
1157
|
+
}
|
|
1158
|
+
if (!isRecord(container)) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
return Object.entries(container).some(
|
|
1162
|
+
([key, value]) => key.toLowerCase() === identity.toLowerCase() || containsIdentity(value, identity),
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function containsRoundIdentity(container: unknown, label: string, zeroBasedIndex: number): boolean {
|
|
1167
|
+
if (typeof container === "number") {
|
|
1168
|
+
return container === zeroBasedIndex || container === zeroBasedIndex + 1;
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof container === "string") {
|
|
1171
|
+
const normalized = container.toLowerCase();
|
|
1172
|
+
return (
|
|
1173
|
+
normalized === label.toLowerCase() ||
|
|
1174
|
+
normalized === String(zeroBasedIndex) ||
|
|
1175
|
+
normalized === String(zeroBasedIndex + 1) ||
|
|
1176
|
+
new RegExp(`\\bround[-_: ]?${zeroBasedIndex + 1}\\b`, "i").test(container)
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
if (Array.isArray(container)) {
|
|
1180
|
+
return container.some((value) => containsRoundIdentity(value, label, zeroBasedIndex));
|
|
1181
|
+
}
|
|
1182
|
+
return (
|
|
1183
|
+
isRecord(container) &&
|
|
1184
|
+
Object.entries(container).some(
|
|
1185
|
+
([key, value]) =>
|
|
1186
|
+
containsRoundIdentity(key, label, zeroBasedIndex) || containsRoundIdentity(value, label, zeroBasedIndex),
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function containsTaskResult(container: unknown, workId: string, expected: unknown): boolean {
|
|
1192
|
+
if (Array.isArray(container)) {
|
|
1193
|
+
return container.some((value) => containsTaskResult(value, workId, expected));
|
|
1194
|
+
}
|
|
1195
|
+
if (!isRecord(container)) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
for (const [key, value] of Object.entries(container)) {
|
|
1199
|
+
if (new RegExp(`\\b${workId}\\b`, "i").test(key) && containsValue(value, expected)) {
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const values = Object.values(container);
|
|
1204
|
+
if (
|
|
1205
|
+
values.some((value) => typeof value === "string" && value.toLowerCase() === workId.toLowerCase()) &&
|
|
1206
|
+
values.some((value) => containsValue(value, expected))
|
|
1207
|
+
) {
|
|
1208
|
+
return true;
|
|
1209
|
+
}
|
|
1210
|
+
return values.some((value) => containsTaskResult(value, workId, expected));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function containsTaskResultEvidence(container: unknown, identity: string, expected: unknown): boolean {
|
|
1214
|
+
if (isDeepStrictEqual(container, expected)) {
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1217
|
+
if (Array.isArray(container)) {
|
|
1218
|
+
return container.some((value) => containsTaskResultEvidence(value, identity, expected));
|
|
1219
|
+
}
|
|
1220
|
+
if (!isRecord(container)) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (isRecord(expected) && containsIdentity(container, identity)) {
|
|
1224
|
+
const producedPayload = Object.entries(expected).filter(
|
|
1225
|
+
([, value]) =>
|
|
1226
|
+
typeof value !== "boolean" &&
|
|
1227
|
+
!(typeof value === "string" && value.toLowerCase() === identity.toLowerCase()) &&
|
|
1228
|
+
!(Array.isArray(value) && value.length === 0),
|
|
1229
|
+
);
|
|
1230
|
+
if (producedPayload.some(([, value]) => containsValue(container, value))) {
|
|
1231
|
+
return true;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return Object.values(container).some((value) => containsTaskResultEvidence(value, identity, expected));
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function containsNamedResultProjection(container: unknown, expected: unknown, keys: string[]): boolean {
|
|
1238
|
+
if (Array.isArray(container)) {
|
|
1239
|
+
return container.some((value) => containsNamedResultProjection(value, expected, keys));
|
|
1240
|
+
}
|
|
1241
|
+
if (!isRecord(container) || !isRecord(expected)) return false;
|
|
1242
|
+
if (
|
|
1243
|
+
keys.every(
|
|
1244
|
+
(key) =>
|
|
1245
|
+
Object.hasOwn(expected, key) &&
|
|
1246
|
+
Object.hasOwn(container, key) &&
|
|
1247
|
+
isDeepStrictEqual(container[key], expected[key]),
|
|
1248
|
+
)
|
|
1249
|
+
) {
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
return Object.values(container).some((value) => containsNamedResultProjection(value, expected, keys));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function containsResultProjection(container: unknown, expected: unknown): boolean {
|
|
1256
|
+
if (isDeepStrictEqual(container, expected)) return true;
|
|
1257
|
+
if (Array.isArray(container)) {
|
|
1258
|
+
return container.some((value) => containsResultProjection(value, expected));
|
|
1259
|
+
}
|
|
1260
|
+
if (!isRecord(container)) return false;
|
|
1261
|
+
if (isRecord(expected)) {
|
|
1262
|
+
const expectedEntries = Object.entries(expected);
|
|
1263
|
+
if (
|
|
1264
|
+
expectedEntries.length > 0 &&
|
|
1265
|
+
expectedEntries.every(([key, value]) => Object.hasOwn(container, key) && isDeepStrictEqual(container[key], value))
|
|
1266
|
+
) {
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return Object.values(container).some((value) => containsResultProjection(value, expected));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function containsValue(container: unknown, expected: unknown): boolean {
|
|
1274
|
+
if (isDeepStrictEqual(container, expected)) {
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
if (Array.isArray(container)) {
|
|
1278
|
+
return container.some((value) => containsValue(value, expected));
|
|
1279
|
+
}
|
|
1280
|
+
return isRecord(container) && Object.values(container).some((value) => containsValue(value, expected));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function promptContainsValue(prompt: string, value: unknown): boolean {
|
|
1284
|
+
if (typeof value === "string") {
|
|
1285
|
+
return prompt.includes(value);
|
|
1286
|
+
}
|
|
1287
|
+
const serialized = JSON.stringify(value);
|
|
1288
|
+
return serialized !== undefined && prompt.includes(serialized);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function promptContainsCandidate(prompt: string, candidate: Record<string, unknown>): boolean {
|
|
1292
|
+
return (
|
|
1293
|
+
typeof candidate.id === "string" &&
|
|
1294
|
+
typeof candidate.proposal === "string" &&
|
|
1295
|
+
prompt.includes(candidate.id) &&
|
|
1296
|
+
prompt.includes(candidate.proposal)
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function isIdentityKey(key: string): boolean {
|
|
1301
|
+
return /^(?:id|candidate.?id)$/i.test(key);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function isDecisionBooleanKey(key: string): boolean {
|
|
1305
|
+
return /^(?:accept|accepted|pass|passed|selected)$/i.test(key);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function isDecisionExplanationKey(key: string): boolean {
|
|
1309
|
+
return /^(?:reason|rationale|explanation)$/i.test(key);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function hasDecisionProperties(properties: Record<string, unknown>): boolean {
|
|
1313
|
+
return Object.keys(properties).some(
|
|
1314
|
+
(key) => isDecisionBooleanKey(key) || isDecisionExplanationKey(key) || /^(?:decision|verdict|score)$/i.test(key),
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function schemaLiteralOr(schema: unknown, fallback: unknown): unknown {
|
|
1319
|
+
if (isRecord(schema) && Object.hasOwn(schema, "const")) {
|
|
1320
|
+
return schema.const;
|
|
1321
|
+
}
|
|
1322
|
+
if (isRecord(schema) && Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
1323
|
+
return schema.enum[0];
|
|
1324
|
+
}
|
|
1325
|
+
return fallback;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function isCandidateSchema(schema: unknown): boolean {
|
|
1329
|
+
if (!isRecord(schema) || schema.type !== "object") {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1333
|
+
return Object.hasOwn(properties, "id") && Object.hasOwn(properties, "proposal") && !hasDecisionProperties(properties);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function schemaContainsCandidate(schema: unknown): boolean {
|
|
1337
|
+
if (isCandidateSchema(schema)) {
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
if (!isRecord(schema)) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
if (
|
|
1344
|
+
(Object.hasOwn(schema, "const") && generatedCandidatesFromResult(schema.const) !== null) ||
|
|
1345
|
+
(Array.isArray(schema.enum) && generatedCandidatesFromResult(schema.enum) !== null)
|
|
1346
|
+
) {
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
if (schema.type === "array") {
|
|
1350
|
+
const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : [];
|
|
1351
|
+
return (
|
|
1352
|
+
prefixItems.some((itemSchema) => schemaContainsCandidate(itemSchema)) ||
|
|
1353
|
+
(schema.items !== false && (schema.items === undefined || schemaContainsCandidate(schema.items)))
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1357
|
+
return Object.values(properties).some((propertySchema) => schemaContainsCandidate(propertySchema));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function isGenerateFilterGeneratorSchema(schema: unknown): boolean {
|
|
1361
|
+
if (!isRecord(schema)) {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1365
|
+
if (hasDecisionProperties(properties)) {
|
|
1366
|
+
return false;
|
|
1367
|
+
}
|
|
1368
|
+
return schemaContainsCandidate(schema);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function candidateFromSchema(
|
|
1372
|
+
schema: unknown,
|
|
1373
|
+
candidate: Record<string, unknown>,
|
|
1374
|
+
label: string,
|
|
1375
|
+
): Record<string, unknown> {
|
|
1376
|
+
const sampled = sampleSchema(schema, label, typeof candidate.id === "string" ? candidate.id : undefined);
|
|
1377
|
+
if (!isRecord(sampled)) {
|
|
1378
|
+
return candidate;
|
|
1379
|
+
}
|
|
1380
|
+
const properties = isRecord(schema) && isRecord(schema.properties) ? schema.properties : {};
|
|
1381
|
+
return {
|
|
1382
|
+
...sampled,
|
|
1383
|
+
id: schemaLiteralOr(properties.id, candidate.id),
|
|
1384
|
+
proposal: schemaLiteralOr(properties.proposal, candidate.proposal),
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function fillGeneratorSchema(
|
|
1389
|
+
schema: unknown,
|
|
1390
|
+
candidates: Array<Record<string, unknown>>,
|
|
1391
|
+
label: string,
|
|
1392
|
+
): GeneratorFixtureValue {
|
|
1393
|
+
if (isRecord(schema) && Object.hasOwn(schema, "const")) {
|
|
1394
|
+
return {
|
|
1395
|
+
value: schema.const,
|
|
1396
|
+
consumed: generatedCandidatesFromResult(schema.const)?.length ?? 0,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (isCandidateSchema(schema)) {
|
|
1400
|
+
const candidate = candidates[0] ?? { id: "signal-a", proposal: "proposal:signal-a" };
|
|
1401
|
+
return { value: candidateFromSchema(schema, candidate, label), consumed: 1 };
|
|
1402
|
+
}
|
|
1403
|
+
if (!isRecord(schema)) {
|
|
1404
|
+
return { value: candidates, consumed: candidates.length };
|
|
1405
|
+
}
|
|
1406
|
+
if (schema.type === "array") {
|
|
1407
|
+
const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : [];
|
|
1408
|
+
const prefixed = prefixItems.map((itemSchema, index) => {
|
|
1409
|
+
const candidate = candidates[index] ?? { id: `signal-${index + 1}`, proposal: `proposal:signal-${index + 1}` };
|
|
1410
|
+
return candidateFromSchema(itemSchema, candidate, label);
|
|
1411
|
+
});
|
|
1412
|
+
if (schema.items === false) {
|
|
1413
|
+
return { value: prefixed, consumed: prefixed.length };
|
|
1414
|
+
}
|
|
1415
|
+
const enumeratedItems =
|
|
1416
|
+
isRecord(schema.items) && Array.isArray(schema.items.enum)
|
|
1417
|
+
? generatedCandidatesFromResult(schema.items.enum)
|
|
1418
|
+
: null;
|
|
1419
|
+
if (enumeratedItems) {
|
|
1420
|
+
const maxItems =
|
|
1421
|
+
typeof schema.maxItems === "number" && Number.isFinite(schema.maxItems)
|
|
1422
|
+
? Math.max(0, Math.floor(schema.maxItems))
|
|
1423
|
+
: enumeratedItems.length + prefixed.length;
|
|
1424
|
+
const selected = enumeratedItems.slice(0, Math.max(0, maxItems - prefixed.length));
|
|
1425
|
+
return { value: [...prefixed, ...selected], consumed: prefixed.length + selected.length };
|
|
1426
|
+
}
|
|
1427
|
+
const remaining = candidates.slice(prefixed.length);
|
|
1428
|
+
return {
|
|
1429
|
+
value: [...prefixed, ...remaining.map((candidate) => candidateFromSchema(schema.items, candidate, label))],
|
|
1430
|
+
consumed: prefixed.length + remaining.length,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
const sampled = sampleSchema(schema, label);
|
|
1434
|
+
if (!isRecord(sampled)) {
|
|
1435
|
+
return { value: sampled, consumed: 0 };
|
|
1436
|
+
}
|
|
1437
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1438
|
+
let consumed = 0;
|
|
1439
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
1440
|
+
if (!schemaContainsCandidate(propertySchema)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
const nested = fillGeneratorSchema(propertySchema, candidates.slice(consumed), label);
|
|
1444
|
+
sampled[key] = nested.value;
|
|
1445
|
+
consumed += nested.consumed;
|
|
1446
|
+
}
|
|
1447
|
+
return { value: sampled, consumed };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function createGeneratorResult(schema: unknown, candidates: Array<Record<string, unknown>>, label: string): unknown {
|
|
1451
|
+
if (isCandidateSchema(schema)) {
|
|
1452
|
+
const duplicate = candidates.find(({ id }) => id === "shared") ?? candidates[0];
|
|
1453
|
+
return duplicate ? candidateFromSchema(schema, duplicate, label) : sampleSchema(schema, label);
|
|
1454
|
+
}
|
|
1455
|
+
return fillGeneratorSchema(schema, candidates, label).value;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function generatedCandidatesFromResult(result: unknown): Array<Record<string, unknown>> | null {
|
|
1459
|
+
const candidates: Array<Record<string, unknown>> = [];
|
|
1460
|
+
const collect = (value: unknown): void => {
|
|
1461
|
+
if (Array.isArray(value)) {
|
|
1462
|
+
for (const item of value) {
|
|
1463
|
+
collect(item);
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (!isRecord(value)) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (typeof value.id === "string" && typeof value.proposal === "string") {
|
|
1471
|
+
candidates.push(value);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
for (const nested of Object.values(value)) {
|
|
1475
|
+
collect(nested);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
collect(result);
|
|
1479
|
+
return candidates.length > 0 ? candidates : null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function fillFilterDecisionSchema(schema: unknown, identity: string, accepted: boolean, label: string): unknown {
|
|
1483
|
+
if (!isRecord(schema) || schema.type !== "object") {
|
|
1484
|
+
return sampleSchema(schema, label, identity);
|
|
1485
|
+
}
|
|
1486
|
+
const sampled = sampleSchema(schema, label, identity);
|
|
1487
|
+
if (!isRecord(sampled)) {
|
|
1488
|
+
return sampled;
|
|
1489
|
+
}
|
|
1490
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1491
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
1492
|
+
if (isIdentityKey(key)) {
|
|
1493
|
+
sampled[key] = schemaLiteralOr(propertySchema, identity);
|
|
1494
|
+
} else if (key === "proposal") {
|
|
1495
|
+
sampled[key] = schemaLiteralOr(propertySchema, `proposal:${identity}`);
|
|
1496
|
+
} else if (isDecisionBooleanKey(key)) {
|
|
1497
|
+
sampled[key] = schemaLiteralOr(propertySchema, accepted);
|
|
1498
|
+
} else if (isDecisionExplanationKey(key)) {
|
|
1499
|
+
sampled[key] = schemaLiteralOr(propertySchema, `filter:${identity}`);
|
|
1500
|
+
} else if (isRecord(propertySchema) && propertySchema.type === "object") {
|
|
1501
|
+
sampled[key] = fillFilterDecisionSchema(propertySchema, identity, accepted, label);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return sampled;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function createFilterDecisionResult(schema: unknown, identity: string, accepted: boolean): unknown {
|
|
1508
|
+
const result = fillFilterDecisionSchema(schema, identity, accepted, `filter:${identity}`);
|
|
1509
|
+
if (isRecord(result) && Object.keys(result).length > 0) {
|
|
1510
|
+
return result;
|
|
1511
|
+
}
|
|
1512
|
+
return { id: identity, accepted, reason: `filter:${identity}` };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function filterDecisionIsAccepted(result: unknown): boolean {
|
|
1516
|
+
if (Array.isArray(result)) {
|
|
1517
|
+
return result.some((value) => filterDecisionIsAccepted(value));
|
|
1518
|
+
}
|
|
1519
|
+
if (!isRecord(result)) {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
return Object.entries(result).some(
|
|
1523
|
+
([key, value]) =>
|
|
1524
|
+
(isDecisionBooleanKey(key) && value === true) ||
|
|
1525
|
+
((!isDecisionBooleanKey(key) || value !== false) && filterDecisionIsAccepted(value)),
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function containsCandidateProjection(container: unknown, candidate: Record<string, unknown>): boolean {
|
|
1530
|
+
return containsNamedResultProjection(container, candidate, ["id", "proposal"]);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function decisionPayloadValues(result: unknown): unknown[] {
|
|
1534
|
+
if (Array.isArray(result)) {
|
|
1535
|
+
return result.flatMap((value) => decisionPayloadValues(value));
|
|
1536
|
+
}
|
|
1537
|
+
if (!isRecord(result)) {
|
|
1538
|
+
return [];
|
|
1539
|
+
}
|
|
1540
|
+
return Object.entries(result).flatMap(([key, value]) => {
|
|
1541
|
+
if (isDecisionExplanationKey(key)) {
|
|
1542
|
+
return [value];
|
|
1543
|
+
}
|
|
1544
|
+
return decisionPayloadValues(value);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function containsFilterDecisionEvidence(container: unknown, result: unknown, identity: string): boolean {
|
|
1549
|
+
if (containsResultProjection(container, result)) {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
if (!containsIdentity(container, identity)) {
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
return decisionPayloadValues(result).every((value) => containsValue(container, value));
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function singleFieldProjection(value: unknown): unknown[] {
|
|
1559
|
+
if (!isRecord(value)) return [];
|
|
1560
|
+
const values = Object.values(value);
|
|
1561
|
+
return values.length === 1 ? values : [];
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function jsonValuesInText(text: string): unknown[] {
|
|
1565
|
+
const values: unknown[] = [];
|
|
1566
|
+
for (let start = 0; start < text.length; start++) {
|
|
1567
|
+
if (text[start] !== "{" && text[start] !== "[") {
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
let depth = 0;
|
|
1571
|
+
let inString = false;
|
|
1572
|
+
let escaped = false;
|
|
1573
|
+
for (let end = start; end < text.length; end++) {
|
|
1574
|
+
const character = text[end];
|
|
1575
|
+
if (inString) {
|
|
1576
|
+
if (escaped) {
|
|
1577
|
+
escaped = false;
|
|
1578
|
+
} else if (character === "\\") {
|
|
1579
|
+
escaped = true;
|
|
1580
|
+
} else if (character === '"') {
|
|
1581
|
+
inString = false;
|
|
1582
|
+
}
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (character === '"') {
|
|
1586
|
+
inString = true;
|
|
1587
|
+
} else if (character === "{" || character === "[") {
|
|
1588
|
+
depth++;
|
|
1589
|
+
} else if (character === "}" || character === "]") {
|
|
1590
|
+
depth--;
|
|
1591
|
+
if (depth === 0) {
|
|
1592
|
+
try {
|
|
1593
|
+
const parsed: unknown = JSON.parse(text.slice(start, end + 1));
|
|
1594
|
+
values.push(parsed);
|
|
1595
|
+
} catch {
|
|
1596
|
+
// Continue scanning other balanced JSON candidates in the prompt.
|
|
1597
|
+
}
|
|
1598
|
+
start = end;
|
|
1599
|
+
break;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return values;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function promptContainsJsonProjection(prompt: string, result: unknown): boolean {
|
|
1608
|
+
return promptContainsValue(prompt, result) || jsonValuesInText(prompt).some((value) => containsValue(value, result));
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function promptContainsResultOrSingleFieldProjection(prompt: string, result: unknown): boolean {
|
|
1612
|
+
return (
|
|
1613
|
+
promptContainsJsonProjection(prompt, result) ||
|
|
1614
|
+
singleFieldProjection(result).some((value) => promptContainsJsonProjection(prompt, value))
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function containsMissingCoverage(container: unknown, identity: string): boolean {
|
|
1619
|
+
if (Array.isArray(container)) {
|
|
1620
|
+
return container.some((value) => containsMissingCoverage(value, identity));
|
|
1621
|
+
}
|
|
1622
|
+
if (!isRecord(container)) {
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
if (containsIdentity(container, identity)) {
|
|
1626
|
+
const missing = Object.entries(container).some(([key, value]) => {
|
|
1627
|
+
if (/^(?:missing|failed|unavailable)$/i.test(key)) {
|
|
1628
|
+
return value === true || containsIdentity(value, identity);
|
|
1629
|
+
}
|
|
1630
|
+
if (/^status$/i.test(key)) {
|
|
1631
|
+
return typeof value === "string" && /miss|fail|unavailable/i.test(value);
|
|
1632
|
+
}
|
|
1633
|
+
if (/^(?:result|value|data)$/i.test(key)) {
|
|
1634
|
+
return value === null;
|
|
1635
|
+
}
|
|
1636
|
+
if (/^missing.*ids?$/i.test(key)) {
|
|
1637
|
+
return containsIdentity(value, identity);
|
|
1638
|
+
}
|
|
1639
|
+
return false;
|
|
1640
|
+
});
|
|
1641
|
+
if (missing) {
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return Object.values(container).some((value) => containsMissingCoverage(value, identity));
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function judgedAttemptFromPrompt(prompt: string, candidateResult: unknown): unknown {
|
|
1649
|
+
return jsonValuesInText(prompt).find((value) => containsResultProjection(value, candidateResult));
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function containsResultOrSingleFieldProjection(container: unknown, result: unknown): boolean {
|
|
1653
|
+
return (
|
|
1654
|
+
containsValue(container, result) || singleFieldProjection(result).some((value) => containsValue(container, value))
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1659
|
+
return typeof value === "object" && value !== null;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function serializable(value: unknown): unknown {
|
|
1663
|
+
return JSON.parse(JSON.stringify(value));
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function failure(stage: Exclude<ComprehensionFailureStage, "assertion">, error: unknown) {
|
|
1667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1668
|
+
return {
|
|
1669
|
+
stage,
|
|
1670
|
+
message,
|
|
1671
|
+
...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
|
|
1672
|
+
};
|
|
1673
|
+
}
|