/** * dispatcher.ts — DAG task dispatcher (the "middleman"). * * When a plan is approved, the engine compiles the steps into a Directed * Acyclic Graph (DAG) and executes independent branches concurrently using * the `runAgent` function from `@kmmuntasir/pi-nested-subagents`. * * Key design decisions: * - **runAgent, not pi.callTool**: pi's ExtensionAPI does NOT expose a * `callTool` method. The correct way for an extension to spawn a * sub-agent is `runAgent(ctx, type, prompt, options)` from the * nested-subagents package. Using the non-existent `pi.callTool` was the * root cause of every step failing instantly. * - **Strict context isolation**: each sub-agent is spawned with * `inheritContext: false` and receives only a scoped prompt (step text + * predecessor results + nesting protocol + gate). * - **Self-similar nesting protocol**: every step prompt includes the full * protocol (built via buildNestingProtocol), which the sub-agent is told * to propagate verbatim to its own nested spawns — so recursion works * without the dispatcher * pre-planning every level. * - **Structured result contract**: sub-agents end with a `=== STEP RESULT ===` * block; `extractStepResult` pulls just that block so dependent prompts * stay lean. * - **Hardened dispatch loop**: idempotency guard (no double-spawn), * serialized re-entrancy lock (no overlapping dispatch passes), drain loop * (keep dispatching until no ready steps or cap saturated), and a watchdog * timer that recovers from lost completion callbacks. * - **Resume validation**: `init()` demotes any `done` step whose * predecessors are not all `done` back to `pending`, preventing ordering * inversions after a crash/restart. */ import type { Model, ThinkingLevel } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { PlanStep } from "./types.ts"; import { type StepConfig, type StepMetrics, type DispatcherEvent, type DispatchStatus, type SubDispatcherConfig } from "./types.ts"; import { SubDispatcherNode } from "./sub-dispatcher.ts"; /** * Type of the injectable sub-agent spawn function. Tests pass a stub; the * default delegates to runAgent from @kmmuntasir/pi-nested-subagents. */ /** * Sub-agent spawn function signature (injectable for testing). * * The model from the parent session (ExtensionContext.model) is passed * explicitly as `options.model` so the spawned child inherits the same model. * `runAgent` from pi-nested-subagents already falls back to ctx.model when * options.model is absent, but passing it explicitly makes the contract visible * and ensures the type system enforces propagation when overriding spawnAgent. * * `thinkingLevel` is NOT automatically inherited from the parent context * because ExtensionContext does not expose a thinking-level property. It must * be set explicitly or left undefined to inherit the agent type's default. * This is a known gap; when ExtensionContext gains a thinking-level accessor, * the dispatcher should read it here and pass it through. */ export type SpawnAgentFn = (ctx: ExtensionContext, type: string, prompt: string, options: { pi: ExtensionAPI; inheritContext: boolean; isolated: boolean; depth: number; /** Parent session model object — passed explicitly so children inherit it. */ model?: Model; /** Thinking level override. Omit to inherit agent type's default. */ thinkingLevel?: ThinkingLevel; /** Maximum agentic turns before forced wrap-up. */ maxTurns?: number; }) => Promise<{ responseText: string; aborted: boolean; }>; export declare class Dispatcher { private steps; private cpMap; private predecessorResults; private planSummary; private gateCommand; private concurrencyLimit; private dispatching; private watchdogTimer; private stopped; private lastDeadlockedCount; private pi; private ctx; private planMarkdown; private artifactDir; private spawnAgent; private stepTimeouts; private defaultStepCfg; private timeoutMs; private metrics; private subDispatchers; private onEventCallback?; private onAllDoneCb?; private onStatusChangeCb?; constructor(pi: ExtensionAPI, ctx: ExtensionContext, planMarkdown: string, artifactDir: string, concurrencyLimit?: number, timeoutMs?: number, spawnAgent?: SpawnAgentFn, onEvent?: (event: DispatcherEvent) => void, onAllDone?: (status: DispatchStatus) => void, onStatusChange?: (done: number, total: number) => void | Promise, defaultConfig?: Partial); init(): Promise; getSteps(): PlanStep[]; getStatus(): { done: number; inFlight: number; failed: number; queued: number; total: number; }; /** Return a snapshot of all collected step metrics, including from sub-dispatchers. */ getMetrics(): StepMetrics[]; /** * Create and register a nested SubDispatcherNode for an independent sub-graph. * * Extracts the specified steps (by ID) from the current plan, constructs a * SubDispatcherConfig using the parent's settings (concurrency, timeout, * retry), and wires the parentMetrics callback so sub-dispatcher lifecycle * events flow into the parent's event stream. * * The created sub-dispatcher is NOT automatically started — call `.run()` on * the returned SubDispatcherNode to begin execution. * * @param stepIds - IDs of the steps to include in the sub-graph. * @param configOverride - Optional overrides for the sub-dispatcher config. * @returns The newly created SubDispatcherNode (not yet started). */ createSubDispatcher(stepIds: number[], configOverride?: Partial): SubDispatcherNode; /** Stop the watchdog and prevent further dispatch. Called on walkthrough/reset/shutdown. */ stop(): void; private writeStatusFile; private emitEvent; private startWatchdog; /** * Dispatch all ready steps, respecting the concurrency limit. * Serialized via a promise chain so overlapping calls (from completions, * the watchdog, or init) queue rather than race. */ dispatchReady(): Promise; private executeStep; private clearStepTimeout; private onStepComplete; private onStepFail; /** * Propagate failure downstream through the DAG using an iterative (stack-based) * traversal instead of recursion, preventing stack overflow on deep dependency chains. */ private propagateFailure; /** * Check for deadlocked steps — pending steps whose transitive predecessors * are all failed. Such steps can never become ready and would block execution * indefinitely. When detected, they are marked as failed with a deadlock error. * * Uses iterative ancestor traversal (via detectDeadlockedSteps from dag.ts) * to avoid stack overflow on deeply nested DAGs. * * Only emits notifications when the deadlock count changes, preventing * repeated noise on every watchdog tick. */ private checkDeadlock; /** * Check if all steps have reached a terminal state (done or failed). * * When all steps are terminal: * 1. Stops the watchdog timer. * 2. Emits a "dispatcher_stopped" event with the final status. * 3. Invokes the optional onAllDone callback so the owning code * (index.ts) can react to completion or total failure proactively, * rather than waiting for the next LLM turn's agent_end handler. * * Called from onStepComplete, onStepFail, and the watchdog timer. * Safe to call repeatedly — the stopped flag prevents re-invocation. */ private checkCompletion; private updateTasksFile; } //# sourceMappingURL=dispatcher.d.ts.map