import type { AgentOptions, ArtifactBody, ArtifactRef, BrowserSession, BrowserSessionOptions, CallOptions, HumanInputOptions, HumanInputResult, PhaseOptions, ShellResult, SleepArg, UsageSnapshot } from "@boardwalk-labs/workflow/runtime"; import type { ShellOptions } from "@boardwalk-labs/workflow"; import type { BrowserSessionManager } from "./browser_session.js"; import type { DesktopSessionHandle, DesktopSessionManager, DesktopSessionOpenOptions } from "./desktop_session.js"; import { type LeafResume } from "@boardwalk-labs/engine/core"; import { type SuspendSignal } from "./suspension.js"; import type { FreezeCoordinator } from "./freeze_coordinator.js"; import type { TurnEventSink } from "./agent/events.js"; /** Parse a `humanInput({ timeout })` string (`"48h"`, `"30m"`, `"90s"`, `"7d"`) to milliseconds, or * null when absent/unparseable (the gate then waits indefinitely). */ export declare function parseTimeoutMs(timeout: string | undefined): number | null; /** The 7-day ceiling on a single hold (matches the legacy sleep tool). */ export declare const MAX_SLEEP_MS: number; /** Runs one ephemeral agent leaf to completion (text, or a schema-validated object). The `signal` * carries cooperative cancellation — the leaf stops its model loop and throws when it fires. A * `resume` (present only on a tool-level human-input resume) re-enters a parked leaf from its * checkpoint + the answers, instead of starting fresh. A leaf that PARKS (the model called the * `human_input` tool with no answer yet) throws {@link LeafParked}, which the host turns into a * suspend — the executor itself never catches it. */ export interface LeafExecutor { run(prompt: string, opts: AgentOptions | undefined, signal?: AbortSignal, resume?: LeafResume): Promise; } /** * When + how often a `workflows.schedule` fires (exactly one of cron/rate/at). MIRRORS the SDK's * `ScheduleOptions` — defined locally so the host compiles BEFORE the @boardwalk-labs/workflow bump * that adds `scheduleWorkflow` to `WorkflowHost`; once that lands, `scheduleWorkflow` below satisfies * the (optional) interface member structurally. */ export interface ScheduleOptions { cron?: string; rate?: string; at?: string | Date; timezone?: string; idempotencyKey?: string; } /** A child run's terminal-relevant state, as the start/poll seams return it. `outputSchema` is * the callee's PINNED version's stored `output_schema` (what lets the SDK revive a typed child's * return); null = untyped callee or a payload that predates the field. */ export interface ChildResult { childRunId: string; status: string; output: unknown; outputSchema: Record | null; } /** What `workflows.call` resolves: the completed child's output + the callee's output schema. */ export interface ChildCallOutput { output: unknown; outputSchema: Record | null; } /** Dispatches child runs: `call` holds in-process + returns the completed child's output (the * hold path); `start`/`poll` back the snapshot-substrate callWorkflow seam (start once, freeze * `waiting_for_child` on a non-terminal child); `run` is fire-and-forget → run id; `schedule` * provisions a durable future/recurring run → schedule id. The `signal` lets a hold/start abort * promptly when the parent run is cancelled. */ export interface ChildDispatcher { call(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise; /** Poll a child run's current state (used by the freeze-wake path to fetch the callee's * output schema — the wake payload carries the output but not the schema); null = not this * run's child. */ poll(childRunId: string): Promise; /** Start (or idempotently re-attach to) a child run; resolves its current state. */ start(slug: string, input: unknown, opts: CallOptions | undefined, signal?: AbortSignal): Promise; run(slug: string, input: unknown, opts: CallOptions | undefined): Promise; schedule(slug: string, input: unknown, opts: ScheduleOptions): Promise; } /** Resolves a granted secret to its plaintext value (audited, fail-closed). */ export interface SecretAccessor { get(name: string): Promise; } /** Register a HELD HITL gate so it is answerable while the run keeps its process, and poll for * the answer. Backed by the broker's `inputs` endpoints. On the snapshot substrate this is the * register-without-release half of a freeze; on a no-freeze runtime (a self-hosted daemon) it is * the WHOLE mechanism — the seam registers, then holds and polls until answered. */ export interface HeldInputPort { register(seq: number, gate: SuspendSignal["humanInput"]): Promise; poll(seq: number): Promise>; } /** Holds the process for `ms` milliseconds. The seam exists so tests don't wait on real time. An * abort fires the hold REJECT (with the signal's RunAbortedError) and clears the timer — so a * multi-day sleep aborted early doesn't leave a live timer pinning the event loop open. */ export interface SleepController { hold(ms: number, signal?: AbortSignal): Promise; } /** Phase lifecycle controller injected by the worker's telemetry layer. */ export interface PhaseController { set(name: string, opts: PhaseOptions | undefined): void; capture(): string | null; runInPhase(phaseId: string | null, fn: () => Promise): Promise; } /** Default controller: a real timer. `setTimeout`'s ~24.8-day max comfortably covers MAX_SLEEP_MS. */ export declare class TimerSleepController implements SleepController { hold(ms: number, signal?: AbortSignal): Promise; } /** A gate's identity on the wire. The runner never learns the control plane's row id (register * answers `{registered}`), and it doesn't need to: `(seq, key)` is unique per gate INSTANCE within * a run — the same key raised at two different parks is two gates — and `key` alone is what a * reader joins the durable row by. */ export declare function gateRequestId(seq: number, key: string): string; /** * The run's identity + on-demand public-API token, surfaced to the program as * `import { runtime } from "@boardwalk-labs/workflow"`. MIRRORS the SDK's `RuntimeContext` — defined * locally so the host compiles BEFORE the @boardwalk-labs/workflow bump that adds the optional * `runtime` member to `WorkflowHost`; once that lands, the host's `runtime` satisfies it * structurally. Platform credentials are NEVER placed in `process.env` (the run env/credential rules): * trusted program code reaches the public-API bearer ONLY through `apiToken()`, which is redacted * from all LLM context. */ export interface RuntimeContext { runId: string; workflowId: string; orgId: string; /** Public API base origin (e.g. `https://api.boardwalk.sh`); the program appends `/v1` or `/mcp/v1`. */ apiUrl: string; /** A short-lived, manifest-scoped bearer for the public API / MCP / CLI. */ apiToken(): Promise; /** A short-lived OIDC id-token asserting this run's identity for `audience`, for federation into * the org's OWN cloud (AWS `AssumeRoleWithWebIdentity` / GCP / Azure). Minted per call by the * broker (gated server-side on `permissions.id_token: "write"`) with the CURRENT run token, so it * needs no swap handling across suspend/resume — unlike the captured `apiToken` bearer. */ idToken(audience: string): Promise; } export interface WorkerWorkflowHostDeps { leaf: LeafExecutor; children: ChildDispatcher; secrets: SecretAccessor; /** The run's identity + on-demand public-API bearer (see {@link RuntimeContext}), exposed to the * program via `import { runtime }`. The bearer never sits in env — it's served on demand here. */ runtime: RuntimeContext; /** Persists a file artifact for the run (→ broker artifact store); resolves to its id + signed * download URL. Absent ⇒ artifacts.write is unsupported and the host method rejects clearly. */ writeArtifact?: (name: string, contentType: string, body: ArtifactBody, metadata: Record | undefined) => Promise; /** Per-run browser-session manager (computer use, the browser tier). Absent ⇒ `computer.openBrowser` * is unsupported (no desktop/browser backend) and the host method rejects clearly. When present, * `agent({ session })` binds the session's in-VM Playwright MCP to the leaf. */ browserSessions?: BrowserSessionManager; /** Per-run desktop-session manager (computer use, the desktop tier). Absent ⇒ `computer.openDesktop` * is unsupported. A desktop `agent({ session })` passes the handle through to the leaf executor, * which binds the raw-coordinate desktop tools (ToolHost hooks) for that leaf. */ desktopSessions?: DesktopSessionManager; /** Cooperative-cancellation signal for the run (credit exhaustion today; user cancel later). Every * hook checks it at entry and unwinds (throws RunAbortedError); the spending/blocking hooks * (`agent`/`sleep`/`callWorkflow`) thread it down so an in-flight op stops promptly. Absent ⇒ no * cancellation (local/pre-watcher path). */ signal?: AbortSignal; /** Called before a real (`ms > 0`) hold begins — used to snapshot the persistent workspace so a * crash during a long sleep can restore it. Best-effort (its own errors are swallowed). Absent ⇒ * no pre-sleep hook (workspace persistence off). */ onBeforeSleep?: () => Promise; /** Defaults to {@link TimerSleepController}. */ sleeper?: SleepController; /** Optional run-detail phase marker support. Absent ⇒ Phase markers are a no-op in this host. */ phases?: PhaseController; /** The run's event stream, for the human-in-the-loop gate frames (`human_input_requested` / * `human_input_resolved`). Without these a gate leaves no trace in the run's story once it is * answered — the question and the answer live only in the respond form, which disappears with * the pause. Observability only; absent ⇒ gates work, unrecorded. */ events?: TurnEventSink; /** Injected clock for `until`-relative sleeps. Defaults to Date.now. */ now?: () => number; /** Override the 7-day hold ceiling (tests). */ maxSleepMs?: number; /** * Snapshot-substrate suspension (the microVM freeze model): when present, a suspending seam * BLOCKS on the coordinator — the platform freezes the whole VM and a wake resolves the seam in * place, heap intact. Every hook also runs under the coordinator's quiescence gate: a freeze * never captures a live platform stream, and work arriving while a freeze is pending queues * until the wake. Absent ⇒ the no-substrate HOLD path: a waiting seam blocks the live process * for the whole wait (self-hosted daemons, the Fargate break-glass, unit tests). */ freeze?: FreezeCoordinator; /** Register + poll for HELD human-input gates (see {@link HeldInputPort}). Absent alongside * `freeze` ⇒ a held gate is answerable only once it freezes; absent WITHOUT `freeze` ⇒ * humanInput is unsupported in this runtime and rejects clearly. */ heldInput?: HeldInputPort; /** Poll interval for a held gate's answer (default 3s). */ heldPollIntervalMs?: number; /** Backs the protocol's `shell` capability (shell_exec's runShell in production). Absent ⇒ * `shell()` is unsupported in this runtime and rejects clearly. */ shell?: (cmd: string, opts: ShellOptions | undefined) => Promise; /** Live budget state for `usage.get` (the BudgetMeter's usageSnapshot in production). Absent ⇒ * `usage.get()` is unsupported in this runtime and rejects clearly. */ usage?: () => UsageSnapshot; /** * Budget clearance (docs/SUSPEND_POLICY.md Decision 3), awaited at the host's blocking/spending * capability seams — `sleep`, `shell`, `workflows.call` — in addition to the leaf executor's * `streamModel` seam. `usd`/`tokens` only move at model calls, but `max_compute_seconds` burns * continuously, so a breach between model calls parks at whichever capability the program touches * next (the park-at-next-seam contract; see budget_gate.ts). Runs INSIDE `guarded()` so the park's * `budgetClearance` work-accounting (endWork around the freeze wait) balances, exactly like the * leaf seam. Absent ⇒ no host-seam parks (tests / legacy paths). */ budgetGate?: { clear(): Promise; }; } export declare class WorkerWorkflowHost { private readonly deps; private readonly sleeper; private readonly now; private readonly maxSleepMs; private readonly heldPollIntervalMs; /** Monotonic per-run counter keying suspensions + their HITL gate rows. */ private readonly seq; /** Run context + on-demand public-API bearer the SDK `runtime` accessor reads off the host. */ readonly runtime: RuntimeContext; constructor(deps: WorkerWorkflowHostDeps); /** Run `fn` only if the run isn't aborted; otherwise REJECT (never throw synchronously) with the * signal's RunAbortedError — every Promise-returning hook funnels through this so callers always * get a rejected promise on abort, not a sync throw. On the snapshot substrate the body also runs * under the freeze coordinator's quiescence gate (see {@link WorkerWorkflowHostDeps.freeze}). */ private guarded; /** A suspending seam's freeze wait: step out of the "work" count around the park (the wait itself * is what the gate waits FOR, not work that blocks it), then rejoin on resume. */ private freezeWait; /** * Snapshot-substrate `humanInput()` with REGISTER-WITHOUT-RELEASE. * A gate reached while a sibling seam is still in flight HOLDS (the quiescence gate won't freeze * yet), but a human must still be able to answer during that hold. So: register the gate with the * broker immediately (it surfaces in the inbox/API at once), then race two outcomes — * - the answer arrives (brokered poll) while holding ⇒ WITHDRAW the freeze wait and resolve * in-process (the run never froze); or * - quiescence is reached with no answer ⇒ the wait freezes, and the wake carries the answer. * Once frozen the poll is frozen too (same process), so the race is only live during the hold. * A register/poll failure degrades to the plain freeze wait — the gate still works, just without * the answerable-while-held property. */ private freezeHumanInput; /** Poll the broker for a held gate's answer until it arrives or the wait withdraws. Resolves with * the answer value; never rejects the run (a transient poll error just retries next tick). Runs * OUTSIDE the quiescence gate (it is not run work — it must not block a freeze). */ private pollHeldAnswer; /** * The no-freeze HOLD for a human-input gate: register it with the broker (it surfaces in the * inbox/API at once), then poll until the answer arrives — the process stays alive and pays * for the wait. Rejects promptly when the run's abort signal fires (cancel / credit stop), * which is also how a server-side gate expiry that fails the run unwinds this loop. */ private holdForAnswer; /** Map a froze/aborted freeze outcome to the gate's answer (or throw on an unexpected abort). */ private resolveFreezeAnswer; /** The wake's answer for one gate key. A wake whose value is missing the parked gate means the * control plane and the snapshot disagree about what this run was waiting for — a platform bug, * failed loudly, never a retry. */ private gateAnswer; setPhase(name: string, opts: PhaseOptions | undefined): void; agent(prompt: string, opts: AgentOptions | undefined): Promise; /** The `agent()` seam: run the leaf to completion. A leaf that PARKS (the model called the * `human_input` tool) waits for the answer — a freeze on the snapshot substrate, a held poll * otherwise — and re-enters from its in-memory checkpoint. Nothing is memoized: a crash-restart * re-runs the leaf (restart-from-top semantics). */ private agentSeam; /** Program-level `humanInput()`: wait on a gate — a freeze on the snapshot substrate, a held * poll otherwise — and return the validated answer. The SDK marks this optional; the hosted * host always implements it. */ humanInput(opts: HumanInputOptions): Promise; private humanInputSeam; /** Wait a registered gate out on whichever substrate the run has: freeze in place on the * snapshot fleet, or (self-hosted runner / local dev) hold the live process and poll until a * person answers. The one place the freeze-vs-hold decision for a gate is made. */ private awaitGate; /** Mark the run as waiting on a person, on the wire. Emitted at the seam (not at registration) so * it lands once for a gate however the substrate waits it out — freeze, hold, or the * register-then-race in between. */ private announceGate; /** …and that the wait is over. The ANSWER isn't on this frame (the wire doesn't carry it): the * gate's durable row holds the response, and a reader joins it by `key`. */ private announceGateResolved; /** * Park the run at a BUDGET gate and resolve with the responder's answer (docs/SUSPEND_POLICY.md * Decision 3). Called when any budget cap (`max_usd` / `max_tokens` / `max_compute_seconds`) is * breached — by the leaf executor's `streamModel` seam (from INSIDE an in-flight `agent()`) and by * the host's own `sleep`/`shell`/`workflows.call` seams (from inside their `guarded()` bodies). * Either way the caller is already work-tracked, which drives two deliberate differences from * {@link humanInputSeam}: * * - **No `guarded()` wrapper.** The enclosing `agent()` seam already counted this leaf as work via * `trackWork`. Wrapping again would double-count it, and the extra count would never be released * — quiescence would never be reached and the freeze would hang forever. `freezeHumanInput` → * `freezeWait` does the right thing here: it `endWork`s for the duration of the park (this leaf * is waiting, not working, which is exactly what lets the run reach quiescence and freeze) and * rejoins on resume. * - **Abort is not re-checked up front.** `streamModel` has just done it; a park is not a new * entry point. * * The gate itself is an ordinary {@link HumanInputGate} keyed `budget`, so it persists, surfaces in * the inbox, and is answered by the same machinery as any other gate. No timeout: an unanswered * budget gate is aged out by the control plane's inactive-cancel reaper, not by a wake we schedule. */ budgetClearance(gate: { prompt: string; inputSpec: unknown; }): Promise; /** Absolute wake time for a `humanInput({ timeout })`, or null when there is none / it's unparseable. */ private timeoutExpiry; callWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise; /** The `workflows.call` seam: start the child once (idempotently — the child's run row is the * durable memo, so a crash-restarted parent re-attaches instead of re-spawning). A non-terminal * child suspends the parent `waiting_for_child` on the snapshot substrate (the wake carries the * finalized child, heap intact); without one the parent HOLDS in-process and polls. */ private callWorkflowSeam; /** Fire-and-forget trigger of another workflow; resolves to the new run's id (no hold/poll). */ runWorkflow(slug: string, input: unknown, opts: CallOptions | undefined): Promise; /** Provision a durable schedule (one-shot/recurring) that fires the target later; resolves to the * new schedule's id WITHOUT running it now. Satisfies the SDK's optional `scheduleWorkflow`. */ scheduleWorkflow(slug: string, input: unknown, opts: ScheduleOptions): Promise; getSecret(name: string): Promise; writeArtifact(name: string, contentType: string, body: ArtifactBody, metadata: Record | undefined): Promise; /** `computer.openBrowser()`: open a program-owned, in-VM browser session (the browser tier of * computer use). Not a durable seam — a session is a live resource, reaped at run end, never * persisted. Absent backend ⇒ a clear "not available" error. */ openBrowserSession(opts: BrowserSessionOptions | undefined): Promise; /** `computer.openDesktop()`: THE run's desktop session (at most one open; the whole screen). * Absent backend ⇒ a clear "not available" (no desktop tier on this runner). */ openDesktopSession(opts: DesktopSessionOpenOptions | undefined): Promise; /** Translate `agent({ session })` per tier. A BROWSER session becomes the leaf's `mcp`: append its * in-VM Playwright MCP (an http ref that passes assertHostedMcpAllowed) and strip the handle (the * engine doesn't understand it). A DESKTOP session passes THROUGH — the leaf executor resolves it * to the raw-coordinate ToolHost hooks. A session that is neither live tier fails clearly. */ private bindComputerSession; sleep(arg: SleepArg): Promise; /** A short sleep HOLDS the task in-process (cheaper than a snapshot round-trip); a long one * (≥ {@link SUSPEND_THRESHOLD_MS}) SUSPENDS on the snapshot substrate — the VM freezes and the * wake resolves this very await, heap intact. Without a freeze substrate EVERY sleep holds, * whatever its length (the no-substrate rule: snapshot or hold, never replay). */ private sleepSeam; /** The protocol's `shell` capability. Runs under the same abort/freeze gate as every hook, so * a freeze never snapshots around an unguarded seam and an aborted run rejects promptly. */ shell(cmd: string, opts: ShellOptions | undefined): Promise; /** Live budget state for `usage.get` — every dimension `{spent, cap, remaining}`. */ usage(): Promise; /** `auth.idToken(audience)` — minted per call by the broker via {@link RuntimeContext}. */ idToken(audience: string): Promise; /** `auth.apiToken()` — the run's short-lived, manifest-scoped public-API bearer. */ apiToken(): Promise; /** Resolve any {@link SleepArg} shape to a millisecond duration from now. Duration STRINGS * normally never reach the host (SDK ≥0.3.9 normalizes them client-side, and the wire schema * has no string member), but the author-facing type includes them, so resolve here too. */ private resolveSleepMs; }