import type { SourceLocationOpts } from "./state/checkpointStore.js"; import type { RuntimeContext } from "./state/context.js"; import type { State } from "./state/stateStack.js"; import { StateStack } from "./state/stateStack.js"; import type { ThreadStore } from "./state/threadStore.js"; import type { HandlerFn } from "./types.js"; /** Options bag for the new `Runner.thread(id, method, opts, callback)` * signature. All fields are optional; emitter passes only the ones * the user supplied via `thread(label: ..., summarize: ..., continue: ..., session: ...) { ... }`. */ export type ThreadStepOpts = { label?: string; summarize?: boolean; /** Slug form (e.g. "t3"). Stripped to raw counter id internally. */ continueId?: string; /** Session name; runtime maps it to a thread id via openSession. */ session?: string; /** When true, the created thread is excluded from `listThreads()`. */ hidden?: boolean; }; /** Strip the leading `t` from a public thread slug to recover the * internal counter-string id. Only strips when the slug matches the * canonical `t` shape so user-chosen ids that happen to * begin with `t` (e.g. session names like `"telephone"`) are not * silently mangled. */ export declare function stripSlug(slug: string): string; /** Serialize a fork branch's return value for the `forkBranchEnd` event * WITHOUT ever throwing — telemetry must not break execution. Returns: * - `undefined` when there is no value (non-success outcome, or a value * JSON can't represent, e.g. a function); * - a deep clone for normal small values (so it renders cleanly); * - a truncated string for oversized values; * - `"[unserializable]"` when (de)serialization throws (e.g. a cycle). * * Redaction happens HERE, during the stringify — not only at post() time. * The truncation branch returns a plain string, and post()'s redaction * replacer cannot see inside a string, so an oversized redact()ed branch * value would otherwise leak its first FORK_VALUE_CHAR_CAP chars into the * event. The redact check composes with the shared nativeTypeReplacer * (like deepClone) so untagged natives and non-redact tags still round-trip * intact for the small path. Reads the caller's store leniently: both call * sites run at the fork join inside the parent's ALS frame; with no frame * this degrades to tag-preserving cloning, which post() still redacts on * the un-truncated path. */ export declare function safeStatelogValue(value: unknown): unknown; /** * Runner centralizes step execution logic for generated Agency code. * * Each node/function gets its own Runner wrapping its State frame. * The builder assigns explicit step IDs that match the source map paths. * The Runner maintains a `path` array that tracks nesting depth — * push when entering a nested scope, pop when leaving. * * Halt propagation: when an interrupt or debug pause occurs, call runner.halt(result). * All subsequent step calls become no-ops. */ export declare class Runner { halted: boolean; haltResult: any; private ctx; private frame; private path; private nodeContext; private state; private moduleId; private scopeName; /** The StateStack this Runner is operating on. When the Runner is running * inside a fork/race branch, this is the branch's stack — and reading * `stack.abortSignal` lets us notice if the branch has been cancelled * (e.g., a race loser whose winner already resolved). */ private stack?; /** ThreadStore active for this Runner's scope. Captured into the * per-step AsyncLocalStorage frame so stdlib helpers that read * `getRuntimeContext().threads` (e.g. the std::thread `*Message` * builtins migrated off the context-injected pattern) see the same * ThreadStore the codegen would otherwise have prepended as a * positional arg. Optional so existing call sites (and the runner * unit tests) keep working without passing it. */ private threads?; constructor(ctx: RuntimeContext, frame: State, opts?: { nodeContext?: boolean; state?: any; moduleId?: string; scopeName?: string; stack?: StateStack; threads?: ThreadStore; }); /** Run `fn` inside an `agencyStore.run` frame seeded with this * Runner's `ctx` / `stack` / `threads`. Stdlib helpers invoked from * inside `fn` will see those values via `getRuntimeContext()` — * matching what the deprecated `__ctx, __stateStack, __threads` * positional args would have carried. If `stack` or `threads` is * missing (older test harnesses that build a Runner without them), * fall through to whatever frame is already on the ALS stack to * avoid clobbering an outer frame with `undefined`. */ private runInScope; /** Whether this runner is driving a graph-node body (vs. a function or * resumable-scope body). TS helpers that need to produce the same halt * payload shape as the codegen `interrupt` templates (`{messages, data}` * in a node body, raw `data` in a function body) read this. Public to * let `agency.interrupt` mirror the codegen branch without exposing * the rest of Runner internals. */ get isNodeContext(): boolean; /** The current step path as a string, e.g. "1.0.2" */ key(): string; /** Return checkpoint metadata for the current step. */ getCheckpointInfo(): SourceLocationOpts; private getCounter; private setCounter; halt(result: any): void; /** Resume any paused guards on the active stack. Idempotent — a * guard already in "running" state no-ops. Called at the top of * every step-equivalent entry point (step, hook, pipe, thread, * fork, debugger). After a halt, the first step entry re-arms * TimeGuards' timers; subsequent entries within the same active * window are no-ops. Also rebuilds non-serialized runtime state * (AbortController, abortSignal composition) after deserialization. */ private beforeStep; private _break; private _continue; /** Pending match-expression exit: the matchId whose owning ifElse will clear * this. Mirrors _break/_continue unwind. NEVER serialized (transient unwind * state; interrupts cannot fire while skipping). Must never be set from a * parallel/fork child — the lowering forbids returns across concurrency * boundaries; this scalar would race. */ private _matchExit; /** Signal the current loop to break after this iteration */ breakLoop(): void; /** Signal the current loop to continue to the next iteration */ continueLoop(): void; /** Yield `value` from a match arm: store it as the match result and skip * everything until the owning ifElse (matchId) consumes the flag. */ exitMatch(matchId: number, value: unknown): void; /** Check if execution should skip (halted, breaking, or continuing). * Also halts if the runner's branch stack has been aborted — but * if any guard's `check()` reports a trip, throw the structured * GuardExceededError instead of silently halting. That lets the * stdlib `guard` function's `try block()` convert it to a Failure * with maxTime/actualTime (or maxCost/actualCost). Race-loser * branch cancels (no guard tripped) still halt silently. * * Three-way decision when `abortSignal.aborted`: * 1. Some guard.check() returns an error → throw it (first trip * reaches user code via stdlib `guard`'s `try`). * 2. No guard returns an error but some guard.isTripped() → the * abort came from a guard whose trip is already consumed * (caught by `try`). The popGuard cleanup steps still need to * run, so don't halt; fall through. * 3. No guard returns an error and none isTripped() → external * abort (race-loser branch cancel). Halt silently as before. */ private shouldSkip; /** Step-boundary guard-trip raise (resumable-guards PR 3). The fast * path is one sync array scan; the raise machinery only engages when * an unsuspended, non-root guard is over budget and armed. NOT * skipped inside tool-call windows (unlike the debugger hook): a * trip mid-tool rides the same in-tool interrupt path as an input() * inside a tool, and on approve the tool continues where it paused * and its result reaches the thread normally — there is never a * dangling tool_use, because the tool call completes on resume. */ private maybeRaiseGuardTrip; /** * Fires the debug/trace hook for a step. Returns truthy if the debugger * wants to pause (in which case the caller should halt). * * Uses a flag in frame.locals to avoid re-triggering on resume: * - First entry: no flag → fire hook → if it halts, set flag * - Resume: flag exists → skip hook → run code → clean up flag * * The flag is NOT deleted here on resume. Instead, step() deletes it * after the callback completes without halting. This handles the case * where a step halts due to a nested interrupt (e.g., function call * that pauses) — the flag stays set so the next resume skips the hook. */ private maybeDebugHook; /** Clean up the debug flag for a step after it completes without halting. */ private clearDebugFlag; private stepPath; private debugFlagKey; /** Seed branch.stack.localCost / localTokens from the parent stack * unless they're already populated (e.g., restored from a checkpoint * on resume). Idempotent. * * Also records `seedCost` / `seedTokens` — the IMMUTABLE baseline used * later by propagateBranchCost to compute this branch's delta. Storing * the baseline on the branch (rather than re-reading the parent at * join time) means the delta survives the parent being mutated in the * meantime — e.g. race losers propagating their spend into the parent * before the winner resumes. See docs/superpowers/specs/2026-05-20- * thread-builtins-and-stdlib-design.md. */ private seedBranchCost; /** Propagate cost/token deltas from a set of branches back to the * outer stack. Delta = branch.localCost - branch.seedCost (the baseline * captured when the branch was seeded). Using the per-branch seed * rather than the parent's current totals is what makes the math * correct when sibling branches have already propagated their spend * into the parent (race losers → parent before winner resumes). * Caller invokes this BEFORE popBranches() or deleteBranch — * otherwise the branch stacks are gone. */ private propagateBranchCost; step(id: number, callback: (runner: Runner) => Promise): Promise; /** * Fire a codegen-emitted callback hook (onFunctionStart, onNodeStart, * onNodeEnd, onEmit) as a substep-counter-idempotent step. Unlike * `runner.step`, this does NOT call `maybeDebugHook` — codegen-emitted * hook sites have no user-visible source line, so pausing on them * would surprise the user (single-step would land on an internal hook * with no current line). * * Callback bodies cannot raise interrupts (statically forbidden by the * typechecker — see `checkCallbackBodyInterrupts`), so `bodyFn` is * fire-and-forget. The substep counter advances after `bodyFn` * resolves so resume re-entries (after a deeper interrupt or debug * pause) skip the hook instead of re-firing it. */ hook(id: number, bodyFn: () => Promise): Promise; debugger(id: number, label: string): Promise; pipe(id: number, input: any, fn: (value: any) => any): Promise; thread(id: number, method: "create" | "createSubthread", optsArg: ThreadStepOpts | (() => ThreadStepOpts | Promise), callback: (runner: Runner) => Promise): Promise; handle(id: number, handlerFn: HandlerFn, callback: (runner: Runner) => Promise): Promise; ifElse(id: number, branches: { condition: () => boolean | Promise; body: (runner: Runner) => Promise; }[], elseBranch?: (runner: Runner) => Promise, opts?: { matchId?: number; }): Promise; loop(id: number, items: any[] | Record | (() => any[] | Record | Promise>), callback: (item: any, second: any, runner: Runner) => Promise): Promise; whileLoop(id: number, condition: () => boolean | Promise, callback: (runner: Runner) => Promise): Promise; branchStep(id: number, branchKey: string, callback: (runner: Runner) => Promise): Promise; /** * Run blockFn for each input in parallel. Each branch gets its own * BranchState (StateStack) for isolation and serialization. * * mode "all" (fork): waits for all, returns results array. * mode "race": returns first to complete. * * blockFn receives (item, index, branchStack) where branchStack is the * branch's isolated StateStack (new or deserialized from interrupt). * * If any branch (fork) or the winner (race) interrupts, returns an * `Interrupt[]` — the caller (generated code) detects this via * `hasInterrupts(...)` and halts. All concurrent interrupts are batched * into the same array under a single shared checkpoint. */ fork(id: number, items: any[], blockFn: (item: any, index: number, branchStack: StateStack) => Promise, mode: "all" | "race", stateStack: StateStack, shared?: boolean): Promise; /** Run all branches in parallel (fork mode). Returns an array of values, * or an Interrupt[] if any branch interrupted. * * Thin adapter over `runBatch({ mode: "all" })`. The primitive owns: * branch lifecycle, abort composition, settle, leaf checkpoint capture, * shared checkpoint stamp + intr.checkpoint overwrite, popBranches on * success. This adapter wires up the fork-specific statelog events and * cost propagation. */ private runForkAll; /** Run all branches concurrently but return as soon as one settles. * * Thin adapter over `runBatch({ mode: "race" })`. The primitive owns * branch lifecycle, abort composition, settle (`Promise.race` with * loser abort), shared checkpoint stamp + intr.checkpoint overwrite, * winner-index persistence under `raceWinnerLocalKey`, loser-branch * deletion, and resume dispatch (re-running only the recorded winner * when a persisted winner is present). The adapter wires up race- * specific statelog events and the asymmetric cost-propagation hooks: * losers propagate eagerly at race time, winner propagates when it * finally completes (no-interrupt resume). */ private runRace; private raceWinnerKey; private forkBranchKey; private forkResultKey; }