/** * Graph Execution Engine v2 — Loop-Group Executor * * Version: 2.0 * Date: 2026-07-24 * * The orchestration layer that ties the Phase 2 primitives into a single * bounded-cycle execution decision for a convergence node inside a loop group. * Each primitive owns one concern and stays import-only here: * * - traversal counting → `engine-state.ts` (`incrementLoopTraversal`, * `isLoopExhausted`, `loopGroups`) * - convergence tracking → `engine-state.ts` (`recordConvergenceOutput`, * `resetConvergenceTracker`) * - revise-driven re-dispatch + stuck detection + hard-cap enforcement → * `signal-propagation.ts` (`propagateRevise` — single enforcement point) * - worst-signal forward propagation → `signal-propagation.ts` * (`propagateEscalate`) * - upstream cancellation → `cascade-canceller.ts` (`cancelPendingUpstreams`) * - join arbitration → `join-evaluator.ts` (`evaluateJoin`) * * {@link executeLoopStep} is the single entry point: given a loop-group member * node's terminating signal, it decides which of the three soft early-exits in * `.rolebox/design/failure-resilience.md` §4.3 applies and executes it: * * | Condition | Trigger | Outcome | * |---|---|---| * | **converged** | Convergence node signals `answer` | Loop exits naturally — only forward edges run (engine's `answer` data flow); the stale tracker is reset and no traversal is consumed. | * | **max_traversals exhausted** | A `revise_needed` arrives when the loop group's `max_traversals` hard cap is reached | `completed → done` with the structured payload `{ reason: "max_traversals exhausted", unresolved, traversals }` (§1.6). | * | **stuck** | Consecutive convergence outputs are identical for `>= CONSECUTIVE_STALE_THRESHOLD` (= 2) traversals | `completed → done` with reason `"stuck"` before any further traversal is consumed (§4.3). | * * Both exhaustion exits retire the node to terminal `done` — **not** * `escalate`; `signal-propagation.ts` is the single enforcement point. A * {@link LoopStepReport.escalated} entry therefore means "retired by this * step", not "the node's status is escalate"; read the node's actual status * when the distinction matters (Y17). * * Otherwise (`revise_needed` with traversals remaining) the executor delegates * to {@link propagateRevise}, which now owns stuck detection and hard-cap * enforcement as the single enforcement point: it increments the traversal * counter and re-enters the offending upstream nodes (bounded-cycle * re-dispatch). An * `escalate` from a loop-group member delegates to {@link propagateEscalate} * and, at any convergence node whose join has now failed, retires the * still-pending upstream nodes via the cascade canceller (§3.3). * * Design references: * - `.rolebox/design/failure-resilience.md` §1.6, §2.1, §3.3, §4 * - `.rolebox/design/graph-model.md` §4 (bounded-cycle loop model), §5.1 (lattice) * - `.rolebox/design/orchestration-patterns.md` §1.6 * - `src/loop/constants.ts:66` — `CONSECUTIVE_STALE_THRESHOLD = 2` */ import type { EngineState, NodeRuntimeState } from "../../types.engine-v2.ts"; import type { SignalType } from "./signal-bridge.ts"; import type { SignalPropagationReport } from "./signal-propagation.ts"; import { type CancelDispatchPort } from "./cascade-canceller.ts"; import { fingerprintPayload, recordConvergenceOutput, resetConvergenceTracker } from "./engine-state.ts"; /** * The resolved early-exit / continuation branch of one bounded-cycle step. * * - `converged` — the node signalled `answer`; the loop ends on the happy path * (forward flow only, no traversal consumed, stale tracker reset). * - `revising` — a `revise_needed` with traversals remaining; the back-edge * re-entered the offending upstream nodes (one traversal consumed). * - `stuck` — consecutive identical convergence outputs crossed * `CONSECUTIVE_STALE_THRESHOLD`; the node was retired `completed → done` with * reason `"stuck"`. * - `max_traversals_exhausted` — a `revise_needed` arrived at the hard cap; the * node was retired `completed → done` with the structured exhaustion payload. * - `escalating` — the node signalled `escalate`; the worst signal propagated * forward and failed convergence nodes had their pending upstreams cancelled. * - `ignored` — the node was not a loop-group member; the defensive non-member * guard short-circuited and NO loop semantics ran (no traversal accounting, * no convergence-tracker touch, no propagation). */ export type LoopOutcome = "converged" | "revising" | "stuck" | "max_traversals_exhausted" | "escalating" | "ignored"; /** * The structured escalation payload mandated by failure-resilience.md §1.6 / §4.3. * * Reported for the `stuck` / `max_traversals_exhausted` exits even though the * node lands in terminal `done` (not `escalate`) — the payload describes the * exit, not the node's resulting status (Y17). */ export interface LoopEscalatePayload { /** The machine-readable exit reason: `"max_traversals exhausted"` or `"stuck"`. */ reason: "max_traversals exhausted" | "stuck"; /** Items that could not be resolved by the final traversal (best-effort). */ unresolved: unknown[]; /** The loop group's traversal counter at exit time. */ traversals: number; } /** * What one bounded-cycle step did, for diagnostics and tests. * * Every branch surfaces the same shape so a single call can be inspected * precisely: which upstream nodes were re-marked `ready` (revise), which nodes * were escalated, which pending upstreams were cancelled, and — for an * exhaustion / stuck exit — the full structured escalation payload. */ export interface LoopStepReport { /** The branch taken by this step (see {@link LoopOutcome}). */ outcome: LoopOutcome; /** Loop-group id the step ran against (absent when the node was not a member). */ groupId?: string; /** The loop group's traversal counter after the step. */ traversals: number; /** Upstream nodes re-marked `ready` and added to the frontier (revise). */ revisedUpstream: string[]; /** * Nodes this step retired terminally, whatever status they landed in: a * join-failure escalation (`escalate`) or a `stuck` / * `max_traversals_exhausted` retirement (`completed → done`). The field name * is unchanged for API compatibility — read the node's actual status when the * distinction matters (Y17). */ escalated: string[]; /** * Machine-readable reason of the propagation that ran, mirroring * `SignalPropagationReport.reason` (`"no loop group"` / * `"max_traversals exhausted"` / `"stuck"` / the escalate payload's reason). * Present exactly when {@link propagation} is, so * `engine-advance._notifyPropagatedEscalations` can consume this report's * `{ escalated, reason }` shape directly (Y16). */ reason?: string; /** The escalating node that was re-marked `ready` for an automatic retry. */ retried: string[]; /** Pending upstream nodes retired to `cancelled → done` (cascade). */ cancelled: string[]; /** Upstream nodes that already recorded a payload and were left untouched. */ alreadyResolved: string[]; /** * The underlying signal-propagation report (revise or escalate), when the * step delegated to one of the propagation primitives. */ propagation?: SignalPropagationReport; /** * Structured escalation payload for `stuck` / `max_traversals_exhausted`. * The node lands in terminal `done` for both (Y17). */ escalatePayload?: LoopEscalatePayload; /** Human-readable reason when `answer` was downgraded to `revise_needed` semantics. */ downgradeReason?: string; } export { fingerprintPayload, recordConvergenceOutput, resetConvergenceTracker }; /** * Best-effort extraction of the unresolved items from a `revise_needed` payload. * * Accepts the conventional shapes an orchestration prompt may emit: a top-level * `unresolved` or `items` array of findings, a `findings` array, or — as a * last resort — the payload itself wrapped in a single-element array so the * escalation report never loses the reviewer's message. */ export declare function extractUnresolved(payload: unknown): unknown[]; /** * Run one bounded-cycle step for a loop-group member's terminating signal. * * This is the coalesced integration point for subtasks 1-5. The three soft * early-exits of failure-resilience.md §4.3 are decided here: * * 1. **converged** (`answer`) — reset the stuck tracker and, since the node's * join is satisfied, retire any still-pending upstream nodes the cascade * canceller no longer needs. Forward data flow is left to the caller * (engine-advance's `answer` branch); nothing here consumes a traversal. * 2. **revise_needed** — first check the stuck condition. If identical output * repeats for `>= CONSECUTIVE_STALE_THRESHOLD` traversals, retire the node * `completed → done` with reason `"stuck"` *without* consuming another * traversal. Otherwise check the hard cap: if `isLoopExhausted`, retire it * `completed → done` with the structured * `{ reason: "max_traversals exhausted", unresolved, traversals }` payload. * With traversals remaining, delegate to {@link propagateRevise} for the * traversal increment + back-edge re-entry. * 3. **escalate** — delegate to {@link propagateEscalate} (worst-signal forward * propagation), then, at every convergence node whose join has just failed, * cancel the still-pending upstream nodes via {@link cancelPendingUpstreams}. * * @param state Engine state (source of loop-group + node runtime state). * @param node The loop-group member that emitted the terminating signal. * @param signalType The terminating signal (`answer` | `revise_needed` | `escalate`). * @param payload The signal payload (revision findings for `revise_needed`). * @param cancelPort Optional cascade-canceller seam; when omitted, cancelled * nodes still reach `cancelled → done` but no dispatch task * is torn down. * @returns A {@link LoopStepReport} describing the branch taken. */ export declare function executeLoopStep(state: EngineState, node: NodeRuntimeState, signalType: SignalType, payload: unknown, cancelPort?: CancelDispatchPort): LoopStepReport; //# sourceMappingURL=loop-group-executor.d.ts.map