@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,445 @@
1
+ /**
2
+ * The OrchestrationHost: the lifecycle owner for a compiled {@link RunPlan}. It does not schedule
3
+ * work itself - each top-level DAG layer is handed to a kernel {@link WorkflowRunner}, exactly as
4
+ * {@link compileRunPlan} would fold them into one `sequence`. Driving layer by layer gives a safe
5
+ * pause boundary (in-flight nodes finish; no new layer starts) and a cancellation seam (the layer's
6
+ * runner and any bounded subagent share the run's abort signal) without a second engine.
7
+ *
8
+ * Every transition is guarded by an explicit state machine: an illegal transition returns a stable
9
+ * code and mutates nothing. A terminal run is frozen - a late completion from an aborted layer
10
+ * cannot change its status, counters, or digest. Terminal results expose status, completed node ids,
11
+ * an order-independent evidence digest, artifact refs, counters, and a failure code only: never a
12
+ * step output, prompt, or payload.
13
+ */
14
+
15
+ import type { CapabilityDescriptor } from "@nifrajs/agent/registry"
16
+ import type {
17
+ ArtifactPort,
18
+ ArtifactRef,
19
+ NodeEffectKey,
20
+ RunNode,
21
+ RunNodeKind,
22
+ RunPlan,
23
+ } from "@nifrajs/agent-protocol"
24
+ import { parseRunPlan, RUN_PLAN_VERSION } from "@nifrajs/agent-protocol"
25
+ import type { WorkflowStep } from "../workflows.ts"
26
+ import { type WorkflowEvent, WorkflowRunner } from "../workflows.ts"
27
+ import { noopArtifactPort } from "./artifact-port.ts"
28
+ import type { StepCatalog } from "./catalog.ts"
29
+ import { compileRunPlanLayers, digestRunPlan, type OrchestrationLimits } from "./compile.ts"
30
+ import { type EvidenceCounters, type EvidenceStore, MemoryEvidenceStore } from "./evidence-store.ts"
31
+ import { admitCapability, ChildVectorTracker, type HostPolicy } from "./policy.ts"
32
+
33
+ export type RunState = "submitted" | "running" | "paused" | "succeeded" | "failed" | "cancelled"
34
+
35
+ const TERMINAL: ReadonlySet<RunState> = new Set(["succeeded", "failed", "cancelled"])
36
+
37
+ function isTerminal(state: RunState): boolean {
38
+ return TERMINAL.has(state)
39
+ }
40
+
41
+ /** Thrown on an illegal lifecycle transition or an unknown run. `code` is stable. */
42
+ export class OrchestrationStateError extends Error {
43
+ constructor(
44
+ reason: string,
45
+ readonly code: string,
46
+ ) {
47
+ super(`orchestration host: ${reason}`)
48
+ this.name = "OrchestrationStateError"
49
+ }
50
+ }
51
+
52
+ /** Content-free progress view of a run. */
53
+ export interface RunStatus {
54
+ readonly runId: string
55
+ readonly state: RunState
56
+ readonly planDigest: string
57
+ readonly layerIndex: number
58
+ readonly layerCount: number
59
+ readonly counters: EvidenceCounters
60
+ }
61
+
62
+ /** Deterministic terminal result. Outcome identity only - no step output, prompt, or payload. */
63
+ export interface RunResult {
64
+ readonly runId: string
65
+ readonly status: "succeeded" | "failed" | "cancelled"
66
+ readonly planDigest: string
67
+ readonly completedNodeIds: readonly string[]
68
+ readonly evidenceDigest: string
69
+ readonly artifacts: readonly ArtifactRef[]
70
+ readonly counters: EvidenceCounters
71
+ readonly failureCode?: string
72
+ }
73
+
74
+ export interface SubmitOptions {
75
+ /** Caller-supplied for reproducibility; defaults to a per-host monotonic id (no clock, no random). */
76
+ readonly runId?: string
77
+ /** Caller-owned payload sink. Defaults to hash-and-discard. */
78
+ readonly artifactPort?: ArtifactPort
79
+ /** Evidence store for this run. Defaults to a bounded {@link MemoryEvidenceStore}. */
80
+ readonly store?: EvidenceStore
81
+ }
82
+
83
+ export interface OrchestrationHostOptions {
84
+ readonly catalog: StepCatalog
85
+ readonly limits?: OrchestrationLimits
86
+ /** Kernel step ceiling per layer. Generous by default so a full 256-node layer runs. */
87
+ readonly maxSteps?: number
88
+ readonly maxDepth?: number
89
+ /**
90
+ * Authoritative admission policy. When set, {@link OrchestrationHost.admit} refuses any capability
91
+ * descriptor that asks to run outside the policy's kinds, capability allowlist, isolation floor, or
92
+ * approval requirement. A descriptor, plan, model, or extension cannot override it.
93
+ */
94
+ readonly policy?: HostPolicy
95
+ }
96
+
97
+ const DEFAULT_MAX_STEPS = 100_000
98
+ const DEFAULT_MAX_DEPTH = 64
99
+
100
+ interface RunRecord {
101
+ readonly runId: string
102
+ readonly planDigest: string
103
+ readonly layers: readonly WorkflowStep[]
104
+ readonly store: EvidenceStore
105
+ readonly controller: AbortController
106
+ readonly kindById: ReadonlyMap<string, RunNodeKind>
107
+ readonly idempotentById: ReadonlyMap<string, boolean>
108
+ readonly nodeIds: ReadonlySet<string>
109
+ readonly effectKeys: Map<string, NodeEffectKey>
110
+ readonly artifacts: Map<string, ArtifactRef[]>
111
+ state: RunState
112
+ layerIndex: number
113
+ seq: number
114
+ cancelRequested: boolean
115
+ failureCode: string | undefined
116
+ gate: { readonly promise: Promise<void>; readonly resolve: () => void } | undefined
117
+ done: Promise<void>
118
+ finish: () => void
119
+ }
120
+
121
+ /** Owns run lifecycle and evidence projection over the kernel WorkflowRunner. */
122
+ export class OrchestrationHost {
123
+ private readonly catalog: StepCatalog
124
+ private readonly limits: OrchestrationLimits | undefined
125
+ private readonly maxSteps: number
126
+ private readonly maxDepth: number
127
+ private readonly policy: HostPolicy | undefined
128
+ private readonly vectors = new ChildVectorTracker()
129
+ private readonly runs = new Map<string, RunRecord>()
130
+ private counter = 0
131
+
132
+ constructor(options: OrchestrationHostOptions) {
133
+ this.catalog = options.catalog
134
+ this.limits = options.limits
135
+ this.maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS
136
+ this.maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH
137
+ this.policy = options.policy
138
+ }
139
+
140
+ /**
141
+ * Admit a capability descriptor against the host policy before it can be invoked. Throws
142
+ * {@link OrchestrationStateError} (`E_POLICY_REJECTED`) carrying the admission reason when refused.
143
+ * With no policy configured this is a passthrough - the host admits nothing it is not told to gate.
144
+ */
145
+ admit(descriptor: CapabilityDescriptor): void {
146
+ if (this.policy === undefined) return
147
+ const admission = admitCapability(this.policy, descriptor)
148
+ if (!admission.ok)
149
+ throw new OrchestrationStateError(
150
+ `capability refused by host policy (${admission.code})`,
151
+ "E_POLICY_REJECTED",
152
+ )
153
+ }
154
+
155
+ /**
156
+ * Allocate the next monotonic child vector for a live run, addressing a decision boundary the run
157
+ * pauses on. Strictly increasing per run, so a decision carrying any other vector is provably stale.
158
+ */
159
+ openBoundaryVector(runId: string): number {
160
+ const run = this.require(runId)
161
+ if (isTerminal(run.state))
162
+ throw new OrchestrationStateError(
163
+ `run '${runId}' is terminal; no boundary opens`,
164
+ "E_NOT_TERMINAL",
165
+ )
166
+ return this.vectors.open(runId)
167
+ }
168
+
169
+ /**
170
+ * Parse, compile, and register a run in `submitted`. All structural ceilings and capability
171
+ * authority are enforced here (a `CompileError` with a stable code), so an over-limit or
172
+ * unauthorized plan never reaches `running`.
173
+ */
174
+ async submit(source: RunPlan | unknown, options: SubmitOptions = {}): Promise<string> {
175
+ const plan = parseRunPlan(source)
176
+ const planDigest = await digestRunPlan(plan)
177
+ const runId = options.runId ?? `run-${++this.counter}`
178
+ if (this.runs.has(runId))
179
+ throw new OrchestrationStateError(`run '${runId}' already exists`, "E_DUPLICATE_RUN")
180
+
181
+ const artifactPort = options.artifactPort ?? noopArtifactPort()
182
+ const effectKeys = new Map<string, NodeEffectKey>()
183
+ const artifacts = new Map<string, ArtifactRef[]>()
184
+ const layers = compileRunPlanLayers(plan, {
185
+ catalog: this.catalog,
186
+ planDigest,
187
+ artifactPort,
188
+ ...(this.limits !== undefined ? { limits: this.limits } : {}),
189
+ onNodeEffect: (nodeId, key) => effectKeys.set(nodeId, key),
190
+ collectArtifact: (nodeId, ref) => {
191
+ const list = artifacts.get(nodeId)
192
+ if (list === undefined) artifacts.set(nodeId, [ref])
193
+ else list.push(ref)
194
+ },
195
+ })
196
+
197
+ const { kindById, idempotentById, nodeIds } = this.indexPlan(plan)
198
+ let finish!: () => void
199
+ const done = new Promise<void>((resolve) => {
200
+ finish = resolve
201
+ })
202
+ this.runs.set(runId, {
203
+ runId,
204
+ planDigest,
205
+ layers,
206
+ store: options.store ?? new MemoryEvidenceStore(),
207
+ controller: new AbortController(),
208
+ kindById,
209
+ idempotentById,
210
+ nodeIds,
211
+ effectKeys,
212
+ artifacts,
213
+ state: "submitted",
214
+ layerIndex: 0,
215
+ seq: 0,
216
+ cancelRequested: false,
217
+ failureCode: undefined,
218
+ gate: undefined,
219
+ done,
220
+ finish,
221
+ })
222
+ return runId
223
+ }
224
+
225
+ /** Content-free progress view. Throws `E_NOT_FOUND` for an unknown run. */
226
+ inspect(runId: string): RunStatus {
227
+ const run = this.require(runId)
228
+ return {
229
+ runId,
230
+ state: run.state,
231
+ planDigest: run.planDigest,
232
+ layerIndex: run.layerIndex,
233
+ layerCount: run.layers.length,
234
+ counters: run.store.counters(),
235
+ }
236
+ }
237
+
238
+ /** `submitted` -> `running`. Begins the driver. */
239
+ start(runId: string): void {
240
+ const run = this.require(runId)
241
+ this.transition(run, "start", ["submitted"], "running")
242
+ void this.drive(run)
243
+ }
244
+
245
+ /** `running` -> `paused`. Takes effect at the next layer boundary; in-flight nodes finish. */
246
+ pause(runId: string): void {
247
+ const run = this.require(runId)
248
+ this.transition(run, "pause", ["running"], "paused")
249
+ let resolve!: () => void
250
+ const promise = new Promise<void>((r) => {
251
+ resolve = r
252
+ })
253
+ run.gate = { promise, resolve }
254
+ }
255
+
256
+ /** `paused` -> `running`. Releases the layer boundary. */
257
+ resume(runId: string): void {
258
+ const run = this.require(runId)
259
+ this.transition(run, "resume", ["paused"], "running")
260
+ run.gate?.resolve()
261
+ run.gate = undefined
262
+ }
263
+
264
+ /**
265
+ * Cancel from any non-terminal state. Aborts the in-flight layer and any bounded subagent, frees a
266
+ * paused gate, and freezes the run in `cancelled`. A late completion cannot revive it.
267
+ */
268
+ cancel(runId: string): void {
269
+ const run = this.require(runId)
270
+ if (isTerminal(run.state))
271
+ throw new OrchestrationStateError(
272
+ `run '${runId}' cannot cancel from terminal '${run.state}'`,
273
+ "E_ILLEGAL_TRANSITION",
274
+ )
275
+ run.cancelRequested = true
276
+ run.state = "cancelled"
277
+ run.failureCode = "E_CANCELLED"
278
+ run.controller.abort()
279
+ run.gate?.resolve()
280
+ run.gate = undefined
281
+ this.vectors.release(run.runId)
282
+ run.finish()
283
+ }
284
+
285
+ /** Terminal result. Throws `E_NOT_TERMINAL` while the run is still live. */
286
+ async result(runId: string): Promise<RunResult> {
287
+ const run = this.require(runId)
288
+ if (!isTerminal(run.state))
289
+ throw new OrchestrationStateError(
290
+ `run '${runId}' is '${run.state}', not terminal`,
291
+ "E_NOT_TERMINAL",
292
+ )
293
+ return this.buildResult(run)
294
+ }
295
+
296
+ /** Await the run reaching a terminal state, then return its result. */
297
+ async settled(runId: string): Promise<RunResult> {
298
+ const run = this.require(runId)
299
+ await run.done
300
+ return this.buildResult(run)
301
+ }
302
+
303
+ private require(runId: string): RunRecord {
304
+ const run = this.runs.get(runId)
305
+ if (run === undefined)
306
+ throw new OrchestrationStateError(`unknown run '${runId}'`, "E_NOT_FOUND")
307
+ return run
308
+ }
309
+
310
+ private transition(run: RunRecord, op: string, from: readonly RunState[], to: RunState): void {
311
+ if (!from.includes(run.state))
312
+ throw new OrchestrationStateError(
313
+ `cannot ${op} run '${run.runId}' from '${run.state}'`,
314
+ "E_ILLEGAL_TRANSITION",
315
+ )
316
+ run.state = to
317
+ }
318
+
319
+ private async buildResult(run: RunRecord): Promise<RunResult> {
320
+ const status = run.state as "succeeded" | "failed" | "cancelled"
321
+ return {
322
+ runId: run.runId,
323
+ status,
324
+ planDigest: run.planDigest,
325
+ completedNodeIds: run.store.completedNodeIds(),
326
+ evidenceDigest: await run.store.digest(),
327
+ artifacts: run.store.artifacts(),
328
+ counters: run.store.counters(),
329
+ ...(run.failureCode !== undefined ? { failureCode: run.failureCode } : {}),
330
+ }
331
+ }
332
+
333
+ private async drive(run: RunRecord): Promise<void> {
334
+ try {
335
+ for (; run.layerIndex < run.layers.length; run.layerIndex++) {
336
+ while (run.state === "paused") await run.gate?.promise
337
+ if (isTerminal(run.state)) return // cancelled while paused
338
+ const layer = run.layers[run.layerIndex] as WorkflowStep
339
+ const result = await new WorkflowRunner({
340
+ signal: run.controller.signal,
341
+ maxSteps: this.maxSteps,
342
+ maxDepth: this.maxDepth,
343
+ onEvent: (event) => this.project(run, event),
344
+ }).run(layer)
345
+ if (isTerminal(run.state)) return // cancelled mid-layer: ignore the late result
346
+ if (!result.ok) {
347
+ run.state = "failed"
348
+ run.failureCode = run.failureCode ?? "E_STEP_FAILED"
349
+ return
350
+ }
351
+ }
352
+ if (!isTerminal(run.state)) run.state = "succeeded"
353
+ } finally {
354
+ if (!isTerminal(run.state)) run.state = "failed"
355
+ this.vectors.release(run.runId)
356
+ run.finish()
357
+ }
358
+ }
359
+
360
+ /** Project one kernel event into a content-free evidence record. Frozen once the run is terminal. */
361
+ private async project(run: RunRecord, event: WorkflowEvent): Promise<void> {
362
+ if (isTerminal(run.state)) return // a late completion cannot mutate a terminal run
363
+ if (!("id" in event) || !run.nodeIds.has(event.id)) return
364
+ const base = {
365
+ version: RUN_PLAN_VERSION,
366
+ runId: run.runId,
367
+ planDigest: run.planDigest,
368
+ nodeId: event.id,
369
+ seq: run.seq++,
370
+ idempotent: run.idempotentById.get(event.id) ?? false,
371
+ } as const
372
+ const key = run.effectKeys.get(event.id)
373
+ const effect = key !== undefined ? { effectKey: key.digest } : {}
374
+
375
+ if (event.type === "step.started") {
376
+ await run.store.append({ ...base, status: "started" })
377
+ return
378
+ }
379
+ if (event.type === "step.completed") {
380
+ if (isGateRejection(run.kindById.get(event.id), event.output)) {
381
+ run.failureCode = "E_GATE_REJECTED"
382
+ await run.store.append({ ...base, status: "failed", errorCode: "GATE_REJECTED", ...effect })
383
+ return
384
+ }
385
+ const refs = run.artifacts.get(event.id)
386
+ await run.store.append({
387
+ ...base,
388
+ status: "completed",
389
+ ...effect,
390
+ ...(refs !== undefined && refs.length > 0 ? { artifacts: refs } : {}),
391
+ })
392
+ return
393
+ }
394
+ if (event.type === "step.failed") {
395
+ run.failureCode = "E_STEP_FAILED"
396
+ await run.store.append({ ...base, status: "failed", errorCode: "STEP_FAILED", ...effect })
397
+ }
398
+ }
399
+
400
+ /** Walk the plan tree once: node kinds, idempotency flags, and the full id set. */
401
+ private indexPlan(plan: RunPlan): {
402
+ readonly kindById: ReadonlyMap<string, RunNodeKind>
403
+ readonly idempotentById: ReadonlyMap<string, boolean>
404
+ readonly nodeIds: ReadonlySet<string>
405
+ } {
406
+ const kindById = new Map<string, RunNodeKind>()
407
+ const idempotentById = new Map<string, boolean>()
408
+ const nodeIds = new Set<string>()
409
+ const walk = (node: RunNode): void => {
410
+ nodeIds.add(node.id)
411
+ kindById.set(node.id, node.kind)
412
+ const step = "step" in node ? this.catalog.get(node.step) : undefined
413
+ idempotentById.set(node.id, step?.selectEffect !== undefined)
414
+ switch (node.kind) {
415
+ case "sequence":
416
+ case "parallel":
417
+ for (const child of node.children) walk(child)
418
+ break
419
+ case "retry":
420
+ walk(node.child)
421
+ break
422
+ case "branch":
423
+ walk(node.then)
424
+ if (node.otherwise !== undefined) walk(node.otherwise)
425
+ break
426
+ }
427
+ }
428
+ for (const node of plan.nodes) walk(node)
429
+ return { kindById, idempotentById, nodeIds }
430
+ }
431
+ }
432
+
433
+ /** A verify/approve node whose output is not a pass. Other kinds never gate. */
434
+ function isGateRejection(kind: RunNodeKind | undefined, output: unknown): boolean {
435
+ if (kind === "verify") {
436
+ if (output === true) return false
437
+ return !(
438
+ typeof output === "object" &&
439
+ output !== null &&
440
+ (output as { ok?: unknown }).ok === true
441
+ )
442
+ }
443
+ if (kind === "approve") return output !== true
444
+ return false
445
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `@nifrajs/coding-agent/orchestration` - declarative RunPlans compiled onto the existing
3
+ * WorkflowRunner, with content-free evidence, a caller-owned artifact seam, and content-free
4
+ * idempotency keys. No second engine; no payload sink. See the P0 addendum for the design lock.
5
+ */
6
+
7
+ export type {
8
+ ArtifactContext,
9
+ ArtifactPort,
10
+ ArtifactRef,
11
+ NodeEffectKey,
12
+ RunBranchNode,
13
+ RunEvidence,
14
+ RunEvidenceStatus,
15
+ RunLeafKind,
16
+ RunLeafNode,
17
+ RunNode,
18
+ RunNodeKind,
19
+ RunParallelNode,
20
+ RunPlan,
21
+ RunRetryNode,
22
+ RunRetryPolicy,
23
+ RunSequenceNode,
24
+ RunStructuralKind,
25
+ } from "@nifrajs/agent-protocol"
26
+ export {
27
+ assertEvidenceSize,
28
+ EVIDENCE_MAX_BYTES,
29
+ FORBIDDEN_CONTENT_KEYS,
30
+ parseArtifactRef,
31
+ parseRunEvidence,
32
+ parseRunPlan,
33
+ RUN_PLAN_VERSION,
34
+ RunContractError,
35
+ } from "@nifrajs/agent-protocol"
36
+
37
+ export {
38
+ type MemoryArtifactPort,
39
+ memoryArtifactPort,
40
+ noopArtifactPort,
41
+ } from "./artifact-port.ts"
42
+ export {
43
+ CatalogError,
44
+ type CatalogStep,
45
+ createStepCatalog,
46
+ mergeStepCatalogs,
47
+ type StepCatalog,
48
+ type StepEffectContext,
49
+ type StepRunContext,
50
+ stepVersion,
51
+ } from "./catalog.ts"
52
+ export {
53
+ CompileError,
54
+ type CompileOptions,
55
+ compileRunPlan,
56
+ compileRunPlanLayers,
57
+ digestRunPlan,
58
+ type OrchestrationLimits,
59
+ } from "./compile.ts"
60
+ export {
61
+ createRunDispatch,
62
+ DISPATCH_JOB_NAME,
63
+ type DispatchBoundary,
64
+ type DispatchInspection,
65
+ type DispatchResultCode,
66
+ type DispatchState,
67
+ type DispatchWriteResult,
68
+ type DurableDispatchAdapter,
69
+ deriveRunIdempotencyKey,
70
+ type InjectedClock,
71
+ MAX_DISPATCH_ATTEMPTS,
72
+ MAX_DISPATCH_STRING,
73
+ parseRunCheckpoint,
74
+ parseRunDispatch,
75
+ parseRunDispatchEvidence,
76
+ parseRunLease,
77
+ RUN_DISPATCH_VERSION,
78
+ type RunCheckpoint,
79
+ type RunDispatch,
80
+ type RunDispatchEvidence,
81
+ type RunDispatchStore,
82
+ type RunLease,
83
+ systemClock,
84
+ } from "./dispatch.ts"
85
+ export {
86
+ createDurableJobsStore,
87
+ createMemoryRunDispatchStore,
88
+ type DurableJobsStoreOptions,
89
+ } from "./durable-jobs.ts"
90
+ export { deriveNodeEffectKey, type EffectKeyMaterial } from "./effect-key.ts"
91
+ export {
92
+ type EvidenceCounters,
93
+ type EvidenceStore,
94
+ FileEvidenceStore,
95
+ type FileEvidenceStoreOptions,
96
+ MemoryEvidenceStore,
97
+ type MemoryEvidenceStoreOptions,
98
+ } from "./evidence-store.ts"
99
+ export { canonicalJson, sha256Hex, sha256HexOf } from "./hash.ts"
100
+ export {
101
+ OrchestrationHost,
102
+ type OrchestrationHostOptions,
103
+ OrchestrationStateError,
104
+ type RunResult,
105
+ type RunState,
106
+ type RunStatus,
107
+ type SubmitOptions,
108
+ } from "./host.ts"
109
+ export {
110
+ type Admission,
111
+ type AdmissionRejection,
112
+ admitCapability,
113
+ assertAdmitted,
114
+ ChildVectorTracker,
115
+ type HostPolicy,
116
+ PolicyError,
117
+ } from "./policy.ts"
118
+ export {
119
+ type CommittedEffect,
120
+ EffectRejectedError,
121
+ type IdempotencyProofStore,
122
+ MemoryIdempotencyProofStore,
123
+ RecoveryCrashError,
124
+ type RecoveryMachineOptions,
125
+ type RecoveryProcessResult,
126
+ type RunEffect,
127
+ type RunEffectContext,
128
+ RunRecoveryMachine,
129
+ TestClock,
130
+ } from "./recovery.ts"
131
+ export { type RunTraceOptions, type RunTraceResult, runTrace } from "./tracer.ts"
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Host policy admission for the orchestration host (REG-06) and the monotonic child-vector allocator
3
+ * (REG-07).
4
+ *
5
+ * A {@link HostPolicy} is the authority: it declares which capability kinds may run, an allowlist of
6
+ * required capabilities, an isolation floor, and whether every capability must be approved. A
7
+ * {@link CapabilityDescriptor} is only a request - {@link admitCapability} lets the descriptor tighten
8
+ * nothing it did not already promise and, crucially, cannot loosen the policy. A descriptor asking for
9
+ * weaker isolation than the floor, declaring a capability outside the allowlist, or offering `none`
10
+ * approval where the host requires approval is refused with a stable code. Descriptors, plans, models,
11
+ * extensions, and clients therefore cannot override host policy.
12
+ *
13
+ * The {@link ChildVectorTracker} hands each newly opened decision boundary a strictly increasing
14
+ * per-run vector. A decision must later carry the exact vector its boundary opened with, so a replayed
15
+ * or superseded decision cannot resume work. The tracker never allocates a non-advancing vector.
16
+ */
17
+
18
+ import type {
19
+ ApprovalClass,
20
+ CapabilityDescriptor,
21
+ CapabilityKind,
22
+ IsolationClass,
23
+ } from "@nifrajs/agent/registry"
24
+
25
+ /** The authoritative admission policy. Empty allowlists deny everything of that kind. */
26
+ export interface HostPolicy {
27
+ /** Capability kinds the host will admit. A descriptor of any other kind is refused. */
28
+ readonly allowedKinds: readonly CapabilityKind[]
29
+ /** Required-capability allowlist. Every `requiredCapabilities` entry must be present. */
30
+ readonly allowedCapabilities: readonly string[]
31
+ /** Isolation floor. A descriptor offering weaker isolation than this is refused. */
32
+ readonly minIsolation: IsolationClass
33
+ /** When true, a descriptor whose approval class is `none` is refused as a downgrade. */
34
+ readonly requireApproval: boolean
35
+ }
36
+
37
+ /** Stable, content-free reasons the host refuses to admit a capability. */
38
+ export type AdmissionRejection =
39
+ | "kind_not_allowed"
40
+ | "capability_not_allowed"
41
+ | "isolation_too_weak"
42
+ | "approval_downgrade"
43
+
44
+ export type Admission =
45
+ | { readonly ok: true }
46
+ | { readonly ok: false; readonly code: AdmissionRejection }
47
+
48
+ /** Thrown when the host refuses admission and the caller wants an exception rather than a result. */
49
+ export class PolicyError extends Error {
50
+ readonly code: AdmissionRejection
51
+
52
+ constructor(code: AdmissionRejection) {
53
+ super(`host policy: ${code}`)
54
+ this.code = code
55
+ this.name = "PolicyError"
56
+ }
57
+ }
58
+
59
+ // Isolation is a total order: a higher rank is stronger and satisfies any lower floor.
60
+ const ISOLATION_RANK: Readonly<Record<IsolationClass, number>> = {
61
+ inherit: 0,
62
+ process: 1,
63
+ sandbox: 2,
64
+ }
65
+
66
+ function approvalIsNone(approval: ApprovalClass): boolean {
67
+ return approval.kind === "none"
68
+ }
69
+
70
+ /**
71
+ * Admit a capability descriptor against the host policy. The policy is the floor; the descriptor can
72
+ * only meet or exceed it. Returns the first failing reason, or `{ ok: true }` when every gate passes.
73
+ */
74
+ export function admitCapability(policy: HostPolicy, descriptor: CapabilityDescriptor): Admission {
75
+ if (!policy.allowedKinds.includes(descriptor.kind)) return { ok: false, code: "kind_not_allowed" }
76
+ const allowed = new Set(policy.allowedCapabilities)
77
+ for (const capability of descriptor.requiredCapabilities)
78
+ if (!allowed.has(capability)) return { ok: false, code: "capability_not_allowed" }
79
+ if (ISOLATION_RANK[descriptor.isolation] < ISOLATION_RANK[policy.minIsolation])
80
+ return { ok: false, code: "isolation_too_weak" }
81
+ if (policy.requireApproval && approvalIsNone(descriptor.approval))
82
+ return { ok: false, code: "approval_downgrade" }
83
+ return { ok: true }
84
+ }
85
+
86
+ /** Admit or throw {@link PolicyError}. Use at the host boundary where a refusal must stop the run. */
87
+ export function assertAdmitted(policy: HostPolicy, descriptor: CapabilityDescriptor): void {
88
+ const admission = admitCapability(policy, descriptor)
89
+ if (!admission.ok) throw new PolicyError(admission.code)
90
+ }
91
+
92
+ /**
93
+ * Per-run monotonic child-vector allocator. `open` returns the next strictly increasing vector for a
94
+ * run; `last` reports the high-water mark (`-1` before the first boundary). A vector is never reused,
95
+ * so a decision carrying anything other than its boundary's opened vector is provably stale.
96
+ */
97
+ export class ChildVectorTracker {
98
+ private readonly highWater = new Map<string, number>()
99
+
100
+ /** Allocate the next vector for a run. Strictly greater than every previously allocated vector. */
101
+ open(runId: string): number {
102
+ const next = (this.highWater.get(runId) ?? -1) + 1
103
+ this.highWater.set(runId, next)
104
+ return next
105
+ }
106
+
107
+ /** The last allocated vector for a run, or `-1` when none has opened. */
108
+ last(runId: string): number {
109
+ return this.highWater.get(runId) ?? -1
110
+ }
111
+
112
+ /** Forget a run's vectors once it is terminal. Idempotent. */
113
+ release(runId: string): void {
114
+ this.highWater.delete(runId)
115
+ }
116
+ }