@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.
- package/EXTENSIONS.md +25 -0
- package/PERFORMANCE.md +19 -0
- package/README.md +69 -0
- package/REGISTRY.md +15 -0
- package/SECURITY.md +32 -0
- package/dist/approvals.d.ts +70 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +174 -0
- package/dist/approvals.js.map +1 -0
- package/dist/capabilities.d.ts +20 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +49 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +380 -0
- package/dist/cli.js.map +1 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +66 -0
- package/dist/context.js.map +1 -0
- package/dist/deployment-adapters.d.ts +40 -0
- package/dist/deployment-adapters.d.ts.map +1 -0
- package/dist/deployment-adapters.js +147 -0
- package/dist/deployment-adapters.js.map +1 -0
- package/dist/diff.d.ts +17 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +61 -0
- package/dist/diff.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions.d.ts +104 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +284 -0
- package/dist/extensions.js.map +1 -0
- package/dist/handoffs.d.ts +97 -0
- package/dist/handoffs.d.ts.map +1 -0
- package/dist/handoffs.js +199 -0
- package/dist/handoffs.js.map +1 -0
- package/dist/healing.d.ts +56 -0
- package/dist/healing.d.ts.map +1 -0
- package/dist/healing.js +54 -0
- package/dist/healing.js.map +1 -0
- package/dist/host.d.ts +61 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +369 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/isolated-worker.d.ts +2 -0
- package/dist/isolated-worker.d.ts.map +1 -0
- package/dist/isolated-worker.js +155 -0
- package/dist/isolated-worker.js.map +1 -0
- package/dist/isolated.d.ts +52 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +293 -0
- package/dist/isolated.js.map +1 -0
- package/dist/native.d.ts +92 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +339 -0
- package/dist/native.js.map +1 -0
- package/dist/orchestration/artifact-port.d.ts +30 -0
- package/dist/orchestration/artifact-port.d.ts.map +1 -0
- package/dist/orchestration/artifact-port.js +59 -0
- package/dist/orchestration/artifact-port.js.map +1 -0
- package/dist/orchestration/catalog.d.ts +71 -0
- package/dist/orchestration/catalog.d.ts.map +1 -0
- package/dist/orchestration/catalog.js +46 -0
- package/dist/orchestration/catalog.js.map +1 -0
- package/dist/orchestration/compile.d.ts +59 -0
- package/dist/orchestration/compile.d.ts.map +1 -0
- package/dist/orchestration/compile.js +268 -0
- package/dist/orchestration/compile.js.map +1 -0
- package/dist/orchestration/dispatch.d.ts +115 -0
- package/dist/orchestration/dispatch.d.ts.map +1 -0
- package/dist/orchestration/dispatch.js +220 -0
- package/dist/orchestration/dispatch.js.map +1 -0
- package/dist/orchestration/durable-jobs.d.ts +35 -0
- package/dist/orchestration/durable-jobs.d.ts.map +1 -0
- package/dist/orchestration/durable-jobs.js +273 -0
- package/dist/orchestration/durable-jobs.js.map +1 -0
- package/dist/orchestration/effect-key.d.ts +23 -0
- package/dist/orchestration/effect-key.d.ts.map +1 -0
- package/dist/orchestration/effect-key.js +24 -0
- package/dist/orchestration/effect-key.js.map +1 -0
- package/dist/orchestration/evidence-store.d.ts +75 -0
- package/dist/orchestration/evidence-store.d.ts.map +1 -0
- package/dist/orchestration/evidence-store.js +183 -0
- package/dist/orchestration/evidence-store.js.map +1 -0
- package/dist/orchestration/hash.d.ts +12 -0
- package/dist/orchestration/hash.d.ts.map +1 -0
- package/dist/orchestration/hash.js +43 -0
- package/dist/orchestration/hash.js.map +1 -0
- package/dist/orchestration/host.d.ts +121 -0
- package/dist/orchestration/host.d.ts.map +1 -0
- package/dist/orchestration/host.js +334 -0
- package/dist/orchestration/host.js.map +1 -0
- package/dist/orchestration/index.d.ts +20 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +19 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/policy.d.ts +63 -0
- package/dist/orchestration/policy.d.ts.map +1 -0
- package/dist/orchestration/policy.js +80 -0
- package/dist/orchestration/policy.js.map +1 -0
- package/dist/orchestration/recovery.d.ts +87 -0
- package/dist/orchestration/recovery.d.ts.map +1 -0
- package/dist/orchestration/recovery.js +197 -0
- package/dist/orchestration/recovery.js.map +1 -0
- package/dist/orchestration/tracer.d.ts +31 -0
- package/dist/orchestration/tracer.d.ts.map +1 -0
- package/dist/orchestration/tracer.js +141 -0
- package/dist/orchestration/tracer.js.map +1 -0
- package/dist/plans.d.ts +35 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +63 -0
- package/dist/plans.js.map +1 -0
- package/dist/presets.d.ts +10 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +70 -0
- package/dist/presets.js.map +1 -0
- package/dist/process.d.ts +6 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +42 -0
- package/dist/process.js.map +1 -0
- package/dist/registry.d.ts +37 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +51 -0
- package/dist/registry.js.map +1 -0
- package/dist/replay.d.ts +24 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +154 -0
- package/dist/replay.js.map +1 -0
- package/dist/rpc.d.ts +51 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +412 -0
- package/dist/rpc.js.map +1 -0
- package/dist/session-migration.d.ts +59 -0
- package/dist/session-migration.d.ts.map +1 -0
- package/dist/session-migration.js +315 -0
- package/dist/session-migration.js.map +1 -0
- package/dist/sessions.d.ts +74 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +222 -0
- package/dist/sessions.js.map +1 -0
- package/dist/subagents.d.ts +51 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +141 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools.d.ts +13 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +37 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui.d.ts +42 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +93 -0
- package/dist/ui.js.map +1 -0
- package/dist/verification.d.ts +34 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +91 -0
- package/dist/verification.js.map +1 -0
- package/dist/workflows.d.ts +98 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +174 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +84 -0
- package/src/approvals.ts +249 -0
- package/src/capabilities.ts +74 -0
- package/src/cli.ts +405 -0
- package/src/context.ts +81 -0
- package/src/deployment-adapters.ts +181 -0
- package/src/diff.ts +80 -0
- package/src/errors.ts +46 -0
- package/src/extensions.ts +402 -0
- package/src/handoffs.ts +299 -0
- package/src/healing.ts +93 -0
- package/src/host.ts +465 -0
- package/src/index.ts +165 -0
- package/src/isolated-worker.ts +177 -0
- package/src/isolated.ts +344 -0
- package/src/native.ts +478 -0
- package/src/orchestration/artifact-port.ts +75 -0
- package/src/orchestration/catalog.ts +102 -0
- package/src/orchestration/compile.ts +373 -0
- package/src/orchestration/dispatch.ts +383 -0
- package/src/orchestration/durable-jobs.ts +374 -0
- package/src/orchestration/effect-key.ts +35 -0
- package/src/orchestration/evidence-store.ts +233 -0
- package/src/orchestration/hash.ts +43 -0
- package/src/orchestration/host.ts +445 -0
- package/src/orchestration/index.ts +131 -0
- package/src/orchestration/policy.ts +116 -0
- package/src/orchestration/recovery.ts +271 -0
- package/src/orchestration/tracer.ts +177 -0
- package/src/plans.ts +105 -0
- package/src/presets.ts +90 -0
- package/src/process.ts +47 -0
- package/src/registry.ts +80 -0
- package/src/replay.ts +190 -0
- package/src/rpc.ts +598 -0
- package/src/session-migration.ts +401 -0
- package/src/sessions.ts +303 -0
- package/src/subagents.ts +210 -0
- package/src/tools.ts +52 -0
- package/src/ui.ts +137 -0
- package/src/verification.ts +133 -0
- 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
|
+
}
|