@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,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
|
+
}
|