/** * @module @nhtio/adk/batteries/orchestration/executor * * The breadth-first executor that walks a plan graph to a terminal state and returns a * {@link RunProjection} derived entirely from the durable event log. * * Correctness rests on a strict commit protocol rather than on in-memory bookkeeping. Before a * node may be invoked its `node_entered` event is durably appended and awaited; once it settles, * `node_settled`, every `edge_taken`, and the fresh `frontier_snapshot` are committed together as * one atomic batch. A resume folds the same log, so the projection it produces can never disagree * with what a resumed run would observe — the projection is the log, not a shadow copy this * executor happens to keep. * * Concurrency is controlled by `PlanStore.claimRun`, which enforces that a plan has at most one * live run for a given digest. The optional lock factory is a weaker, best-effort guard; the * durable claim is the contract, and the executor claims before invoking any node. */ import type { PlanStore } from "./store"; import type { RunOptions, RunProjection } from "./types"; /** * Execute a plan against external input and return the projection folded from the durable event * log. External input is materialised as the entry frame's `node_settled` before any other node * runs, so it is addressable by `NodeRef` exactly like any other node output and is rebuilt from * events on resume. * * @remarks * **ORDER OF OPERATIONS, because it is observable and load-bearing.** Everything that can refuse * a request happens BEFORE `claimRun`: the plan is read and folded, the entry node is located, * and `RunOptions.input` is validated against its `DeclaredField[]`. Only then is the run claimed. * * That ordering is not an optimisation. `claimRun` is irreversible by design — a plan admits one * run EVER and the store exposes no release — so a check that ran after it would burn the plan's * only run on a request that never invoked a single tool, leaving it permanently * `run_already_claimed` with `clonePlan` the only recovery. A rejected request must cost the plan * nothing. * * So a caller can rely on this: **if this function throws on invalid input, the plan is still * runnable.** Fix the input and call again. * * The budget is the PLAN'S `bounds.maxSteps`, not a library constant — bounds are plan content, * digested and approved by the operator. Exhausting it settles the run `halted` with * `budget_exhausted{settled}`, never `process_death`: the executor knows why it stopped, and a * resume re-reports the same cause rather than looping, because the bound has not changed. * * @param store The plan store backing the run. * @param planId The id of the plan to execute. * @param options Per-run input and override dependencies. * @returns The {@link RunProjection} folded from the run's event log. * @throws If `RunOptions.input` violates the entry node's declared fields — before any claim. */ export declare function executePlan(store: PlanStore, planId: string, options: RunOptions): Promise;