@nifrajs/coding-agent 3.2.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 (211) hide show
  1. package/EXTENSIONS.md +25 -0
  2. package/PERFORMANCE.md +19 -0
  3. package/README.md +69 -0
  4. package/REGISTRY.md +15 -0
  5. package/SECURITY.md +32 -0
  6. package/dist/approvals.d.ts +70 -0
  7. package/dist/approvals.d.ts.map +1 -0
  8. package/dist/approvals.js +174 -0
  9. package/dist/approvals.js.map +1 -0
  10. package/dist/capabilities.d.ts +20 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +49 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/cli.d.ts +24 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +380 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/context.d.ts +17 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +66 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/deployment-adapters.d.ts +40 -0
  23. package/dist/deployment-adapters.d.ts.map +1 -0
  24. package/dist/deployment-adapters.js +147 -0
  25. package/dist/deployment-adapters.js.map +1 -0
  26. package/dist/diff.d.ts +17 -0
  27. package/dist/diff.d.ts.map +1 -0
  28. package/dist/diff.js +61 -0
  29. package/dist/diff.js.map +1 -0
  30. package/dist/errors.d.ts +9 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +35 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/extensions.d.ts +104 -0
  35. package/dist/extensions.d.ts.map +1 -0
  36. package/dist/extensions.js +284 -0
  37. package/dist/extensions.js.map +1 -0
  38. package/dist/handoffs.d.ts +97 -0
  39. package/dist/handoffs.d.ts.map +1 -0
  40. package/dist/handoffs.js +199 -0
  41. package/dist/handoffs.js.map +1 -0
  42. package/dist/healing.d.ts +56 -0
  43. package/dist/healing.d.ts.map +1 -0
  44. package/dist/healing.js +54 -0
  45. package/dist/healing.js.map +1 -0
  46. package/dist/host.d.ts +61 -0
  47. package/dist/host.d.ts.map +1 -0
  48. package/dist/host.js +369 -0
  49. package/dist/host.js.map +1 -0
  50. package/dist/index.d.ts +26 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +26 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/isolated-worker.d.ts +2 -0
  55. package/dist/isolated-worker.d.ts.map +1 -0
  56. package/dist/isolated-worker.js +155 -0
  57. package/dist/isolated-worker.js.map +1 -0
  58. package/dist/isolated.d.ts +52 -0
  59. package/dist/isolated.d.ts.map +1 -0
  60. package/dist/isolated.js +293 -0
  61. package/dist/isolated.js.map +1 -0
  62. package/dist/native.d.ts +92 -0
  63. package/dist/native.d.ts.map +1 -0
  64. package/dist/native.js +339 -0
  65. package/dist/native.js.map +1 -0
  66. package/dist/orchestration/artifact-port.d.ts +30 -0
  67. package/dist/orchestration/artifact-port.d.ts.map +1 -0
  68. package/dist/orchestration/artifact-port.js +59 -0
  69. package/dist/orchestration/artifact-port.js.map +1 -0
  70. package/dist/orchestration/catalog.d.ts +71 -0
  71. package/dist/orchestration/catalog.d.ts.map +1 -0
  72. package/dist/orchestration/catalog.js +46 -0
  73. package/dist/orchestration/catalog.js.map +1 -0
  74. package/dist/orchestration/compile.d.ts +59 -0
  75. package/dist/orchestration/compile.d.ts.map +1 -0
  76. package/dist/orchestration/compile.js +268 -0
  77. package/dist/orchestration/compile.js.map +1 -0
  78. package/dist/orchestration/dispatch.d.ts +115 -0
  79. package/dist/orchestration/dispatch.d.ts.map +1 -0
  80. package/dist/orchestration/dispatch.js +220 -0
  81. package/dist/orchestration/dispatch.js.map +1 -0
  82. package/dist/orchestration/durable-jobs.d.ts +35 -0
  83. package/dist/orchestration/durable-jobs.d.ts.map +1 -0
  84. package/dist/orchestration/durable-jobs.js +273 -0
  85. package/dist/orchestration/durable-jobs.js.map +1 -0
  86. package/dist/orchestration/effect-key.d.ts +23 -0
  87. package/dist/orchestration/effect-key.d.ts.map +1 -0
  88. package/dist/orchestration/effect-key.js +24 -0
  89. package/dist/orchestration/effect-key.js.map +1 -0
  90. package/dist/orchestration/evidence-store.d.ts +75 -0
  91. package/dist/orchestration/evidence-store.d.ts.map +1 -0
  92. package/dist/orchestration/evidence-store.js +183 -0
  93. package/dist/orchestration/evidence-store.js.map +1 -0
  94. package/dist/orchestration/hash.d.ts +12 -0
  95. package/dist/orchestration/hash.d.ts.map +1 -0
  96. package/dist/orchestration/hash.js +43 -0
  97. package/dist/orchestration/hash.js.map +1 -0
  98. package/dist/orchestration/host.d.ts +121 -0
  99. package/dist/orchestration/host.d.ts.map +1 -0
  100. package/dist/orchestration/host.js +334 -0
  101. package/dist/orchestration/host.js.map +1 -0
  102. package/dist/orchestration/index.d.ts +20 -0
  103. package/dist/orchestration/index.d.ts.map +1 -0
  104. package/dist/orchestration/index.js +19 -0
  105. package/dist/orchestration/index.js.map +1 -0
  106. package/dist/orchestration/policy.d.ts +63 -0
  107. package/dist/orchestration/policy.d.ts.map +1 -0
  108. package/dist/orchestration/policy.js +80 -0
  109. package/dist/orchestration/policy.js.map +1 -0
  110. package/dist/orchestration/recovery.d.ts +87 -0
  111. package/dist/orchestration/recovery.d.ts.map +1 -0
  112. package/dist/orchestration/recovery.js +197 -0
  113. package/dist/orchestration/recovery.js.map +1 -0
  114. package/dist/orchestration/tracer.d.ts +31 -0
  115. package/dist/orchestration/tracer.d.ts.map +1 -0
  116. package/dist/orchestration/tracer.js +141 -0
  117. package/dist/orchestration/tracer.js.map +1 -0
  118. package/dist/plans.d.ts +35 -0
  119. package/dist/plans.d.ts.map +1 -0
  120. package/dist/plans.js +63 -0
  121. package/dist/plans.js.map +1 -0
  122. package/dist/presets.d.ts +10 -0
  123. package/dist/presets.d.ts.map +1 -0
  124. package/dist/presets.js +70 -0
  125. package/dist/presets.js.map +1 -0
  126. package/dist/process.d.ts +6 -0
  127. package/dist/process.d.ts.map +1 -0
  128. package/dist/process.js +42 -0
  129. package/dist/process.js.map +1 -0
  130. package/dist/registry.d.ts +37 -0
  131. package/dist/registry.d.ts.map +1 -0
  132. package/dist/registry.js +51 -0
  133. package/dist/registry.js.map +1 -0
  134. package/dist/replay.d.ts +24 -0
  135. package/dist/replay.d.ts.map +1 -0
  136. package/dist/replay.js +154 -0
  137. package/dist/replay.js.map +1 -0
  138. package/dist/rpc.d.ts +51 -0
  139. package/dist/rpc.d.ts.map +1 -0
  140. package/dist/rpc.js +412 -0
  141. package/dist/rpc.js.map +1 -0
  142. package/dist/session-migration.d.ts +59 -0
  143. package/dist/session-migration.d.ts.map +1 -0
  144. package/dist/session-migration.js +315 -0
  145. package/dist/session-migration.js.map +1 -0
  146. package/dist/sessions.d.ts +74 -0
  147. package/dist/sessions.d.ts.map +1 -0
  148. package/dist/sessions.js +222 -0
  149. package/dist/sessions.js.map +1 -0
  150. package/dist/subagents.d.ts +51 -0
  151. package/dist/subagents.d.ts.map +1 -0
  152. package/dist/subagents.js +141 -0
  153. package/dist/subagents.js.map +1 -0
  154. package/dist/tools.d.ts +13 -0
  155. package/dist/tools.d.ts.map +1 -0
  156. package/dist/tools.js +37 -0
  157. package/dist/tools.js.map +1 -0
  158. package/dist/ui.d.ts +42 -0
  159. package/dist/ui.d.ts.map +1 -0
  160. package/dist/ui.js +93 -0
  161. package/dist/ui.js.map +1 -0
  162. package/dist/verification.d.ts +34 -0
  163. package/dist/verification.d.ts.map +1 -0
  164. package/dist/verification.js +91 -0
  165. package/dist/verification.js.map +1 -0
  166. package/dist/workflows.d.ts +98 -0
  167. package/dist/workflows.d.ts.map +1 -0
  168. package/dist/workflows.js +174 -0
  169. package/dist/workflows.js.map +1 -0
  170. package/package.json +84 -0
  171. package/src/approvals.ts +249 -0
  172. package/src/capabilities.ts +74 -0
  173. package/src/cli.ts +405 -0
  174. package/src/context.ts +81 -0
  175. package/src/deployment-adapters.ts +181 -0
  176. package/src/diff.ts +80 -0
  177. package/src/errors.ts +46 -0
  178. package/src/extensions.ts +402 -0
  179. package/src/handoffs.ts +299 -0
  180. package/src/healing.ts +93 -0
  181. package/src/host.ts +465 -0
  182. package/src/index.ts +165 -0
  183. package/src/isolated-worker.ts +177 -0
  184. package/src/isolated.ts +344 -0
  185. package/src/native.ts +478 -0
  186. package/src/orchestration/artifact-port.ts +75 -0
  187. package/src/orchestration/catalog.ts +102 -0
  188. package/src/orchestration/compile.ts +373 -0
  189. package/src/orchestration/dispatch.ts +383 -0
  190. package/src/orchestration/durable-jobs.ts +374 -0
  191. package/src/orchestration/effect-key.ts +35 -0
  192. package/src/orchestration/evidence-store.ts +233 -0
  193. package/src/orchestration/hash.ts +43 -0
  194. package/src/orchestration/host.ts +445 -0
  195. package/src/orchestration/index.ts +131 -0
  196. package/src/orchestration/policy.ts +116 -0
  197. package/src/orchestration/recovery.ts +271 -0
  198. package/src/orchestration/tracer.ts +177 -0
  199. package/src/plans.ts +105 -0
  200. package/src/presets.ts +90 -0
  201. package/src/process.ts +47 -0
  202. package/src/registry.ts +80 -0
  203. package/src/replay.ts +190 -0
  204. package/src/rpc.ts +598 -0
  205. package/src/session-migration.ts +401 -0
  206. package/src/sessions.ts +303 -0
  207. package/src/subagents.ts +210 -0
  208. package/src/tools.ts +52 -0
  209. package/src/ui.ts +137 -0
  210. package/src/verification.ts +133 -0
  211. package/src/workflows.ts +279 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Compile a declarative {@link RunPlan} into the existing {@link WorkflowRunner} step tree. There is
3
+ * no second execution engine and no copied scheduler: the plan's top-level DAG is layered
4
+ * (deterministic, id tie-break) into the kernel's `sequence`/`parallel` primitives, and every node -
5
+ * leaf or structural - is lowered onto `task`/`verify`/`approve`/`checkpoint`/`handoff`/`retry`/
6
+ * `branch` steps or, for a `subagent` node, onto a {@link BoundedSubagentRunner}. Effect-key
7
+ * derivation and the artifact sink are wired into each leaf closure at compile time.
8
+ *
9
+ * All structural ceilings (node count, nesting depth, parallel breadth, pinned version, and the
10
+ * capability authority ceiling) are enforced here, before a single node runs.
11
+ */
12
+
13
+ import type {
14
+ ArtifactContext,
15
+ ArtifactPort,
16
+ ArtifactRef,
17
+ NodeEffectKey,
18
+ RunNode,
19
+ RunPlan,
20
+ } from "@nifrajs/agent-protocol"
21
+ import { parseRunPlan } from "@nifrajs/agent-protocol"
22
+ import { BoundedSubagentRunner } from "../subagents.ts"
23
+ import type { WorkflowContext, WorkflowStep } from "../workflows.ts"
24
+ import { type CatalogStep, type StepCatalog, type StepRunContext, stepVersion } from "./catalog.ts"
25
+ import { deriveNodeEffectKey } from "./effect-key.ts"
26
+ import { canonicalJson, sha256HexOf } from "./hash.ts"
27
+
28
+ /** Thrown when a plan cannot be lowered. `code` is a stable evidence code. */
29
+ export class CompileError extends Error {
30
+ constructor(
31
+ reason: string,
32
+ readonly code: string,
33
+ ) {
34
+ super(`orchestration compile: ${reason}`)
35
+ this.name = "CompileError"
36
+ }
37
+ }
38
+
39
+ /** Host-owned ceilings. A plan can only tighten, never widen, these. */
40
+ export interface OrchestrationLimits {
41
+ /** Total nodes in the whole tree. Default 256. */
42
+ readonly maxNodes?: number
43
+ /** Maximum structural nesting depth. Default 8. */
44
+ readonly maxDepth?: number
45
+ /** Host concurrency ceiling for any parallel layer or node. Default: the child count. */
46
+ readonly maxParallel?: number
47
+ /** Bounded-subagent child ceiling. Default 4. */
48
+ readonly maxChildren?: number
49
+ /** Capability authority ceiling. `undefined` grants all; otherwise a step's needs must be a subset. */
50
+ readonly allowedCapabilities?: readonly string[]
51
+ }
52
+
53
+ const DEFAULT_MAX_NODES = 256
54
+ const DEFAULT_MAX_DEPTH = 8
55
+ const DEFAULT_MAX_CHILDREN = 4
56
+
57
+ /** Stable digest of a plan's canonical form. Structural identity that node effect keys hang from. */
58
+ export function digestRunPlan(plan: RunPlan): Promise<string> {
59
+ return sha256HexOf(canonicalJson(plan))
60
+ }
61
+
62
+ /** Wiring the compiler injects into every node closure. */
63
+ export interface CompileOptions {
64
+ readonly catalog: StepCatalog
65
+ readonly planDigest: string
66
+ readonly artifactPort: ArtifactPort
67
+ readonly limits?: OrchestrationLimits
68
+ /** Called once a node's content-free effect key is derived (idempotent steps only). */
69
+ onNodeEffect?(nodeId: string, key: NodeEffectKey): void
70
+ /** Called for each artifact ref a node produces through its port. */
71
+ collectArtifact?(nodeId: string, ref: ArtifactRef): void
72
+ }
73
+
74
+ /**
75
+ * Compile `source` (a RunPlan or its serialized form) into a single WorkflowStep. The input is
76
+ * always re-parsed through {@link parseRunPlan}, so a malformed or content-bearing plan fails closed.
77
+ */
78
+ export function compileRunPlan(source: RunPlan | unknown, options: CompileOptions): WorkflowStep {
79
+ const layerSteps = compileRunPlanLayers(source, options)
80
+ return layerSteps.length === 1
81
+ ? (layerSteps[0] as WorkflowStep)
82
+ : { type: "sequence", steps: layerSteps }
83
+ }
84
+
85
+ /**
86
+ * Compile `source` into one {@link WorkflowStep} per top-level DAG layer, preserving the same
87
+ * ceilings and lowering as {@link compileRunPlan}. A driver can run the layers in order and hold at
88
+ * a layer boundary (a safe-pause point) without a second scheduler: each layer is still executed by
89
+ * the kernel {@link WorkflowRunner}, and the layer split is the very partition compileRunPlan folds
90
+ * into a single `sequence`.
91
+ */
92
+ export function compileRunPlanLayers(
93
+ source: RunPlan | unknown,
94
+ options: CompileOptions,
95
+ ): readonly WorkflowStep[] {
96
+ const plan = parseRunPlan(source)
97
+ const limits = options.limits ?? {}
98
+ const total = plan.nodes.reduce((sum, node) => sum + countNodes(node), 0)
99
+ const maxNodes = limits.maxNodes ?? DEFAULT_MAX_NODES
100
+ if (total > maxNodes)
101
+ throw new CompileError(`plan has ${total} nodes over the ${maxNodes} ceiling`, "E_MAX_NODES")
102
+
103
+ const layers = layerTopLevel(plan.nodes)
104
+ return layers.map((layer) => compileLayer(layer, options, 1))
105
+ }
106
+
107
+ /** Recursive node count over the whole tree. */
108
+ function countNodes(node: RunNode): number {
109
+ switch (node.kind) {
110
+ case "sequence":
111
+ case "parallel":
112
+ return 1 + node.children.reduce((sum, child) => sum + countNodes(child), 0)
113
+ case "retry":
114
+ return 1 + countNodes(node.child)
115
+ case "branch":
116
+ return 1 + countNodes(node.then) + (node.otherwise ? countNodes(node.otherwise) : 0)
117
+ default:
118
+ return 1
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Kahn layering of the top-level DAG. Each layer is every node whose dependencies are already
124
+ * emitted, sorted by id for determinism. A layer with more than one node becomes a bounded parallel.
125
+ */
126
+ function layerTopLevel(nodes: readonly RunNode[]): readonly (readonly RunNode[])[] {
127
+ const byId = new Map(nodes.map((node) => [node.id, node] as const))
128
+ const remaining = new Set(nodes.map((node) => node.id))
129
+ const emitted = new Set<string>()
130
+ const layers: RunNode[][] = []
131
+ while (remaining.size > 0) {
132
+ const ready = [...remaining]
133
+ .filter((id) => (byId.get(id)?.dependsOn ?? []).every((dep) => emitted.has(dep)))
134
+ .sort()
135
+ if (ready.length === 0) throw new CompileError("run plan has a dependency cycle", "E_CYCLE")
136
+ for (const id of ready) {
137
+ emitted.add(id)
138
+ remaining.delete(id)
139
+ }
140
+ layers.push(ready.map((id) => byId.get(id) as RunNode))
141
+ }
142
+ return layers
143
+ }
144
+
145
+ function compileLayer(
146
+ layer: readonly RunNode[],
147
+ options: CompileOptions,
148
+ depth: number,
149
+ ): WorkflowStep {
150
+ if (layer.length === 1) return compileNode(layer[0] as RunNode, options, depth)
151
+ const steps = layer.map((node) => compileNode(node, options, depth))
152
+ return { type: "parallel", steps, maxConcurrency: concurrency(steps.length, undefined, options) }
153
+ }
154
+
155
+ /** Effective concurrency: the lower of plan request, host ceiling, and child count. */
156
+ function concurrency(
157
+ breadth: number,
158
+ requested: number | undefined,
159
+ options: CompileOptions,
160
+ ): number {
161
+ const host = options.limits?.maxParallel ?? breadth
162
+ return Math.max(1, Math.min(breadth, requested ?? breadth, host))
163
+ }
164
+
165
+ function compileNode(node: RunNode, options: CompileOptions, depth: number): WorkflowStep {
166
+ const maxDepth = options.limits?.maxDepth ?? DEFAULT_MAX_DEPTH
167
+ if (depth > maxDepth)
168
+ throw new CompileError(
169
+ `node '${node.id}' nests past the ${maxDepth} depth ceiling`,
170
+ "E_MAX_DEPTH",
171
+ )
172
+
173
+ switch (node.kind) {
174
+ case "sequence":
175
+ return {
176
+ type: "sequence",
177
+ steps: node.children.map((c) => compileNode(c, options, depth + 1)),
178
+ }
179
+ case "parallel": {
180
+ const steps = node.children.map((c) => compileNode(c, options, depth + 1))
181
+ return {
182
+ type: "parallel",
183
+ steps,
184
+ maxConcurrency: concurrency(steps.length, node.maxConcurrency, options),
185
+ }
186
+ }
187
+ case "retry": {
188
+ const step: WorkflowStep = {
189
+ type: "retry",
190
+ step: compileNode(node.child, options, depth + 1),
191
+ attempts: node.attempts,
192
+ ...(node.backoffMs !== undefined ? { backoffMs: node.backoffMs } : {}),
193
+ }
194
+ return step
195
+ }
196
+ case "branch": {
197
+ const predicate = resolveStep(node, options)
198
+ if (typeof predicate.when !== "function")
199
+ throw new CompileError(
200
+ `branch node '${node.id}' step declares no predicate`,
201
+ "E_MISSING_HANDLER",
202
+ )
203
+ return {
204
+ type: "branch",
205
+ id: node.id,
206
+ when: (wf) => Promise.resolve(predicate.when!(makeContext(node, options, wf))),
207
+ // biome-ignore lint/suspicious/noThenProperty: the workflow branch step names its arms then/otherwise.
208
+ then: compileNode(node.then, options, depth + 1),
209
+ ...(node.otherwise !== undefined
210
+ ? { otherwise: compileNode(node.otherwise, options, depth + 1) }
211
+ : {}),
212
+ }
213
+ }
214
+ default:
215
+ return compileLeaf(node, options)
216
+ }
217
+ }
218
+
219
+ /** Resolve and validate the catalog step behind a leaf or branch node. */
220
+ function resolveStep(
221
+ node: Extract<RunNode, { readonly step: string }>,
222
+ options: CompileOptions,
223
+ ): CatalogStep {
224
+ const step = options.catalog.get(node.step)
225
+ if (step === undefined)
226
+ throw new CompileError(
227
+ `node '${node.id}' references unknown step '${node.step}'`,
228
+ "E_UNKNOWN_STEP",
229
+ )
230
+ if (step.kind !== node.kind)
231
+ throw new CompileError(
232
+ `node '${node.id}' kind '${node.kind}' does not match catalog step kind '${step.kind}'`,
233
+ "E_KIND_MISMATCH",
234
+ )
235
+ if (
236
+ "stepVersion" in node &&
237
+ node.stepVersion !== undefined &&
238
+ stepVersion(step) !== node.stepVersion
239
+ )
240
+ throw new CompileError(
241
+ `node '${node.id}' pins step version ${node.stepVersion} but catalog has ${stepVersion(step)}`,
242
+ "E_VERSION_DRIFT",
243
+ )
244
+ const allowed = options.limits?.allowedCapabilities
245
+ if (allowed !== undefined) {
246
+ const granted = new Set(allowed)
247
+ const denied = (step.capabilities ?? []).find((cap) => !granted.has(cap))
248
+ if (denied !== undefined)
249
+ throw new CompileError(
250
+ `node '${node.id}' needs ungranted capability '${denied}'`,
251
+ "E_CAPABILITY",
252
+ )
253
+ }
254
+ return step
255
+ }
256
+
257
+ function compileLeaf(
258
+ node: Extract<RunNode, { readonly step: string; readonly kind: string }>,
259
+ options: CompileOptions,
260
+ ): WorkflowStep {
261
+ const step = resolveStep(node, options)
262
+ const base = buildLeafStep(node, step, options)
263
+ const retry = "retry" in node ? node.retry : undefined
264
+ if (retry === undefined) return base
265
+ return {
266
+ type: "retry",
267
+ step: base,
268
+ attempts: retry.attempts,
269
+ ...(retry.backoffMs !== undefined ? { backoffMs: retry.backoffMs } : {}),
270
+ }
271
+ }
272
+
273
+ function buildLeafStep(node: RunNode, step: CatalogStep, options: CompileOptions): WorkflowStep {
274
+ const id = node.id
275
+ switch (node.kind) {
276
+ case "subagent": {
277
+ if (step.executor === undefined || typeof step.spec !== "function")
278
+ throw new CompileError(
279
+ `subagent node '${id}' step declares no executor or spec`,
280
+ "E_MISSING_HANDLER",
281
+ )
282
+ return { type: "task", id, run: (wf) => runSubagent(node, step, options, wf) }
283
+ }
284
+ case "verify":
285
+ return { type: "verify", id, run: (wf) => runLeaf(node, step, options, wf) }
286
+ case "approve":
287
+ return {
288
+ type: "approve",
289
+ id,
290
+ reason: "reason" in node && node.reason !== undefined ? node.reason : "",
291
+ run: (wf) => runLeaf(node, step, options, wf),
292
+ }
293
+ case "checkpoint":
294
+ return { type: "checkpoint", id, run: (wf) => runLeaf(node, step, options, wf) }
295
+ case "handoff":
296
+ return { type: "handoff", id, run: (wf) => runLeaf(node, step, options, wf) }
297
+ default:
298
+ return { type: "task", id, run: (wf) => runLeaf(node, step, options, wf) }
299
+ }
300
+ }
301
+
302
+ /** Build the step context, wiring the artifact collector and effect-key derivation. */
303
+ function makeContext(node: RunNode, options: CompileOptions, wf: WorkflowContext): StepRunContext {
304
+ const artifact: ArtifactPort = {
305
+ put: async (payload: Uint8Array, ctx: ArtifactContext): Promise<ArtifactRef> => {
306
+ const ref = await options.artifactPort.put(payload, ctx)
307
+ options.collectArtifact?.(node.id, ref)
308
+ return ref
309
+ },
310
+ }
311
+ return {
312
+ nodeId: node.id,
313
+ planDigest: options.planDigest,
314
+ signal: wf.signal,
315
+ artifact,
316
+ values: wf.values,
317
+ set: (name, value) => wf.set(name, value),
318
+ }
319
+ }
320
+
321
+ async function deriveEffect(
322
+ node: RunNode,
323
+ step: CatalogStep,
324
+ options: CompileOptions,
325
+ wf: WorkflowContext,
326
+ ): Promise<void> {
327
+ if (step.selectEffect === undefined) return
328
+ const selector = step.selectEffect({
329
+ nodeId: node.id,
330
+ planDigest: options.planDigest,
331
+ values: wf.values,
332
+ })
333
+ const key = await deriveNodeEffectKey({
334
+ planDigest: options.planDigest,
335
+ nodeId: node.id,
336
+ selector,
337
+ })
338
+ options.onNodeEffect?.(node.id, key)
339
+ }
340
+
341
+ async function runLeaf(
342
+ node: RunNode,
343
+ step: CatalogStep,
344
+ options: CompileOptions,
345
+ wf: WorkflowContext,
346
+ ): Promise<unknown> {
347
+ if (typeof step.run !== "function")
348
+ throw new CompileError(`node '${node.id}' step declares no run body`, "E_MISSING_HANDLER")
349
+ const context = makeContext(node, options, wf)
350
+ await deriveEffect(node, step, options, wf)
351
+ return step.run(context)
352
+ }
353
+
354
+ async function runSubagent(
355
+ node: RunNode,
356
+ step: CatalogStep,
357
+ options: CompileOptions,
358
+ wf: WorkflowContext,
359
+ ): Promise<unknown> {
360
+ const context = makeContext(node, options, wf)
361
+ await deriveEffect(node, step, options, wf)
362
+ const runner = new BoundedSubagentRunner(step.executor!, {
363
+ maxChildren: options.limits?.maxChildren ?? DEFAULT_MAX_CHILDREN,
364
+ signal: wf.signal,
365
+ ...(options.limits?.allowedCapabilities !== undefined
366
+ ? { allowedCapabilities: options.limits.allowedCapabilities }
367
+ : {}),
368
+ })
369
+ const spec = await step.spec!(context)
370
+ const result = await runner.run(spec)
371
+ if (!result.ok) throw new Error(result.error ?? `subagent '${node.id}' failed`)
372
+ return result.output
373
+ }