import { type Interrupt } from "./interrupts.js"; import type { RuntimeContext } from "./state/context.js"; import type { BranchState, State, StateStack } from "./state/stateStack.js"; export type BatchChild = { /** Stable per-child key. Used for `getOrCreateBranch`. Caller is * responsible for uniqueness within `parentFrame.branches`. */ key: string; /** Invoked with the child's own `StateStack` (already seeded with abort * signal composed with parent) and that stack's abort signal. Must * return either a value `T` (success) or an `Interrupt[]` (halted with * interrupts). MUST NOT throw `Interrupt[]`. May throw other errors. */ invoke: (childStack: StateStack, abortSignal: AbortSignal) => Promise; }; export type BatchHooks = { seedBranchCost?: (childStack: StateStack, parentStack: StateStack) => void; /** Used by mode "all" / "sequential" only. The race adapter uses the * asymmetric pair below instead. */ propagateBranchCost?: (branches: BranchState[], parentStack: StateStack) => void; /** Race mode only: propagate loser-branch cost at race-time (before the * losers are deleted). The winner's cost propagates separately on resume * via `propagateWinnerCost`. */ propagateLoserCost?: (loserBranches: BranchState[], parentStack: StateStack) => void; /** Race mode only: propagate the winner's cost when the winner finally * completes (no-interrupt resume). */ propagateWinnerCost?: (winnerBranch: BranchState, parentStack: StateStack) => void; /** Called once per branch start (statelog). */ onBranchStart?: (key: string, index: number) => void; /** Called once per branch end with its outcome and elapsed time in ms. * On a `success` outcome `value` is the branch's return value; for * every other outcome it is `undefined` (there is no value to report). */ onBranchEnd?: (key: string, index: number, outcome: "success" | "interrupted" | "failure" | "aborted", timeMs: number, value?: unknown) => void; /** Called once immediately before `ctx.checkpoints.create` deep-clones * the parent frame. Use this to flush state that was mutated by * sibling branches during the batch into frame-backed locals that need * to survive the checkpoint. Concretely: runPrompt's tool loop pushes * `tool` messages onto the shared `MessageThread` from inside each * branch's body, but `self.messagesJSON` (the snapshot the checkpoint * captures) isn't refreshed by those pushes — the adapter uses this * hook to do `self.messagesJSON = snapshotMessages()` so the * deep-clone sees the up-to-date thread. Without this hook the * checkpoint would carry the pre-batch messages and successful * sibling tool responses would be silently lost on resume. */ beforeCheckpoint?: () => void; /** Called once when the batch stamps its shared checkpoint. */ onCheckpoint?: (checkpointId: number) => void; }; export type RunBatchOpts = { ctx: RuntimeContext; /** The parent's local state stack — used as the capture stack for the * shared batch-level checkpoint. MUST be the local slice (e.g. the * branch stack if `runBatch` is itself called inside a child of an * outer `runBatch`), NOT `ctx.stateStack`. This is the one discipline * the caller of `runBatch` must observe. */ parentStack: StateStack; /** The frame where branch state lives. Usually `parentStack.lastFrame()`. */ parentFrame: State; /** Where the shared checkpoint records its location. Same fields the * existing call sites pass to `ctx.checkpoints.create`. */ checkpointLocation: { moduleId: string; scopeName: string; stepPath: string; }; /** "all" → Promise.allSettled, concurrent; "sequential" → for...of, * each child after the previous (today's callHook semantics); "race" * → first to settle wins, others are aborted. * * IMPORTANT: do not use "all" for hook-callback batching — that would * change today's strictly-sequential `callHook` ordering. Use * "sequential". */ mode: "all" | "sequential" | "race"; children: BatchChild[]; /** Mode "race" only: the `parentFrame.locals` key under which the winner * index is persisted. The caller (race adapter) computes * `__race_winner_${id}` and passes it. */ raceWinnerLocalKey?: string; /** When `true` (default), runBatch records the per-branch outcome on * `parentFrame.branches` via `setResultOnBranch` (success) or * `setInterruptOnBranch` (interrupt). When `false`, the caller's * `invoke` is responsible for managing branch state itself — runBatch * still stamps the shared checkpoint and overwrites * `intr.checkpoint`/`checkpointId`, but does NOT touch BranchState * fields. Used by runPrompt's tool loop where the body manages the * real tool result on the branch. */ recordBranchOutcomes?: boolean; /** When `true`, the branch's ALS frame pointer-shares the parent's * `GlobalStore`. Writes inside the branch land on the parent's store; * siblings see them; the parent observes them after join. User * `fork(..., shared: true)` / `parallel(shared: true)` / * `race(..., shared: true)` opts into this. `runPrompt`'s tool- * dispatch loop also enables it (tool calls are conceptually * sequential function invocations and any global state should * persist across calls). * * When `false` (default), each branch gets a clone of the parent's * `GlobalStore` (via `GlobalStore.clone`). Writes stay branch-local * and are discarded at join. */ shareGlobals?: boolean; /** When `true`, the branch's ALS frame pointer-shares the parent's * `ThreadStore` (and its `activeStack`). Reserved for * implementation-internal batching — most notably `runPrompt`'s * tool-dispatch loop, where multiple tool calls in one LLM round * must all write into the same active thread. * * When `false` (default), the branch gets a `forkBranchView` of the * parent's `ThreadStore` (shared registry, branch-local * `activeStack` seeded with a fresh subthread). User-facing * `shared: true` does NOT set this — concurrent branches pushing/ * popping the same active thread would corrupt the conversation. */ shareThreads?: boolean; hooks?: BatchHooks; }; export type RunBatchResult = { kind: "values"; values: T[]; } | { kind: "interrupts"; interrupts: Interrupt[]; }; export declare function runBatch(opts: RunBatchOpts): Promise>;