/** * `agency.interrupt` — user-facing TS encapsulation of the codegen * `interrupt(...)` dance. * * Generated Agency code today emits ~35 lines per `interrupt(...)` * call site (see lib/templates/backends/typescriptGenerator/ * interruptAssignment.mustache and interruptReturn.mustache). This * helper mirrors that emission so TS code can raise interrupts the * same way an Agency `interrupt` statement would, fully participating * in handlers, checkpointing, and the resume protocol. * * # Mechanism * * Per call: * 1. Read the persisted interrupt id off the active state frame's * `locals` (keyed by the surrounding step's path). If a response * for that id is already recorded on the context, we are on the * resume side — return it immediately. This is the resume- * idempotency mechanism: the second execution of the same step * body sees the stamp and short-circuits without re-firing * handlers or creating a duplicate checkpoint. * 2. Otherwise, consult `interruptWithHandlers` (the same routine the * codegen path uses). Approved / rejected outcomes flow straight * back to the caller. This routes through `ctx.handlers`, which is * a single stack shared with generated Agency code — so a TS * function called from Agency code can have its * `agency.interrupt(...)` caught by a `handle` block defined in the * calling Agency code, and conversely an Agency `interrupt` * statement nested inside a TS-installed `agency.withHandler(...)` * can be caught by that TS handler. The two surfaces are equal * participants in the same handler stack. * 3. If no handler intercepts, this is a brand-new propagation: * - Persist the new interrupt id on `frame.locals` BEFORE * creating the checkpoint so the id is captured in the * snapshot. (Same ordering as the codegen template — the post- * restore replay relies on reading the id from the restored * frame.) * - Create a checkpoint via `ctx.checkpoints.create` with the * location attached to the active ALS callsite, attach the * resulting id + checkpoint object onto the propagated * interrupt, and `runner.halt(...)` with the interrupt array. * - Throw `HaltSignal`, which the surrounding `Runner.step` * absorbs once the runner is halted. This stops the caller * from executing post-interrupt code on the first pass; on * resume the same call site reaches step 1 and returns the * user's response without re-tripping. * * # Interrupt-id key allocation * * The codegen path keys persisted ids off a compile-time-stable N per * call site (`__interruptId_${N}`). At runtime we don't have N, so * we key off the active callsite's `stepPath` instead: * `__interrupt_${stepPath}`. This makes the constraint explicit: * * **One `agency.interrupt(...)` per step body.** * * Inside `agency.withResumableScope`, each `s.step(...)` body gets its * own substep path, so the natural pattern is "wrap each interrupt in * its own `s.step(...)`". When called from a TS function invoked from * generated code, the surrounding generated step's `stepPath` is the * key — so likewise, raise at most one interrupt per generated step * call site. Code that needs multiple sequential interrupts should * split them across multiple `s.step(...)` calls. * * What happens if you violate this: two raises that share a `stepPath` * share one `frame.locals` key. Take a helper that raises, is answered, * and raises again. The first raise stores its interrupt id under the key * and the run pauses. On resume the helper runs again from its first line. * The first raise finds the stored id and returns the user's answer. The * second raise finds the same stored id and returns that same answer, so * no handler and no user is asked about the second interrupt. * * Two raises made at the same time collide the same way. The second * replaces the first one's stored id and the first one's pause, so the user * is shown only the second interrupt, and on resume both raises return its * answer. * * So TypeScript code raises one interrupt per run, and `interruptFor` * refuses a second. That covers `agency.interrupt()` called twice in one * step, and the handle from `agency.current()`, which raises every * interrupt on the one run it captured. Splitting interrupts across * `s.step(...)` calls is the only safe pattern. * * Agency code is not limited this way. Each `interrupt` statement has its * own key, and a fork or a round of tool calls raises many at once. * * # Halting and HaltSignal * * After `runner.halt(...)`, this function throws `HaltSignal` rather * than returning a synthetic "halted" value. The codegen template * stops execution with an explicit `return` after `runner.halt`; from * inside a TS helper we cannot force the caller to return, so the * throw is the only way to keep post-interrupt code from running on * the first pass. `Runner.step` catches `HaltSignal` when * `this.halted` is set; the user never sees it. * * # Required ALS frame * * Throws if called outside a frame seeded with a `Runner`. In practice * this means: inside `s.step(...)` of `agency.withResumableScope`, or * inside a TS function called from a generated Agency step body. Calls * from the top-level `withResumableScope` body (outside any * `s.step(...)`) intentionally have no runner in the ALS frame and * will throw — wrap the interrupt in `s.step(async () => { ... })`. */ import { type Run } from "./asyncContext.js"; import { type InterruptResponse } from "./interrupts.js"; export type InterruptOpts = { /** Stable identifier for the interrupt effect. Mirrors the effect a * generated `interrupt foo` statement would emit. Used by handlers * to decide whether to intercept. Optional — defaults to * `"unknown"` so quick TS scripts can call `agency.interrupt({message:...})` * without inventing an effect name. */ effect?: string; /** Human-readable description shown to the user / dashboards when * the interrupt propagates. */ message: string; /** Arbitrary structured data attached to the interrupt. Forwarded * to handlers and persisted on the propagated `Interrupt`. * Optional — interrupts that carry no payload (e.g. a bare * "needs user attention") can omit it. */ data?: T; /** Declare that the caller will USE the approval value (the TS analog of * an assignment-position raise, `const x = raise …`). Surfaces as * `Interrupt.expectsValue` so approvers know to provide one. */ expectsValue?: boolean; }; export declare function interrupt(opts: InterruptOpts): Promise; /** `agency.interrupt` for a run the caller already holds. The handle from * `agency.current()` uses this, so a helper can raise after an `await`. */ export declare function interruptFor(rt: Run, opts: InterruptOpts): Promise;