/** * v3 orchestrator — pure decision layer. * * Mirrors v0.2's `orchestrator.ts` pattern: a pure function maps the current * run state + DAG to a list of action descriptors. The runtime (`runtime.ts`) * owns every side effect — journal/STATE writes, ephemeral-worker dispatch via * `runNode`, humanGate card posting — and the per-bot/per-CLI/global * concurrency caps. Keeping the decision pure makes the critical-path * semantics testable without spawning workers or touching the filesystem. * * MVP scope: static DAG, fail-fast. No loops / decisions / dynamic expand * (those are deferred — design Q3/§7). Gate set is frozen at authoring time; * the orchestrator never invents or skips a gate (design Q10). */ import { type V3Dag } from './dag.js'; import type { V3LoopRef, V3RunFailureReason } from './event-contract.js'; export type V3NodeStatus = 'pending' | 'gateWaiting' | 'running' | 'done' | 'skipped' | 'cancelled' | 'blocked' | 'superseded' | 'failed'; export interface V3NodeState { status: V3NodeStatus; /** True once an approved humanGate cleared this node — so after approval the * next tick dispatches work instead of re-dispatching the gate. A rejected * gate transitions the node straight to `failed` (set by the runtime), so * this flag only ever records the approved case. */ gateCleared?: boolean; /** Host-only: the frozen input approved by this gate. It must match the * prepared sidecar before the runtime may publish hostEffectIntent. */ approvedHostInput?: { attemptId: string; approvalDigest: string; inputHash: string; }; /** The current live runtime instance of this DEFINITION node (`A#002`). * Set on dispatch; cleared when a revisit supersedes it (the node then * re-dispatches a fresh instance). Absent on the pre-instance-layer path * (plain nodeId-keyed events) and loop body expansions. */ effectiveInstanceId?: string; } /** nodeId → state. A node absent from the map is treated as `pending`. */ export type V3RunState = Map; /** * Per-loop composite state, folded from the loop lifecycle events. A loop * absent from the map has not started. The loop's coarse status (running / * blocked / done) lives in the regular node-state map under the loop's id; * this struct carries what that one enum can't: where the iteration cursor is * and what the last decision said. */ export interface V3LoopState { /** Current iteration, 1-based; 0 between loopStarted and the first * loopIterationStarted. */ iteration: number; /** True once the CURRENT iteration's decision event is recorded (reset by * the next loopIterationStarted). */ decided: boolean; /** The latest decision — drives what the orchestrator does next. */ lastDecision?: 'exit' | 'continue' | 'exhausted'; /** Extra iterations granted (each loopIterationGranted adds one); the * effective budget is maxIterations + granted. */ granted: number; /** An appended-but-unconsumed grant (cleared by the next * loopIterationStarted) — the idempotency key for "already granted". */ pendingGrant: boolean; } /** loopId → loop state. */ export type V3LoopRunState = Map; export interface V3EdgeState { active: boolean; sourceAttemptId: string; } /** `${from}->${to}` → conditional edge state. */ export type V3EdgeRunState = Map; export interface V3OmittedInput { from: string; reason: 'edgeInactive' | 'sourceSkipped' | 'sourceCancelled' | 'earlyRelease'; } export type V3Action = /** Read one source result.json once and append edgeResolved. */ { kind: 'resolveEdge'; from: string; to: string; } /** Mark a node skipped because its triggerRule cannot be satisfied. */ | { kind: 'skipNode'; nodeId: string; detail?: string; } /** Abort an early-release loser whose remaining products are no longer used. */ | { kind: 'cancelNode'; nodeId: string; byNodeId: string; detail?: string; } /** Post the humanGate approval card + persist a `waits/.json` (Q10). */ | { kind: 'dispatchGate'; nodeId: string; instanceId?: string; } /** Spawn an ephemeral worker via `runNode` for this node's goal. `loop` is * set for body-instance dispatches (the runtime synthesizes the instance * node from the loop's body definition). `instanceId` is set when this is a * cross-node-revisit RE-DISPATCH (`A#002`): the prior instance was * superseded, so decideNext computes the next instance number deterministically * from `state.instances` (constraint 4 — the action carries it, the runtime * does not guess). Absent on a first dispatch / loop body (those keep the * pre-instance-layer path until the runtime brick threads instances through). */ | { kind: 'dispatchWork'; nodeId: string; instanceId?: string; loop?: V3LoopRef; omitted?: V3OmittedInput[]; } /** Outer deps of a loop are done → append loopStarted. */ | { kind: 'startLoop'; loopId: string; } /** Begin iteration N (first, after a continue-decision, or after a grant). */ | { kind: 'startLoopIteration'; loopId: string; iteration: number; } /** Current iteration's body is fully done and undecided → the runtime reads * the exit node's result.json, evaluates exit.when, appends the decision. */ | { kind: 'evaluateLoopIteration'; loopId: string; iteration: number; } /** Decision was 'exit' → seal the loop with a nodeSucceeded on the LOOP id * carrying the output projection's manifest (downstream inputs/deps then * treat the loop like any done node — zero special-casing). */ | { kind: 'completeLoop'; loopId: string; iteration: number; } /** Terminal: every node done; the run's product is the sink set. */ | { kind: 'completeRunSucceeded'; } /** Terminal (fail-fast): a node failed, so the run cannot proceed. */ | { kind: 'completeRunFailed'; failedNodeId?: string; reason?: V3RunFailureReason; detail?: string; } /** Terminal-for-now: a node is blocked (contract failure, recoverable). * Halts dispatch like failed, but the run can resume via a retry event. */ | { kind: 'completeRunBlocked'; blockedNodeId: string; }; /** * Pure decision: given the current `state`, return every action that can be * taken *now*. The runtime applies concurrency caps by acting on a prefix of * the returned dispatch actions and re-invoking on the next tick — this * function intentionally returns ALL ready dispatches (it does not throttle). * * Ordering follows topological order so callers see deps-ready nodes first. * Fail-fast: the moment any node is `failed`, the only action is * `completeRunFailed` (the runtime's attempt-quiescence barrier tears down and * proves close for every in-flight peer before publishing the run terminal). * When no dispatch is possible and nothing is pending, the run is complete. */ export declare function decideNext(dag: V3Dag, state: V3RunState, loops?: V3LoopRunState, edges?: V3EdgeRunState, instances?: V3RunState): V3Action[]; /** Sink nodes — no other node depends on them. Their products are the run's * output. Pure helper for the runtime's success path. */ export declare function findSinks(dag: V3Dag): string[]; //# sourceMappingURL=orchestrator.d.ts.map