import type { Run } from "./asyncContext.js"; import type { StatelogClient } from "../statelogClient.js"; import { type StopLocation } from "./debugger.js"; import type { MessageJSON } from "smoltalk"; import type { MessageThreadJSON } from "./state/messageThread.js"; import { type Interrupt } from "./interrupts.js"; import { type RunBatchResult } from "./runBatch.js"; import type { SourceLocationOpts } from "./state/checkpointStore.js"; import type { RuntimeContext } from "./state/context.js"; import { type State, type StateStack } from "./state/stateStack.js"; /** * Thrown by {@link PromptRunner.step} when a step body returns interrupts. * * Caught only at the top of `runPrompt`, which extracts the batched * `interrupts` and returns them as `runPrompt`'s result so the generated * caller can checkpoint and propagate them. NEVER propagates outside * `lib/runtime/prompt.ts`. */ export declare class PromptBailout extends Error { readonly interrupts: Interrupt[]; constructor(interrupts: Interrupt[]); } /** * Frame-backed completion tracking. Lives on `self.runnerState` so it * survives checkpoint/restore the same way `self.messagesJSON` does: * the frame's `locals` object is what gets serialized. An array (used * as a set) keeps the format JSON-safe and matches the project's * "arrays instead of sets" convention. */ export type RunnerState = { completedSteps: string[]; /** The step whose body raised the interrupts the frame paused on * (`bailOut`), so the resume knows which step it re-enters. */ pendingStep?: string; }; /** Which model request of the tool loop a step is: the first one, the * one after a round of tool calls, or a validation retry. The runner * derives the step's key and the debugger's label from it. */ export type WhichLlmCall = { first: true; } | { afterToolRound: number; } | { validationRetry: number; }; /** The step key of a model request. Keys are recorded in checkpoints * (`runnerState.completedSteps`), so they keep the names they had before * the runner derived them. The tool loop uses them in the keys of the * gate steps it runs around a request. */ export declare function llmCallKey(which: WhichLlmCall): string; /** The step path of the statement a runner step is under: the part of * the step's path before the slash that introduces the runner's key * (see `step`). For a path with no key, the path itself. */ export declare function statementStepPath(stepPath: string): string; export type PromptRunnerOpts = { /** Frame-local `self` object (== the function's locals bag). */ self: any; ctx: RuntimeContext; /** The run `runPrompt` was called under, for the debugger's stops. */ run: Run; /** The logger of the run `runPrompt` was called under. `step()` runs in * that run's own flow, never inside a tool branch. */ log: StatelogClient; stateStack: StateStack; /** The runPrompt frame (== `stateStack.lastFrame()`). Used by * `parallel()` as `runBatch`'s `parentFrame` for per-tool branch * lifecycle. Defaulted from `stateStack.lastFrame()` for callers that * don't pass it explicitly. */ parentFrame?: State; /** Source location of the surrounding generated call site. Used as the * base `stepPath` when stamping a checkpoint on bailout — see * `step()` for the per-key suffix. */ checkpointInfo: SourceLocationOpts | undefined; /** Callback that snapshots the current message thread JSON. Called only * on bailout so it doesn't pay the cost on the happy path. * * Returns the FULL `MessageThreadJSON` so per-message debug labels * survive the round-trip; `MessageThread.fromJSON` also still accepts * the bare `MessageJSON[]` that older checkpoints hold. */ snapshotMessages: () => MessageThreadJSON | MessageJSON[]; }; /** * Control-flow helper for `runPrompt`. Owns the idempotent-step + * checkpoint-on-interrupt machinery so the surrounding `runPrompt` * body can be a linear script. * * See `docs/superpowers/plans/2026-05-22-prompt-runner.md`. */ export declare class PromptRunner { private opts; constructor(opts: PromptRunnerOpts); /** * Run `body` as an idempotent step, identified by `key`. * * If `key` is already recorded as completed (resume case), this is a * no-op. Otherwise the body runs: * - if it returns `Interrupt[]`, snapshot messages, stamp a * checkpoint (non-pinned, matching `Runner`'s interrupt * checkpoints), attach the checkpoint id to every interrupt, and * throw `PromptBailout` so `runPrompt` can return the batch up * the stack. The key is NOT marked completed — on resume, the * step re-enters, the body re-runs, and the saved * `__interruptId_N` matches the user's response. * - if it returns nothing (the happy path), the key is marked * completed so the next resume skips this step. * * The checkpoint's `stepPath` is `${checkpointInfo.stepPath}/${key}` * so multiple steps within one `runPrompt` produce distinct * checkpoints. Without this they would all share the runPrompt-level * `stepPath` and the checkpoint store could not tell them apart on * resume. */ step(key: string, body: () => Promise): Promise; /** * Run `body`, a model request, as a step. With a debugger attached the * step stops before the request, labelled `round N` or `validation * retry N`, so a person can change the thread before the model sees * it. See `docs/dev/cli/debugger.md`, "Stops between tool calls". */ llmCall(which: WhichLlmCall, body: () => Promise): Promise; /** Run `body` as the invoke step `key` of the tool call `call` on its * branch runner, shown to the debugger as a wait while it runs * (`whileBusy`), as `llmCall` runs a request. The invoke step is the * one that re-runs when the tool's own interrupt is answered, so a * resumed tool is reported again; a step skipped on a resume opens * nothing. */ invokeTool(branch: BranchRunner, key: string, call: { /** The run and the stack of the tool's branch. */ run: Run; branchStack: StateStack; handler: { name: string; }; namedArgs: Record; /** The tool round, counted from 0 after the first request. */ round: number; }, body: () => Promise): Promise; /** Where one of the loop's steps is, for a stop before it and for the * wait while it runs: under the `llm()` statement, at the statement's * path plus the step's key, on `run`'s branch and at its depth. */ loopLocation(key: string, run: Run): StopLocation; /** * The debugger's stop before the tool call `invocationKey` names, * labelled with the call and its arguments. Nothing happens without a * debugger: the stop is a step of its own, `${invocationKey}.before`, * and recording it would cost every run a key per call. `branchKey` is * the branch the call runs on, for a call that was already running when * the frame was restored; see `resumesInto`. */ beforeToolCall(invocationKey: string, toolCall: { name: string; arguments: Record; }, branchKey?: string): Promise; /** A step with one of the loop's own debugger stops before its body. */ private stepWithStop; /** Whether the resume that restored this frame re-enters the step * `key`: the step was in progress at the pause, so the leg before * passed its stop already, and this leg starts its body again with the * interrupt's answer rather than stopping before it. The same rule as * `Runner.maybeDebugHook`, which a frame restored from a checkpoint * taken with no debugger needs, since it has no flag. Two signs: the * frame paused inside the step's own body (`pendingStep`), or a tool * call's branch, `branchKey`, was restored with the frame and waits to * be claimed. A rewind restores no reentry mark, so it stops as * before. */ private resumesInto; private forgetPendingStep; /** Snapshot the thread, stamp a checkpoint against the runPrompt frame's * stack at this step, attach it to every interrupt, and throw * `PromptBailout` so `runPrompt` returns the batch. */ private bailOut; /** Attach the checkpoint `cpId` to every interrupt and throw * `PromptBailout`. */ private halt; /** The debugger's stop before a step's body, when a debugger is * attached. Works like `Runner.maybeDebugHook`: it skips a handler * body and a callback body, which have no step address a resume could * re-enter; and a flag in the runPrompt frame's locals is set before * the hook fires, so a resume that re-enters the same step finds it * and skips the stop; `stepWithStop` clears it once the body * completes. The stop is located under the `llm()` statement, in its * module and scope, at the statement's path plus the step's key, and * marked `loopStop`, which is how the debugger knows it has no source * line of its own. It runs on the runPrompt frame, one call deeper * than the statement, so `next` at the statement runs the whole call * and `next` at a stop moves to the loop's next stop over a tool's * body. */ private debugStop; /** `${checkpointInfo.stepPath}/${key}`, so every step of one `runPrompt` * has its own checkpoint location. */ private stepPathFor; /** * Run `branchFn` concurrently for every item in `items`. Each call * receives a {@link BranchRunner} which exposes its own `step()` that * COLLECTS interrupts rather than throwing — siblings keep running * even when one halts so we can batch all interrupts into a single * shared checkpoint. * * Thin adapter over {@link runBatch} with `mode: "all"` and * `recordBranchOutcomes: false` (the caller's `branchFn` body manages * branch state itself via `stack.setResultOnBranch` / `deleteBranch`). * `runBatch` owns: per-branch abort composition, ALS-isolated invoke, * settle, shared checkpoint stamp at * `${checkpointInfo.stepPath}/${keyPrefix}` with * `intr.checkpoint`/`checkpointId` overwrite, and `popBranches` on * the no-interrupt success path. Returns a {@link RunBatchResult} * tagged union — `"interrupts"` if any branch halted (the caller * bails out of runPrompt with `result.interrupts`), `"values"` * otherwise. * * `keyFor(item, i)` MUST produce the same branch key the `branchFn` * body uses for its `stack.getOrCreateBranch(...)` — otherwise * `runBatch` would allocate a separate branch from the one the body * manages, and the leaf-checkpoint vehicle into State.toJSON's * branches walk would be lost. * * IMPORTANT: do NOT call `pr.step(...)` (which throws `PromptBailout`) * from inside a branch — use `b.step(...)` instead. A throw from * `branchFn` propagates out of `runBatch` and aborts the whole batch. */ parallel(run: Run, keyPrefix: string, items: T[], keyFor: (item: T, index: number) => string, branchFn: (item: T, b: BranchRunner, index: number, branchRun: Run) => Promise): Promise>; } /** * Branch-local step runner produced by {@link PromptRunner.parallel}. * * Differs from `PromptRunner.step` in two ways: * - On interrupt, collects them on `this.interrupts` rather than * throwing. The parallel orchestrator merges across siblings. * - All subsequent `step()` calls on the same branch short-circuit * once `interrupts` is set — the branch is effectively halted. * * Completion keys live on the same `self.runnerState.completedSteps` * map as the outer runner, so callers must pick keys that are unique * per-branch (e.g. include the per-item id in the key). */ export declare class BranchRunner { private self; interrupts: Interrupt[] | null; constructor(self: any); step(key: string, body: () => Promise): Promise; }