/** * ContinuationResolver — Back-edge resolution + iteration counting. * * Resolves dynamic continuations (loop-backs, dynamic next) and tracks * per-node iteration counts for context tree naming. * * Supports three dynamicNext patterns: * - String ID → reference to existing node (resolve via NodeResolver) * - StageNode with fn → truly dynamic node (execute directly) * - StageNode without fn → reference by ID (resolve via NodeResolver) * * Two entry points: * - `resolveTarget` — resolves the continuation to `{ node, context }` and * fires every side effect (iteration counting, debug logs, `onLoop` * narrative) WITHOUT executing. The traverser's trampoline driver uses * this to follow loop edges iteratively — flat stack, so the iteration * limit (not call-stack depth) is what bounds a loop. * - `resolve` — resolveTarget + immediate execution via the provided * `executeNode` callback. Kept for direct/advanced callers. */ import type { StageContext } from '../../memory/StageContext.js'; import type { StageNode } from '../graph/StageNode.js'; import type { TraversalContext } from '../narrative/types.js'; import type { HandlerDeps } from '../types.js'; import type { NodeResolver } from './NodeResolver.js'; import type { ExecuteNodeFn } from './types.js'; export declare const DEFAULT_MAX_ITERATIONS = 1000; /** * A resolved continuation target — the node to execute next plus the * StageContext to execute it in. All side effects (iteration counting, * debug logs, `onLoop` narrative) have already fired by the time this * is returned. */ export interface ResolvedContinuation { node: StageNode; context: StageContext; } export declare class ContinuationResolver { private readonly deps; private readonly nodeResolver; /** * Iteration counter per node ID. * Key: node.id, Value: visit count (0 = first visit). */ private iterationCounters; /** * Total fn-bearing dynamic-next hops this traverser has followed. * * Fresh fn-bearing nodes bypass the per-node-id iteration counter (they * are new nodes, often without stable ids — there is no back-edge to * count). Without a bound, a stage that keeps returning a function-bearing * dynamic `next` runs FOREVER on the flat trampoline (no stack overflow * brakes it either). This run-total counter puts such chains under the * same `maxIterations` budget (default 1000, tuned via * `RunOptions.maxIterations`) that bounds loop edges. */ private dynamicNextHops; private readonly onIterationUpdate?; private readonly maxIterations; constructor(deps: HandlerDeps, nodeResolver: NodeResolver, onIterationUpdate?: (nodeId: string, count: number) => void, maxIterations?: number); /** * Resolve a dynamic continuation and execute it immediately. * Equivalent to `executeNode(...resolveTarget(...))` — the traverser's * driver loop calls `resolveTarget` directly instead so the continuation * becomes a flat trampoline hop rather than a retained recursive frame. */ resolve(dynamicNext: string | StageNode, node: StageNode, context: StageContext, breakFlag: { shouldBreak: boolean; }, branchPath: string | undefined, executeNode: ExecuteNodeFn, traversalContext?: TraversalContext): Promise; /** * Resolve a dynamic continuation to its target node + next StageContext * WITHOUT executing it. Fires the same side effects `resolve` always did * (iteration counting + limit, `dynamicNext*` logs, loop debug message, * `onLoop` narrative), in the same order. * * Three dynamicNext patterns: * - StageNode with fn → truly dynamic node, returned as-is (no per-node * iteration tracking — it is a fresh node, not a back-edge — but the * run-total dynamic-hop budget applies; see `dynamicNextHops`). * - String ID → reference to an existing node, resolved via NodeResolver. * - StageNode without fn → reference by ID, resolved via NodeResolver. */ resolveTarget(dynamicNext: string | StageNode, currentNode: StageNode, context: StageContext, branchPath: string | undefined, traversalContext?: TraversalContext): ResolvedContinuation; /** * Get the next iteration number for a node and increment. * Returns 0 for first visit, 1 for second, etc. * Throws if maxIterations exceeded (infinite loop guard). */ getAndIncrementIteration(nodeId: string): number; /** * Generate an iterated stage name for context tree. * First visit: "askLLM", second: "askLLM.1", third: "askLLM.2". */ getIteratedStageName(baseName: string, iteration: number): string; }