@atom-workflow-agent/workflow-planner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/README.zh-CN.md +39 -0
  4. package/dist/capabilities/capability-coverage-resolver.d.ts +5 -0
  5. package/dist/capabilities/capability-coverage-resolver.d.ts.map +1 -0
  6. package/dist/capabilities/capability-coverage-resolver.js +229 -0
  7. package/dist/capabilities/capability-coverage-resolver.js.map +1 -0
  8. package/dist/catalog/planner-catalog.d.ts +14 -0
  9. package/dist/catalog/planner-catalog.d.ts.map +1 -0
  10. package/dist/catalog/planner-catalog.js +47 -0
  11. package/dist/catalog/planner-catalog.js.map +1 -0
  12. package/dist/draft/planner-draft-normalizer.d.ts +20 -0
  13. package/dist/draft/planner-draft-normalizer.d.ts.map +1 -0
  14. package/dist/draft/planner-draft-normalizer.js +534 -0
  15. package/dist/draft/planner-draft-normalizer.js.map +1 -0
  16. package/dist/evaluation/planning-quality.d.ts +80 -0
  17. package/dist/evaluation/planning-quality.d.ts.map +1 -0
  18. package/dist/evaluation/planning-quality.js +120 -0
  19. package/dist/evaluation/planning-quality.js.map +1 -0
  20. package/dist/index.d.ts +13 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +13 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/model/planning-model-port.d.ts +41 -0
  25. package/dist/model/planning-model-port.d.ts.map +1 -0
  26. package/dist/model/planning-model-port.js +16 -0
  27. package/dist/model/planning-model-port.js.map +1 -0
  28. package/dist/planning/planning-controller.d.ts +77 -0
  29. package/dist/planning/planning-controller.d.ts.map +1 -0
  30. package/dist/planning/planning-controller.js +773 -0
  31. package/dist/planning/planning-controller.js.map +1 -0
  32. package/dist/store/local-plan-store.d.ts +57 -0
  33. package/dist/store/local-plan-store.d.ts.map +1 -0
  34. package/dist/store/local-plan-store.js +190 -0
  35. package/dist/store/local-plan-store.js.map +1 -0
  36. package/dist/store/plan-storage-layout.d.ts +14 -0
  37. package/dist/store/plan-storage-layout.d.ts.map +1 -0
  38. package/dist/store/plan-storage-layout.js +25 -0
  39. package/dist/store/plan-storage-layout.js.map +1 -0
  40. package/dist/validation/planning-draft-validator.d.ts +20 -0
  41. package/dist/validation/planning-draft-validator.d.ts.map +1 -0
  42. package/dist/validation/planning-draft-validator.js +76 -0
  43. package/dist/validation/planning-draft-validator.js.map +1 -0
  44. package/dist/workspace/map-aggregate-schema.d.ts +8 -0
  45. package/dist/workspace/map-aggregate-schema.d.ts.map +1 -0
  46. package/dist/workspace/map-aggregate-schema.js +20 -0
  47. package/dist/workspace/map-aggregate-schema.js.map +1 -0
  48. package/dist/workspace/planning-frontier.d.ts +15 -0
  49. package/dist/workspace/planning-frontier.d.ts.map +1 -0
  50. package/dist/workspace/planning-frontier.js +185 -0
  51. package/dist/workspace/planning-frontier.js.map +1 -0
  52. package/dist/workspace/planning-workspace-service.d.ts +72 -0
  53. package/dist/workspace/planning-workspace-service.d.ts.map +1 -0
  54. package/dist/workspace/planning-workspace-service.js +528 -0
  55. package/dist/workspace/planning-workspace-service.js.map +1 -0
  56. package/dist/workspace/semantic-plan-compiler.d.ts +19 -0
  57. package/dist/workspace/semantic-plan-compiler.d.ts.map +1 -0
  58. package/dist/workspace/semantic-plan-compiler.js +643 -0
  59. package/dist/workspace/semantic-plan-compiler.js.map +1 -0
  60. package/dist/workspace/strict-binding-validator.d.ts +9 -0
  61. package/dist/workspace/strict-binding-validator.d.ts.map +1 -0
  62. package/dist/workspace/strict-binding-validator.js +232 -0
  63. package/dist/workspace/strict-binding-validator.js.map +1 -0
  64. package/package.json +53 -0
@@ -0,0 +1,773 @@
1
+ import { join } from "node:path";
2
+ import { PlannerCapabilityPlanSchema, PlanningStepJsonSchema, PlanningCheckpointSchema, PlanningWorkspaceActionSchema, WorkflowValidationErrorSchema, } from "@atom-workflow-agent/contracts";
3
+ import { resolveCapabilityCoverage } from "../capabilities/capability-coverage-resolver.js";
4
+ import { buildPlannerCatalog, buildPlannerCatalogIndex, selectPlannerCatalog } from "../catalog/planner-catalog.js";
5
+ import { PlanningModelExecutionError, } from "../model/planning-model-port.js";
6
+ import { PlanningWorkspaceService } from "../workspace/planning-workspace-service.js";
7
+ const DEFAULT_MAX_ERRORS = 20;
8
+ const DEFAULT_MAX_CAPABILITY_REPAIRS = 2;
9
+ const DEFAULT_MAX_PLANNING_TURNS = 64;
10
+ export class WorkflowPlanningFailedError extends Error {
11
+ attempts;
12
+ validationErrors;
13
+ rawOutputs;
14
+ audit;
15
+ code;
16
+ retryable;
17
+ constructor(message, attempts, validationErrors, rawOutputs, audit, options = {}) {
18
+ super(message, options);
19
+ this.attempts = attempts;
20
+ this.validationErrors = validationErrors;
21
+ this.rawOutputs = rawOutputs;
22
+ this.audit = audit;
23
+ this.name = "WorkflowPlanningFailedError";
24
+ this.code = options.code ?? "WORKFLOW_PLANNING_FAILED";
25
+ this.retryable = options.retryable ?? false;
26
+ }
27
+ }
28
+ const asJson = (value) => JSON.parse(JSON.stringify(value));
29
+ const fullCatalog = (registry) => buildPlannerCatalog(registry.list({ includeDisabled: true }));
30
+ function pathOf(parts) {
31
+ return parts.reduce((path, part) => typeof part === "number" ? `${path}[${part}]` : `${path}.${String(part)}`, "$");
32
+ }
33
+ function schemaIssues(issues, stage) {
34
+ return issues.flatMap((raw) => {
35
+ const issue = raw;
36
+ const make = (path, message = issue.message) => ({
37
+ code: stage === "capability-schema" ? "PLANNER_CAPABILITY_SCHEMA_INVALID" : "PLANNING_WORKSPACE_ACTION_INVALID",
38
+ stage,
39
+ path: pathOf(path),
40
+ message: message ?? "Planner document does not match the phase contract",
41
+ hint: "Return exactly one complete JSON document matching the current phase contract",
42
+ });
43
+ return issue.code === "unrecognized_keys" && issue.keys?.length
44
+ ? issue.keys.map((key) => make([...(issue.path ?? []), key], `Unrecognized field: ${key}`))
45
+ : [make(issue.path ?? [])];
46
+ });
47
+ }
48
+ const feedback = (errors, limit) => [...errors]
49
+ .sort((left, right) => left.path.localeCompare(right.path) || left.code.localeCompare(right.code)).slice(0, limit);
50
+ function coverageErrors(coverage) {
51
+ return coverage.gaps.map((gap) => ({
52
+ code: gap.code,
53
+ stage: "capability-resolution",
54
+ path: `$.requirements.${gap.requirement.id}`,
55
+ message: gap.message,
56
+ received: gap.fieldIssues ?? gap.nearMatches,
57
+ hint: gap.code === "PLANNING_CAPABILITY_SCHEMA_MISMATCH"
58
+ ? "Reference exact top-level field names from atomCatalogIndex and declare every required input of each selected Atom; Runtime owns their full Schemas"
59
+ : gap.code === "PLANNING_CAPABILITY_ATOM_UNKNOWN"
60
+ ? "Use an exact registered Atom key from atomCatalogIndex"
61
+ : "If this is only a control-flow terminal, retry, loop, branch, literal binding, Signal, Host wait, or rollback path, remove the capability requirement; otherwise keep resolution.kind='missing' to report a genuine missing Atom",
62
+ }));
63
+ }
64
+ function outputFailure(error) {
65
+ return ["PLANNER_OUTPUT_INVALID_JSON", "PLANNER_OUTPUT_EMPTY"].includes(error.code) ? {
66
+ code: "WORKFLOW_JSON_INVALID", stage: "json-extract", path: "$", message: error.message,
67
+ hint: "Return exactly one JSON document without Markdown or explanation",
68
+ } : undefined;
69
+ }
70
+ function capabilityPayload(input, index, referenceDirectory) {
71
+ return asJson({
72
+ goal: input.goal,
73
+ workflowInputs: input.workflowInputs,
74
+ context: input.context,
75
+ atomCatalogIndex: index,
76
+ ...(referenceDirectory ? { references: {
77
+ plannerInput: join(referenceDirectory, "planner-input.json"),
78
+ usage: "Read this file with local file tools only if the inline goal or inputs are insufficient; never modify it.",
79
+ } } : {}),
80
+ contract: {
81
+ schemaVersion: "1.1", kind: "capability-plan",
82
+ requirements: [
83
+ {
84
+ id: "covered-capability", description: "required business capability supplied by registered Atoms", required: true,
85
+ resolution: {
86
+ kind: "covered",
87
+ uses: [{
88
+ atom: "exact registered Atom key",
89
+ inputs: [{ role: "business meaning", field: "exact required input field" }],
90
+ outputs: [{ role: "business meaning", field: "exact relevant output field" }],
91
+ }],
92
+ },
93
+ },
94
+ {
95
+ id: "missing-capability", description: "required business capability absent from the catalog", required: true,
96
+ resolution: {
97
+ kind: "missing",
98
+ desiredInputs: [{ name: "input", description: "business meaning", required: true, acceptedTypes: ["object"] }],
99
+ desiredOutputs: [{ name: "output", description: "business meaning", required: true, acceptedTypes: ["string"] }],
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ rules: [
105
+ "List executable Atom capabilities only. Sequence, parallel, Decision containers and branch routing, Map or Repeat scaffolding, retry exhaustion, terminal states, rollback paths, literal bindings, Signal waiting, and Host waiting are control concerns rather than separate capabilities",
106
+ "Every Atom that a control node will execute is still a capability and must be covered. In particular, when a branch depends on runtime data, select an exact decision-capable Atom from the Catalog and map the runtime facts input plus selected-branch output; never pre-evaluate or omit that selector",
107
+ "Split a composite behavior into the exact registered Atoms that implement it; never invent a handler Atom merely to terminate a Decision or bounded loop branch",
108
+ "Use only exact keys from atomCatalogIndex; never turn a natural-language capability into an Atom key",
109
+ "Give every requirement a unique, stable kebab-case id that describes its business capability; never reuse example ids such as covered-capability or missing-capability",
110
+ "Never output JSON Schema. For a covered capability, reference exact Atom keys and exact top-level field names; Runtime reads and validates the authoritative Schemas from the Catalog",
111
+ "For every selected Atom use, list every inputFields entry whose required flag is true. List only outputs that carry a business result needed by the goal or later planning",
112
+ "Use role to state why the exact Atom field participates in this capability. Do not rename the field or copy its description as a substitute for the field name",
113
+ "Use Workflow map only when each item needs independent branching, retry, pause, rollback, progress, or multiple Atom calls; otherwise call a registered batch Atom once",
114
+ "Use resolution.kind='missing' only when no registered Atom can provide a required executable business capability. acceptedTypes are coarse business shapes, not JSON Schema",
115
+ "Do not return nodes, bindings, control structures, or Workflow JSON in this preflight",
116
+ ],
117
+ });
118
+ }
119
+ function summarizeStep(step) {
120
+ switch (step.kind) {
121
+ case "atom": return { kind: step.kind, id: step.id, atom: step.atom };
122
+ case "sequence": return { kind: step.kind, steps: step.steps.map(summarizeStep) };
123
+ case "parallel": return { kind: step.kind, branches: step.branches.map(summarizeStep) };
124
+ case "decision": return asJson({
125
+ kind: step.kind,
126
+ id: step.id,
127
+ atom: step.atom,
128
+ select: step.select,
129
+ branches: Object.fromEntries(Object.entries(step.branches).map(([branchId, branch]) => [
130
+ branchId,
131
+ branch.mode === "revisit"
132
+ ? { mode: "revisit", maxIterations: branch.maxIterations, flow: summarizeStep(branch.flow) }
133
+ : branch.mode === "continue" && branch.flow
134
+ ? { mode: "continue", flow: summarizeStep(branch.flow) }
135
+ : branch.mode === "continue"
136
+ ? { mode: "continue" }
137
+ : branch,
138
+ ])),
139
+ defaultBranch: step.defaultBranch,
140
+ });
141
+ case "map": return { kind: step.kind, id: step.id, maxItems: step.maxItems, maxConcurrency: step.maxConcurrency ?? null, body: summarizeStep(step.body) };
142
+ case "repeat": return {
143
+ kind: step.kind, maxIterations: step.maxIterations, body: summarizeStep(step.body),
144
+ decision: { id: step.decision.id, atom: step.decision.atom, select: step.decision.select, continueBranch: step.decision.continueBranch, exitBranch: step.decision.exitBranch },
145
+ };
146
+ }
147
+ }
148
+ function schemaShape(schema) {
149
+ if (schema === null || Array.isArray(schema) || typeof schema !== "object")
150
+ return {};
151
+ const value = schema;
152
+ const items = value.items !== null && !Array.isArray(value.items) && typeof value.items === "object"
153
+ ? value.items
154
+ : undefined;
155
+ return asJson({
156
+ ...(typeof value.type === "string" ? { type: value.type } : {}),
157
+ ...(Array.isArray(value.enum) ? { enum: value.enum } : {}),
158
+ ...(typeof value.format === "string" ? { format: value.format } : {}),
159
+ ...(items && typeof items.type === "string" ? { itemType: items.type } : {}),
160
+ });
161
+ }
162
+ function compactFrontier(frontier) {
163
+ return asJson({
164
+ revision: frontier.revision,
165
+ draftHash: frontier.draftHash,
166
+ guaranteedAliases: frontier.guaranteedAliases,
167
+ availableValues: frontier.availableValues.map((value) => ({
168
+ ref: value.ref,
169
+ ...(value.producerAlias ? { producerAlias: value.producerAlias } : {}),
170
+ path: value.path,
171
+ shape: schemaShape(value.schema),
172
+ description: value.description,
173
+ })),
174
+ });
175
+ }
176
+ function regionDigest(step) {
177
+ const atomKeys = new Set();
178
+ const nodeIds = new Set();
179
+ const controls = new Set();
180
+ const branches = new Set();
181
+ const visit = (current) => {
182
+ switch (current.kind) {
183
+ case "atom":
184
+ atomKeys.add(current.atom);
185
+ nodeIds.add(current.id);
186
+ break;
187
+ case "sequence":
188
+ controls.add("sequence");
189
+ current.steps.forEach(visit);
190
+ break;
191
+ case "parallel":
192
+ controls.add("parallel");
193
+ current.branches.forEach(visit);
194
+ break;
195
+ case "decision":
196
+ controls.add("decision");
197
+ atomKeys.add(current.atom);
198
+ nodeIds.add(current.id);
199
+ Object.entries(current.branches).forEach(([branchId, branch]) => {
200
+ branches.add(`${current.id}.${branchId}:${branch.mode}`);
201
+ if (branch.mode !== "terminal" && branch.flow)
202
+ visit(branch.flow);
203
+ });
204
+ break;
205
+ case "map":
206
+ controls.add("map");
207
+ nodeIds.add(current.id);
208
+ visit(current.body);
209
+ break;
210
+ case "repeat":
211
+ controls.add("repeat");
212
+ visit(current.body);
213
+ atomKeys.add(current.decision.atom);
214
+ nodeIds.add(current.decision.id);
215
+ branches.add(`${current.decision.id}.${current.decision.continueBranch}:continue`);
216
+ branches.add(`${current.decision.id}.${current.decision.exitBranch}:exit`);
217
+ break;
218
+ }
219
+ };
220
+ visit(step);
221
+ return { atomKeys: [...atomKeys].sort(), nodeIds: [...nodeIds].sort(), controls: [...controls].sort(), branches: [...branches].sort() };
222
+ }
223
+ function promptReferences(directory, revision) {
224
+ if (!directory)
225
+ return undefined;
226
+ return asJson({
227
+ plannerInput: join(directory, "planner-input.json"),
228
+ catalogAndAudit: join(directory, "planning", "audit.json"),
229
+ workspaceState: join(directory, "planning", "workspace", "state.json"),
230
+ workspaceTransactions: join(directory, "planning", "workspace", "transactions.jsonl"),
231
+ ...(revision > 0 ? { currentRevision: join(directory, "planning", "workspace", "revisions", `${String(revision).padStart(4, "0")}.json`) } : {}),
232
+ usage: "These are read-only authoritative details. Inspect them with local file tools only when the compact payload is insufficient; never modify them.",
233
+ });
234
+ }
235
+ function focusedRules(intent, errors) {
236
+ const codes = new Set(errors.map(({ code }) => code));
237
+ if (codes.has("PLANNING_WORKFLOW_OUTPUT_REQUIRED"))
238
+ return [
239
+ "Only declare stable Workflow outputs from guaranteed final Atom fields; do not add or repeat business regions",
240
+ "If every continuing Decision branch already materializes the same terminal output contract, expose that typed merge as Workflow outputs; never call the same terminal Atom again merely to repackage its outputs",
241
+ "Use the exact current revision and draftHash, then set finalize=true in the same workspace-plan action",
242
+ ];
243
+ if (intent === "repair")
244
+ return [
245
+ "Correct only the rejected transaction described by previousValidationErrors against the exact current revision and draftHash",
246
+ "Reuse exact candidate field names and sources from the errors; do not guess or redesign already committed regions",
247
+ "Preserve branch-local terminal outputs through the typed merge; do not add a duplicate downstream call to the same terminal Atom merely to obtain Workflow outputs",
248
+ "Return one corrected workspace action and no explanation",
249
+ ];
250
+ if (intent === "finalize")
251
+ return [
252
+ "Audit remaining required Atom keys, outputs, and path obligations, then finalize the exact current revision",
253
+ "Use already materialized final branch results as Workflow outputs instead of re-executing their terminal Atom after the merge",
254
+ "Do not add business regions unless the compact progress proves a required capability is still unrealized",
255
+ ];
256
+ return [
257
+ "Add only the next small independently valid business region using exact selected Atom keys",
258
+ "Omit bindings unless Runtime previously reported an ambiguity",
259
+ "Do not repeat committed regions; Runtime owns graph edges, joins, internal references, and compilation",
260
+ ];
261
+ }
262
+ function collectDraftAtomKeys(step, target = new Set()) {
263
+ switch (step.kind) {
264
+ case "atom":
265
+ target.add(step.use);
266
+ break;
267
+ case "sequence":
268
+ step.steps.forEach((nested) => collectDraftAtomKeys(nested, target));
269
+ break;
270
+ case "parallel":
271
+ step.branches.forEach((nested) => collectDraftAtomKeys(nested, target));
272
+ break;
273
+ case "decision":
274
+ target.add(step.use);
275
+ Object.values(step.branches).forEach((branch) => {
276
+ if (branch.kind === "continue" || branch.kind === "terminal")
277
+ return;
278
+ collectDraftAtomKeys(branch.kind === "revisit" ? branch.body : branch, target);
279
+ });
280
+ break;
281
+ case "map":
282
+ collectDraftAtomKeys(step.body, target);
283
+ break;
284
+ case "repeat":
285
+ collectDraftAtomKeys(step.body, target);
286
+ target.add(step.decision.use);
287
+ break;
288
+ }
289
+ return target;
290
+ }
291
+ function compactTransactionResult(result) {
292
+ return asJson(result.status === "committed" ? {
293
+ status: "committed", operationId: result.operationId, revision: result.revision.revision,
294
+ draftHash: result.revision.draftHash, workflowNodeCount: result.revision.validation.workflowNodeCount,
295
+ } : {
296
+ status: "rejected", operationId: result.operationId, code: result.code, message: result.message,
297
+ errors: result.errors,
298
+ });
299
+ }
300
+ function workspacePayload(args) {
301
+ const snapshot = args.workspace.snapshot();
302
+ const latest = snapshot.revisions.at(-1);
303
+ const realizedAtomKeys = latest ? collectDraftAtomKeys(latest.plannerDraft.flow) : new Set();
304
+ const remainingRequiredAtomKeys = snapshot.workspace.requiredAtomKeys.filter((key) => !realizedAtomKeys.has(key));
305
+ const incremental = snapshot.workspace.requiredAtomKeys.length > 12;
306
+ const intent = args.includeFullCatalog
307
+ ? "bootstrap"
308
+ : args.validationErrors.length > 0
309
+ ? "repair"
310
+ : remainingRequiredAtomKeys.length === 0
311
+ ? "finalize"
312
+ : "extend";
313
+ const authority = {
314
+ workspaceId: snapshot.workspace.workspaceId,
315
+ requestId: snapshot.workspace.requestId,
316
+ workflowName: snapshot.workspace.workflowName,
317
+ revision: snapshot.workspace.currentRevision,
318
+ draftHash: snapshot.workspace.currentDraftHash,
319
+ catalogHash: snapshot.workspace.catalogHash,
320
+ };
321
+ const references = promptReferences(args.referenceDirectory, snapshot.workspace.currentRevision);
322
+ const outputRepair = args.validationErrors.some(({ code }) => code === "PLANNING_WORKFLOW_OUTPUT_REQUIRED");
323
+ const outputsExist = Object.keys(latest?.outputs ?? {}).length > 0;
324
+ const actionContract = {
325
+ recommendedAction: outputRepair
326
+ ? "workspace-plan:set-workflow-output"
327
+ : intent === "finalize" && outputsExist
328
+ ? "workspace-finalize"
329
+ : "workspace-plan:add-or-repair-region",
330
+ plan: {
331
+ schemaVersion: "1.0", kind: "workspace-plan", finalize: outputRepair || intent === "finalize",
332
+ transaction: {
333
+ schemaVersion: "1.0", operationId: "stable-unique-id",
334
+ baseRevision: snapshot.workspace.currentRevision, baseDraftHash: snapshot.workspace.currentDraftHash,
335
+ operations: outputRepair || (intent === "finalize" && !outputsExist)
336
+ ? [{ op: "set-workflow-output", name: "result-name", value: { source: "node", nodeId: "guaranteed-final-node", path: "output-field" } }]
337
+ : [{ op: "add-region", block: { blockId: "business-region", plan: "must match the planning step contract from the bootstrap turn" } }],
338
+ },
339
+ },
340
+ supportedPlanOperations: ["add-region", "replace-region", "remove-region", "set-workflow-output", "remove-workflow-output"],
341
+ finalize: { schemaVersion: "1.0", kind: "workspace-finalize", revision: snapshot.workspace.currentRevision, draftHash: snapshot.workspace.currentDraftHash },
342
+ reportGap: {
343
+ schemaVersion: "1.0", kind: "workspace-report-gap",
344
+ report: {
345
+ schemaVersion: "1.0", kind: "missing-input", code: "PLANNING_REQUIRED_INPUT_UNAVAILABLE",
346
+ message: "what is unavailable", paths: ["exact path"], resolution: "what the user or an Atom must provide",
347
+ },
348
+ },
349
+ };
350
+ const progress = {
351
+ planningTurn: args.turn,
352
+ maxPlanningTurns: args.maximum,
353
+ remainingTurnsIncludingCurrent: args.maximum - args.turn + 1,
354
+ realizedAtomKeys: [...realizedAtomKeys].sort(),
355
+ remainingRequiredAtomKeys,
356
+ currentOutputs: latest?.outputs ?? {},
357
+ regionIndex: latest?.blocks.map(({ blockId, plan }) => ({ blockId, ...regionDigest(plan) })) ?? [],
358
+ };
359
+ if (!args.includeFullCatalog)
360
+ return asJson({
361
+ turnIntent: intent,
362
+ goal: args.plannerInput.goal,
363
+ workspaceAuthority: authority,
364
+ progress,
365
+ frontierSummary: compactFrontier(args.workspace.frontier()),
366
+ ...(args.previousResult?.status === "committed" ? { lastCommit: compactTransactionResult(args.previousResult) } : {}),
367
+ contract: actionContract,
368
+ activeRules: focusedRules(intent, args.validationErrors),
369
+ ...(references ? { references } : {}),
370
+ });
371
+ return asJson({
372
+ turnIntent: intent,
373
+ goal: args.plannerInput.goal,
374
+ context: args.plannerInput.context,
375
+ workspaceAuthority: {
376
+ ...authority,
377
+ selectedAtomKeys: snapshot.workspace.selectedAtomKeys,
378
+ requiredAtomKeys: snapshot.workspace.requiredAtomKeys,
379
+ runtimePolicy: snapshot.workspace.runtimePolicy,
380
+ },
381
+ planningCatalog: args.includeFullCatalog
382
+ ? args.catalog
383
+ : { schemaVersion: args.catalog.schemaVersion, hash: args.catalog.hash, atomKeys: args.catalog.atoms.map(({ key }) => key) },
384
+ frontier: args.workspace.frontier(),
385
+ committedRegions: latest?.blocks.map(({ blockId, plan }) => ({ blockId, control: summarizeStep(plan) })) ?? [],
386
+ currentOutputs: latest?.outputs ?? {},
387
+ ...(args.previousResult?.status === "committed" ? { lastCommit: compactTransactionResult(args.previousResult) } : {}),
388
+ planningBudget: { planningTurn: args.turn, maxPlanningTurns: args.maximum, remainingTurnsIncludingCurrent: args.maximum - args.turn + 1 },
389
+ planningStrategy: incremental ? {
390
+ mode: "incremental-regions",
391
+ reason: `This long plan requires ${snapshot.workspace.requiredAtomKeys.length} exact Atoms; keep every JSON response small and independently valid.`,
392
+ recommendedNewAtomCallsThisTurn: { min: 4, max: 10 },
393
+ finalizeThisTurn: remainingRequiredAtomKeys.length === 0,
394
+ remainingRequiredAtomKeys,
395
+ } : {
396
+ mode: "single-turn-if-valid",
397
+ finalizeThisTurn: true,
398
+ remainingRequiredAtomKeys,
399
+ },
400
+ contract: {
401
+ ...actionContract,
402
+ ...(args.includeFullCatalog ? { planningStepSchema: PlanningStepJsonSchema } : {}),
403
+ },
404
+ rules: [
405
+ incremental
406
+ ? "This is a long plan: submit one small independently valid business region with roughly 4-10 new Atom calls and finalize=false. Never emit or repeat the entire Workflow in one response; later turns append later regions"
407
+ : "This is a small plan: prefer one workspace-plan containing all semantic regions with finalize=true",
408
+ "Plan business nodes and structured control only. Runtime owns dependsOn, joins, edges, internal references, traversal budgets, WorkflowSpec and WorkflowIR",
409
+ "Do not write $input/$output/$item references or BindingEvidence. Omit bindings and let Runtime auto-bind exact or uniquely compatible fields",
410
+ "Use bindings only after Runtime reports an ambiguity. A binding selects {source:'workflow',path}, {source:'node',nodeId,path}, {source:'item',path}, or a literal {source:'value',value}",
411
+ "Parallel includes all branches in one region. Runtime creates fan-in dependencies for the following shared node",
412
+ "Use Decision branches with mode='continue' for common downstream flow, mode='revisit' with a finite maxIterations for local adjustment and return to the same gate, and mode='terminal' only for an explicit success/failure path",
413
+ "When two or more Decision branches continue and at least one branch contains local flow, every continuing branch must declare a non-empty result object. Use the same logical result keys on every branch, but map each key to the value produced on that branch. Runtime creates one typed merge and downstream nodes consume only the selected branch result",
414
+ "A branch result uses semantic sources, for example result:{finalDecision:{source:'node',nodeId:'manual-review',path:'reviewDecision'},auditSummary:{source:'node',nodeId:'manual-review',path:'auditSummary'}}. Direct branches may source the Decision Atom itself; branch-local paths must source their fresh final node",
415
+ "If the goal explicitly says one Decision branch skips an expensive Atom, keep that branch flow empty and export schema-compatible literal or already-available values in its result. Never execute the skipped Atom merely to make branch shapes match; Runtime can merge literal and branch-local results",
416
+ "Before finalizing, recursively audit committedRegions. A collect/adjust/revise/user-change branch must revisit the relevant gate or confirmation. It must not terminate after merely invoking its mutation Atom",
417
+ "A revisit branch must contain one complete repair cycle before returning: mutate the current value, rerun every affected calculation/check required by the goal, aggregate those fresh results through the relevant gate, and only then revisit the confirmation or decision. Runtime carries unambiguous branch outputs into the next Decision activation; never rely on its original pre-branch value",
418
+ "Do not duplicate a long common downstream pipeline inside every branch. Keep only branch-specific work in branches, export typed branch results, and let Runtime merge them before the following shared block",
419
+ "Conversely, when each continuing branch already invokes the same terminal Atom and therefore materializes the final output contract, include those final fields in the typed branch results and expose the merge directly as Workflow outputs. Never invoke that terminal Atom again after the merge merely to repackage identical outputs",
420
+ "Do not turn every natural-language mention of iteration into Workflow control: use map/repeat only when iterations require external branching, retry, pause, rollback, progress, multiple Atom calls, or an external stop condition",
421
+ "When one selected Atom accepts and returns the complete collection and only the final result is observable, call it once as a batch Atom; when an Atom accepts one item from an upstream collection and per-item control is required, use map. maxItems is a safety bound and maxConcurrency only limits simultaneous items; neither truncates the collection",
422
+ "For Map, select items with a semantic source. map.id names the control group; body Atom outputs become ordered arrays outside the Map",
423
+ "Runtime auto-binding is strict: exact names win; different names require one uniquely schema-compatible description match. Ambiguity or absence is returned with candidates and must not be guessed",
424
+ "Every requiredAtomKey must appear in a reachable control region before finalize. Conditional capabilities remain frozen in Decision/repeat branches even when the current example input may bypass them",
425
+ "Before finalize, declare at least one stable Workflow output that represents the user-visible result. A dynamically planned Workflow without outputs is invalid; side-effect-only completion is reserved for explicitly imported mature Workflows",
426
+ "A Decision or repeat decision must set select to an exact relative string field path in that Atom's output, such as 'action' or 'result.status'. Atom fields have no implicit control meaning",
427
+ "When the selected output field declares a finite string enum, branch ids must cover it exactly. For an open string field, keep a safe explicit defaultBranch",
428
+ "Set finalize=true when the plan should be validated and frozen in this same turn. Use workspace-finalize only for an already committed Revision",
429
+ "In incremental-regions mode, set finalize=true only when planningStrategy.remainingRequiredAtomKeys is empty and all outputs plus path obligations are complete",
430
+ "Report a deterministic missing input/capability instead of retrying or inventing data",
431
+ ],
432
+ ...(references ? { references } : {}),
433
+ });
434
+ }
435
+ function resultErrors(result) {
436
+ return result.errors.length ? [...result.errors] : [{
437
+ code: result.code, stage: "draft-normalization", path: "$", message: result.message,
438
+ hint: "Inspect the current Revision/frontier and submit a corrected transaction using the exact base Revision and hash",
439
+ }];
440
+ }
441
+ function validationErrorsFromCause(error) {
442
+ const cause = error instanceof Error ? error.cause : undefined;
443
+ if (!Array.isArray(cause))
444
+ return [];
445
+ return cause.flatMap((item) => {
446
+ const parsed = WorkflowValidationErrorSchema.safeParse(item);
447
+ return parsed.success ? [parsed.data] : [];
448
+ });
449
+ }
450
+ function requiredAtomKeys(coverage) {
451
+ return [...new Set(coverage.plan.requirements
452
+ .filter(({ required }) => required)
453
+ .flatMap(({ resolution }) => resolution.kind === "covered" ? resolution.uses.map(({ atom }) => atom) : []))];
454
+ }
455
+ function tokenSum(attempts) {
456
+ const values = attempts.flatMap((attempt) => attempt.usage ? [attempt.usage] : []);
457
+ return values.length ? {
458
+ totalTokens: values.reduce((sum, value) => sum + value.totalTokens, 0),
459
+ inputTokens: values.reduce((sum, value) => sum + value.inputTokens, 0),
460
+ outputTokens: values.reduce((sum, value) => sum + value.outputTokens, 0),
461
+ } : undefined;
462
+ }
463
+ function workflowName(requestId) {
464
+ const suffix = requestId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(-64);
465
+ return `planned-${suffix || "workflow"}`;
466
+ }
467
+ export class PlanningController {
468
+ planner;
469
+ registry;
470
+ now;
471
+ constructor(planner, registry, now = () => new Date()) {
472
+ this.planner = planner;
473
+ this.registry = registry;
474
+ this.now = now;
475
+ }
476
+ async planAndCompile(input, target, options = {}) {
477
+ const resume = options.resumeCheckpoint ? PlanningCheckpointSchema.parse(options.resumeCheckpoint) : undefined;
478
+ if (resume && options.maxPlanningTurns !== undefined && options.maxPlanningTurns !== resume.maxPlanningTurns) {
479
+ throw new TypeError("maxPlanningTurns cannot change while resuming a persisted Planning Workspace");
480
+ }
481
+ const maxPlanningTurns = resume?.maxPlanningTurns ?? options.maxPlanningTurns ?? DEFAULT_MAX_PLANNING_TURNS;
482
+ const maxCapabilityRepairs = options.maxCapabilityRepairs ?? DEFAULT_MAX_CAPABILITY_REPAIRS;
483
+ const maxSelectedAtoms = options.maxSelectedAtoms ?? 128;
484
+ const maxErrors = options.maxErrorsPerRepair ?? DEFAULT_MAX_ERRORS;
485
+ for (const [name, value] of Object.entries({ maxPlanningTurns, maxCapabilityRepairs, maxSelectedAtoms, maxErrors })) {
486
+ if (!Number.isInteger(value) || value < 1)
487
+ throw new TypeError(`${name} must be a positive integer`);
488
+ }
489
+ if (maxPlanningTurns > 256)
490
+ throw new TypeError("maxPlanningTurns cannot exceed 256");
491
+ if (maxSelectedAtoms > 128)
492
+ throw new TypeError("maxSelectedAtoms cannot exceed 128");
493
+ const catalog = fullCatalog(this.registry);
494
+ const catalogIndex = buildPlannerCatalogIndex(catalog, this.registry.list({ includeDisabled: true }));
495
+ const catalogChanged = resume?.catalogIndexHash !== undefined && resume.catalogIndexHash !== catalogIndex.hash;
496
+ if (resume?.catalogIndexHash && resume.catalogIndexHash !== catalogIndex.hash && resume.phase !== "blocked_missing_atom") {
497
+ throw Object.assign(new Error("Cannot resume Planning Workspace because Atom Catalog changed"), { code: "PLANNING_CATALOG_CHANGED", retryable: false });
498
+ }
499
+ const attempts = [];
500
+ const rawOutputs = [];
501
+ const workspaceAttempts = [];
502
+ const validationFeedback = [];
503
+ let validationErrors = [...(resume?.lastValidationErrors ?? [])];
504
+ let planningTurns = resume?.planningTurnsConsumed ?? 0;
505
+ let promptPayloadBytes = 0;
506
+ let coverage = resume?.capabilityCoverage;
507
+ let selectedAtomKeys = [...(resume?.selectedAtomKeys ?? [])];
508
+ let planningCatalog = selectedAtomKeys.length ? selectPlannerCatalog(catalog, selectedAtomKeys) : undefined;
509
+ let workspace = resume?.workspaceSnapshot ? PlanningWorkspaceService.restore(this.registry, resume.workspaceSnapshot, { now: this.now }) : undefined;
510
+ let phase = resume?.phase ?? "capability-coverage";
511
+ let lastSubmittedRequestId = resume?.lastSubmittedRequestId;
512
+ let lastTransactionResult;
513
+ let plannerDraft = resume?.acceptedPlannerDraft ?? workspace?.snapshot().revisions.at(-1)?.plannerDraft;
514
+ if (phase === "blocked_missing_input" && workspace) {
515
+ workspace.resumeEditing(input.workflowInputs);
516
+ phase = "workspace";
517
+ }
518
+ if (phase === "blocked_missing_atom" && coverage) {
519
+ if (catalogChanged) {
520
+ coverage = undefined;
521
+ phase = "capability-coverage";
522
+ validationErrors = [];
523
+ }
524
+ }
525
+ const checkpoint = () => ({
526
+ schemaVersion: "1.0", phase, catalogIndexHash: catalogIndex.hash, selectedAtomKeys,
527
+ ...(planningCatalog ? { planningCatalogHash: planningCatalog.hash } : {}),
528
+ ...(coverage ? { capabilityCoverage: coverage } : {}),
529
+ ...(workspace ? { workspaceSnapshot: workspace.snapshot() } : {}),
530
+ planningTurnsConsumed: planningTurns, maxPlanningTurns, lastValidationErrors: validationErrors,
531
+ ...(lastSubmittedRequestId ? { lastSubmittedRequestId } : {}),
532
+ ...(plannerDraft ? { acceptedPlannerDraft: plannerDraft } : {}),
533
+ });
534
+ const audit = () => {
535
+ const usage = tokenSum(attempts);
536
+ const snapshot = workspace?.snapshot();
537
+ return {
538
+ catalogIndex,
539
+ ...(coverage ? { capabilityCoverage: coverage } : {}),
540
+ ...(planningCatalog ? { planningCatalog } : {}),
541
+ ...(snapshot ? { workspaceSnapshot: snapshot } : {}),
542
+ workspaceAttempts: [...workspaceAttempts],
543
+ ...(plannerDraft ? { plannerDraft } : {}),
544
+ validationFeedback: validationFeedback.map((errors) => [...errors]), checkpoint: checkpoint(),
545
+ metrics: {
546
+ catalogAtomCount: catalogIndex.atoms.length, selectedAtomCount: selectedAtomKeys.length,
547
+ catalogIndexBytes: Buffer.byteLength(JSON.stringify(catalogIndex)),
548
+ planningCatalogBytes: planningCatalog ? Buffer.byteLength(JSON.stringify(planningCatalog)) : 0,
549
+ promptPayloadBytes,
550
+ capabilityTurns: attempts.filter(({ requestId }) => requestId.includes(":capability:")).length,
551
+ workspaceTurns: workspaceAttempts.length,
552
+ committedTransactions: workspaceAttempts.filter(({ status }) => status === "committed").length,
553
+ rejectedTransactions: workspaceAttempts.filter(({ status }) => status === "rejected").length,
554
+ planningTurns, maxPlanningTurns,
555
+ totalDurationMs: attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0),
556
+ ...(usage ? { tokenUsage: usage } : {}),
557
+ },
558
+ };
559
+ };
560
+ const persist = async () => options.onCheckpoint?.(checkpoint(), audit(), { attempts: [...attempts], rawOutputs: [...rawOutputs] });
561
+ const fail = (message, code = "WORKFLOW_PLANNING_FAILED", retryable = false, cause) => new WorkflowPlanningFailedError(message, [...attempts], [...validationErrors], [...rawOutputs], audit(), { code, retryable, ...(cause !== undefined ? { cause } : {}) });
562
+ if (phase === "completed" && workspace) {
563
+ const current = workspace.snapshot().workspace;
564
+ const finalized = workspace.finalize({ revision: current.currentRevision, draftHash: current.currentDraftHash });
565
+ return { workflow: finalized.workflow, ir: finalized.ir, attempts, rawOutputs, audit: audit() };
566
+ }
567
+ if (coverage?.status === "blocked_missing_atom") {
568
+ phase = "blocked_missing_atom";
569
+ validationErrors = coverageErrors(coverage);
570
+ await persist();
571
+ throw fail(coverage.gaps[0].message, coverage.gaps[0].code);
572
+ }
573
+ try {
574
+ return await this.planner.withSession(target, { ...(options.model ?? {}), totalTimeoutMs: null, idleTimeoutMs: null }, async (session) => {
575
+ let workspaceTurnsInSession = 0;
576
+ if (!coverage) {
577
+ for (let round = 0; round < maxCapabilityRepairs; round += 1) {
578
+ const turn = {
579
+ schemaVersion: "1.0", requestId: `${input.requestId}:capability:${round + 1}`, phase: "capability-coverage",
580
+ payload: capabilityPayload(input, catalogIndex, options.promptReferenceDirectory),
581
+ ...(validationErrors.length ? { previousValidationErrors: feedback(validationErrors, maxErrors) } : {}),
582
+ };
583
+ promptPayloadBytes += Buffer.byteLength(JSON.stringify(turn));
584
+ try {
585
+ const result = await session.plan(turn);
586
+ attempts.push(result.attempt);
587
+ rawOutputs.push(result.rawOutput);
588
+ const parsed = PlannerCapabilityPlanSchema.safeParse(result.document);
589
+ if (!parsed.success)
590
+ validationErrors = schemaIssues(parsed.error.issues, "capability-schema");
591
+ else {
592
+ coverage = resolveCapabilityCoverage(parsed.data, this.registry);
593
+ if (coverage.status === "blocked_missing_atom") {
594
+ validationErrors = coverageErrors(coverage);
595
+ validationFeedback.push(feedback(validationErrors, maxErrors));
596
+ if (round + 1 < maxCapabilityRepairs) {
597
+ coverage = undefined;
598
+ phase = "capability-coverage";
599
+ await persist();
600
+ continue;
601
+ }
602
+ phase = "blocked_missing_atom";
603
+ await persist();
604
+ throw fail(coverage.gaps[0].message, coverage.gaps[0].code);
605
+ }
606
+ if (coverage.selectedAtomKeys.length > maxSelectedAtoms)
607
+ validationErrors = [{
608
+ code: "PLANNING_SELECTED_ATOM_LIMIT_EXCEEDED", stage: "capability-resolution", path: "$.requirements",
609
+ message: `Capability plan selected ${coverage.selectedAtomKeys.length} Atoms; maximum is ${maxSelectedAtoms}`,
610
+ expected: maxSelectedAtoms, received: coverage.selectedAtomKeys.length, hint: "Remove unrelated optional capabilities",
611
+ }];
612
+ else {
613
+ selectedAtomKeys = [...coverage.selectedAtomKeys];
614
+ planningCatalog = selectPlannerCatalog(catalog, selectedAtomKeys);
615
+ validationErrors = [];
616
+ phase = "workspace";
617
+ await persist();
618
+ break;
619
+ }
620
+ }
621
+ }
622
+ catch (error) {
623
+ if (error instanceof WorkflowPlanningFailedError)
624
+ throw error;
625
+ if (!(error instanceof PlanningModelExecutionError))
626
+ throw error;
627
+ attempts.push(error.attempt);
628
+ if (error.rawOutput !== undefined)
629
+ rawOutputs.push(error.rawOutput);
630
+ const repairable = outputFailure(error);
631
+ if (!repairable)
632
+ throw fail(error.message, error.code, error.retryable, error);
633
+ validationErrors = [repairable];
634
+ }
635
+ validationFeedback.push(feedback(validationErrors, maxErrors));
636
+ await persist();
637
+ }
638
+ if (!coverage || coverage.status !== "covered" || !planningCatalog) {
639
+ throw fail(`Planner did not produce valid capability coverage after ${maxCapabilityRepairs} attempts`, "PLANNING_CAPABILITY_INVALID");
640
+ }
641
+ }
642
+ if (!coverage || coverage.status !== "covered") {
643
+ throw fail("Planning Workspace has no covered Capability Authority", "PLANNING_CAPABILITY_INVALID");
644
+ }
645
+ if (!planningCatalog)
646
+ planningCatalog = selectPlannerCatalog(catalog, selectedAtomKeys);
647
+ if (workspace?.snapshot().workspace.status === "blocked_missing_atom") {
648
+ workspace.updateCatalogAuthority(selectedAtomKeys, requiredAtomKeys(coverage), coverage.catalogHash);
649
+ }
650
+ if (!workspace) {
651
+ workspace = PlanningWorkspaceService.create(this.registry, {
652
+ workspaceId: `${input.requestId}:workspace`, requestId: input.requestId, goal: input.goal,
653
+ workflowName: workflowName(input.requestId), workflowInputs: input.workflowInputs, context: input.context,
654
+ runtimePolicy: input.runtimePolicy, selectedAtomKeys,
655
+ requiredAtomKeys: requiredAtomKeys(coverage), catalogHash: coverage.catalogHash,
656
+ }, { now: this.now });
657
+ phase = "workspace";
658
+ await persist();
659
+ }
660
+ while (planningTurns < maxPlanningTurns) {
661
+ planningTurns += 1;
662
+ workspaceTurnsInSession += 1;
663
+ const requestId = `${input.requestId}:workspace:${planningTurns}`;
664
+ lastSubmittedRequestId = requestId;
665
+ const payload = workspacePayload({
666
+ plannerInput: input,
667
+ catalog: planningCatalog,
668
+ workspace,
669
+ target,
670
+ turn: planningTurns,
671
+ maximum: maxPlanningTurns,
672
+ includeFullCatalog: workspaceTurnsInSession === 1,
673
+ validationErrors,
674
+ ...(options.promptReferenceDirectory ? { referenceDirectory: options.promptReferenceDirectory } : {}),
675
+ ...(lastTransactionResult ? { previousResult: lastTransactionResult } : {}),
676
+ });
677
+ const turn = {
678
+ schemaVersion: "1.0", requestId, phase: "workspace", payload,
679
+ ...(validationErrors.length ? { previousValidationErrors: feedback(validationErrors, maxErrors) } : {}),
680
+ };
681
+ promptPayloadBytes += Buffer.byteLength(JSON.stringify(turn));
682
+ await persist();
683
+ try {
684
+ const result = await session.plan(turn);
685
+ attempts.push(result.attempt);
686
+ rawOutputs.push(result.rawOutput);
687
+ const parsed = PlanningWorkspaceActionSchema.safeParse(result.document);
688
+ if (!parsed.success) {
689
+ validationErrors = schemaIssues(parsed.error.issues, "draft-schema");
690
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "rejected", errors: validationErrors });
691
+ }
692
+ else if (parsed.data.kind === "workspace-plan") {
693
+ lastTransactionResult = workspace.applyTransaction(parsed.data.transaction);
694
+ if (lastTransactionResult.status === "committed") {
695
+ validationErrors = [];
696
+ plannerDraft = lastTransactionResult.revision.plannerDraft;
697
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "committed", result: lastTransactionResult, errors: [] });
698
+ if (parsed.data.finalize) {
699
+ const finalized = workspace.finalize({ revision: lastTransactionResult.revision.revision, draftHash: lastTransactionResult.revision.draftHash });
700
+ plannerDraft = finalized.revision.plannerDraft;
701
+ phase = "completed";
702
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "finalized", errors: [] });
703
+ await persist();
704
+ return Object.freeze({ workflow: Object.freeze(finalized.workflow), ir: finalized.ir, attempts: Object.freeze([...attempts]), rawOutputs: Object.freeze([...rawOutputs]), audit: audit() });
705
+ }
706
+ }
707
+ else {
708
+ validationErrors = resultErrors(lastTransactionResult);
709
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "rejected", result: lastTransactionResult, errors: validationErrors });
710
+ }
711
+ }
712
+ else if (parsed.data.kind === "workspace-report-gap") {
713
+ workspace.reportGap(parsed.data.report);
714
+ phase = parsed.data.report.kind === "missing-atom" ? "blocked_missing_atom" : "blocked_missing_input";
715
+ validationErrors = [{ code: parsed.data.report.code, stage: "data-link", path: parsed.data.report.paths[0] ?? "$", message: parsed.data.report.message, hint: parsed.data.report.resolution }];
716
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "blocked", errors: validationErrors });
717
+ await persist();
718
+ throw fail(parsed.data.report.message, parsed.data.report.code);
719
+ }
720
+ else {
721
+ const finalized = workspace.finalize({ revision: parsed.data.revision, draftHash: parsed.data.draftHash });
722
+ plannerDraft = finalized.revision.plannerDraft;
723
+ phase = "completed";
724
+ validationErrors = [];
725
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: result.rawOutput, status: "finalized", errors: [] });
726
+ await persist();
727
+ return Object.freeze({ workflow: Object.freeze(finalized.workflow), ir: finalized.ir, attempts: Object.freeze([...attempts]), rawOutputs: Object.freeze([...rawOutputs]), audit: audit() });
728
+ }
729
+ }
730
+ catch (error) {
731
+ if (error instanceof WorkflowPlanningFailedError)
732
+ throw error;
733
+ if (error instanceof PlanningModelExecutionError) {
734
+ attempts.push(error.attempt);
735
+ if (error.rawOutput !== undefined)
736
+ rawOutputs.push(error.rawOutput);
737
+ const repairable = outputFailure(error);
738
+ if (!repairable)
739
+ throw fail(error.message, error.code, error.retryable, error);
740
+ validationErrors = [repairable];
741
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: error.rawOutput ?? "", status: "rejected", errors: validationErrors });
742
+ }
743
+ else {
744
+ const code = typeof error?.code === "string" ? error.code : undefined;
745
+ if (!code?.startsWith("PLANNING_"))
746
+ throw error;
747
+ const caused = validationErrorsFromCause(error);
748
+ validationErrors = caused.length ? caused : [{ code: "PLANNER_DRAFT_NORMALIZATION_FAILED", stage: "draft-normalization", path: "$", message: `${code}: ${error instanceof Error ? error.message : String(error)}`, hint: "Use the current Workspace Revision/hash and resolve all validation issues before finalizing" }];
749
+ workspaceAttempts.push({ requestId, planningTurn: planningTurns, rawOutput: "", status: "rejected", errors: validationErrors });
750
+ }
751
+ }
752
+ validationFeedback.push(feedback(validationErrors, maxErrors));
753
+ await persist();
754
+ }
755
+ validationErrors = [{
756
+ code: "PLANNER_TURN_LIMIT_EXCEEDED", stage: "draft-normalization", path: "$",
757
+ message: `Planning Workspace turn budget exhausted: ${maxPlanningTurns}`,
758
+ expected: maxPlanningTurns, received: planningTurns,
759
+ hint: "Increase maxPlanningTurns or add more complete control regions per transaction",
760
+ }];
761
+ await persist();
762
+ throw fail(`Planner did not finalize the Planning Workspace after ${maxPlanningTurns} turns`, "PLANNER_TURN_LIMIT_EXCEEDED");
763
+ });
764
+ }
765
+ catch (error) {
766
+ if (error instanceof WorkflowPlanningFailedError)
767
+ throw error;
768
+ const code = typeof error?.code === "string" ? error.code : "WORKFLOW_PLANNING_FAILED";
769
+ throw fail(error instanceof Error ? error.message : String(error), code, error?.retryable === true, error);
770
+ }
771
+ }
772
+ }
773
+ //# sourceMappingURL=planning-controller.js.map