import type { StepCheckpoint, SignalInfo, WorkflowCommand, ActivityOptions, ActivityMethodsOf, ApprovalOptions, ForkExecutionPolicy } from '../types.js'; import { NonDeterminismError } from './step-key.js'; import { type ApprovalDecision } from './approval.js'; import type { ImplicitStepHost } from './runtime-context.js'; import { type HttpEffectRecord, type ImplicitStepsConfig } from './http-effect.js'; export { NonDeterminismError }; export { approvalSignalName, isApprovalSignal, approvalNameFromSignal, InvalidApprovalPayloadError } from './approval.js'; export type { ApprovalDecision } from './approval.js'; /** @deprecated No longer thrown; the worker reads `WorkflowContext.takeCommands()` instead. */ export declare class SuspendExecution { readonly commands: WorkflowCommand[]; constructor(commands: WorkflowCommand[]); } /** * Thrown when a `waitForSignal()` call times out. * This is a regular Error (not SuspendExecution) so workflow code can * catch it in try/catch blocks without accidentally catching suspension. */ export declare class SignalTimeoutError extends Error { readonly signalName: string; constructor(signalName: string, message?: string); } export declare function requireFailedCheckpointError(checkpoint: { error?: unknown; }, operation: string): string; /** * WorkflowContext — the checkpoint/memoization engine. * * This is the core of memoflow's durability model. It replaces Temporal's * event-sourcing + replay with a simpler checkpoint approach: * * 1. Each durable step gets a monotonic step_seq (0, 1, 2, ...) and a * content-addressed step_key * 2. Before executing, check if the step is already checkpointed * 3. If COMPLETED/FAILED → return/throw the memoized outcome instantly * 4. If not (or still in flight) → register a command on the context and * return a promise that never settles in this execution slice * 5. The worker drains the microtask queue, then sends ALL collected commands * in one suspension and releases the task — so `Promise.all` fan-out * schedules every branch's first step concurrently * 6. When a step completes, the server re-dispatches the workflow with * updated checkpoints * * On recovery: workflow re-runs from top, completed steps return instantly, * the first uncompleted steps actually execute. */ export declare class WorkflowContext implements ImplicitStepHost { readonly workflowId: string; readonly runId: string; readonly namespace: string; readonly taskQueue: string; readonly forkExecutionPolicy?: ForkExecutionPolicy | undefined; private stepSeq; private stepKeys; /** * Occurrence tracker for implicit HTTP steps (P6.1). Kept separate from the * explicit `stepKeys` tracker but consuming the SAME `stepSeq` counter, so a * bare `fetch` and a `ctx.activity` interleaved in source order each get a * distinct, monotonic seq — while HTTP occurrence indexing stays independent * of activity/timer names. */ private httpStepKeys; /** Implicit-steps config (P6.1). Defaults make bare fetch durable. */ readonly implicitConfig: ImplicitStepsConfig; private checkpoints; private signals; /** Commands collected during this execution slice (fan-out aware). */ private pendingCommands; /** * Steps this slice awaited that are already STARTED server-side — they need * no new command, but they mean "suspend and wait", not "stalled". */ private inFlightSteps; /** Implicit HTTP calls currently on the wire (P6.1). See markHttpInFlight. */ private httpInFlight; /** * P9.3 eager flush: sends effect-record commands to the server MID-SLICE * (a RecordEffects session frame) and resolves only after the server's * DurableAck — i.e. after the store write committed. Wired by the Worker * (bound to the live session + this task's token); absent in bare unit * tests, where records fall back to suspension-time batching. */ private readonly effectFlusher?; /** * A promise for a step the server must resolve. It never settles in this * execution slice; the workflow re-runs from the top when the step's * checkpoint lands. It never rejects either, so parked branches of a * `Promise.all`/`race` can't leak unhandled rejections. * * Parked promises are intentionally never settled: with no executor state * and nothing external retaining them, they (and the continuations awaiting * them) become garbage-collectible as soon as this execution slice's * WorkflowContext is released. Do not "fix" this by settling or tracking * them — settling would resume abandoned continuations against a stale * slice, and tracking would keep the whole slice alive. */ private static parked; constructor(workflowId: string, runId: string, namespace: string, taskQueue: string, completedSteps: StepCheckpoint[], pendingSignals: SignalInfo[], implicitConfig?: Partial, effectFlusher?: (commands: WorkflowCommand[]) => Promise, forkExecutionPolicy?: ForkExecutionPolicy | undefined); assertImplicitEffectAllowed(): void; /** * Execute a durable activity. If the activity has already completed * (checkpoint exists), returns the memoized result immediately. Otherwise, * registers a SCHEDULE_ACTIVITY command and parks — the worker suspends the * task once every concurrently-reachable step has registered. * * @param activityType - The registered activity name (e.g., "OrderActivities.chargePayment") * @param args - Arguments to pass to the activity function * @param options - Activity options (retry policy, timeouts, etc.) */ activity(activityType: string, args: any[], options?: ActivityOptions): Promise; /** * Typed sugar over `activity()` for LLM/AI calls (P3.1 step.llm). * * This does NOT change step identity or checkpoint semantics — step_kind * stays ACTIVITY, occurrence counting is identical to a plain * `ctx.activity()` call (Design Law 1 is untouched). The only difference * is documentary: it signals that the named activity is expected to call * `activityContext().recordLlmMeta()` before returning, so the dashboard * can render it as an LLM step (model, tokens, cost, finish reason). * * @param activityType - The registered activity name, same as `activity()` * @param args - Arguments to pass to the activity function * @param options - Activity options (retry policy, timeouts, etc.) */ llm(activityType: string, args: any[], options?: ActivityOptions): Promise; /** * Durable sleep — pauses the workflow for the specified duration. * The timer is managed server-side and survives worker restarts. * * @param duration - Duration string (e.g., "5s", "1m", "24h") */ sleep(duration: string): Promise; /** * Wait for an external signal. If the signal has already arrived, * returns immediately. Otherwise, suspends until the signal is delivered. * * @param signalName - The name of the signal to wait for * @param options - Optional timeout */ waitForSignal(signalName: string, options?: { timeout?: string; approval?: ApprovalOptions; }): Promise; /** * Human-in-the-loop approval — sugar over `waitForSignal` with a * typed `{ approved, by, comment }` payload. * * Waits on the signal `approval:{name}` (see `approvalSignalName`); the * server creates a durable approval work item with the wait. A decision on * that item emits the signal that completes the wait; the work item and its * audit record are the authoritative decision evidence. * * Rejection is not an error: `{ approved: false }` resolves normally, same * as `{ approved: true }` — the workflow decides what to do with the * decision. A malformed payload (missing/non-boolean `approved`, or * non-string `by`/`comment`) throws `InvalidApprovalPayloadError` rather * than being silently coerced (Design Law 4 spirit — a bad payload must * fail loudly, not fabricate a fake audit record). A timeout surfaces the * existing `SignalTimeoutError`, exactly like a plain `waitForSignal`. * * @param name - Approval name, e.g. 'deploy-plan' * @param options - Optional timeout, same as `waitForSignal` */ approval(name: string, options?: ApprovalOptions): Promise; /** * Record a side effect — a non-deterministic value that is computed once * and memoized (e.g., UUID generation, current time). * * Unlike other durable steps, the function executes immediately and its * value is returned to the workflow right away; a RECORD_SIDE_EFFECT * command is batched with the slice's suspension so the server writes a * COMPLETED checkpoint. Every re-run then returns the identical value * without re-executing the function. (If the slice completes the workflow * without ever suspending, no re-run can happen, so nothing is lost.) * * @param fn - Function that produces the side effect value */ sideEffect(fn: () => T): Promise; /** * Consume this HTTP call's occurrence index and step_seq (Design Laws 1, 9). * Called synchronously by the interceptor in source call order. `source` is * the pre-hashed `"HTTP\n"` string. */ nextHttpStepKey(source: string): { stepSeq: number; stepKey: string; }; /** * Look up a completed HTTP-effect checkpoint for replay. Validates step_key * (NonDeterminismError on call-site drift), soft-warns on method/URL drift. */ lookupHttpEffect(stepSeq: number, stepKey: string, method: string, url: string): HttpEffectRecord | undefined; /** * Record a freshly-executed HTTP effect (a RecordSideEffect with `http` * set). P9.3 eager flush: when the Worker wired an effect flusher, the * record is sent MID-SLICE as a RecordEffects session frame and the * returned promise resolves only after the server's DurableAck — the * interceptor awaits it before delivering the Response, so Design Law 2 * holds PER STEP on the server tier exactly as it does on lite. A flush * rejection (stale task token after a reap — FrameError; or a dead * session) rejects the fetch loudly; the response is never delivered on * an unrecorded effect. * * Without a flusher (bare contexts in unit tests) the record falls back * to suspension-time batching, where the server writes it before * dispatching any sibling schedule (P0.5 ordering). */ recordHttpEffect(stepSeq: number, stepKey: string, record: HttpEffectRecord, meta?: Record): void | Promise; /** * Deterministic idempotency key derived from run_id + step_key — stable * across re-runs, unique per step. * * You normally never need this: the interceptor injects an * `Idempotency-Key` on every mutating implicit call automatically * (`implicitSteps.autoIdempotencyKey`, default on), using THE step's own * key at dispatch time. This method exists for keying an EXPLICIT step's * outbound call, and requires the step key. * * The old no-arg form (defaulting to "the most recent HTTP step's key") * was removed (N2): headers are built BEFORE the fetch consumes its step * key, so under concurrent branches the default could name a different * step on the retry than on the original — an unstable idempotency key is * worse than none, because it LOOKS safe. */ idempotencyKey(stepKey: string): string; /** * Escape hatch (P6.1): `ctx.raw.fetch(...)` performs a NON-durable fetch — * byte-identical to un-instrumented Node — by tagging the request so the * interceptor passes it straight through. Use for health checks, streaming * endpoints, or anything you explicitly do not want checkpointed. */ get raw(): { fetch: typeof fetch; }; /** * Drain the commands collected during this execution slice. Called by the * worker after the microtask queue goes quiescent — everything collected is * sent in a single suspension. */ takeCommands(): WorkflowCommand[]; /** * Total durable actions this slice is waiting on: commands collected plus * steps already in flight server-side. The worker's drain loop stops when * this stops growing; zero at quiescence means the workflow is stalled on * something that is not a durable step. */ get pendingActionCount(): number; /** ImplicitStepHost: track in-flight implicit HTTP calls (P6.1). */ markHttpInFlight(delta: number): void; /** * Implicit HTTP calls currently on the wire. Unlike parked activity steps * (which never resolve within this slice), an in-flight fetch WILL settle — * so the driver must keep draining while this is nonzero, even if the * pending-action count is momentarily stable. */ get httpInFlightCount(): number; /** * Get the current step sequence number (for debugging). */ get currentStep(): number; /** * Check if a specific step has been completed. */ isStepCompleted(stepSeq: number): boolean; /** * Create a type-safe proxy for calling activities. * Returns an object whose methods match the activity class's async methods, * with full type checking on arguments and return types. * * @param activityClass - The activity class constructor (must be decorated with @Activity) * @param options - Default activity options applied to all calls through this proxy * * @example * ```typescript * const acts = this.ctx.proxyActivities(OrderActivities); * const result = await acts.chargePayment(order); // fully typed * ``` */ proxyActivities(activityClass: new (...args: any[]) => T, options?: ActivityOptions): ActivityMethodsOf; } /** * Soft-warn when an implicit HTTP step replays against a checkpoint whose * recorded method+URL differs from the current call's (P6.1). This is NOT a * failure: the call-site step_key already matched (identity is stable), and * URLs legitimately embed checkpoint-derived dynamic values. A drift usually * means a memoized value the URL depends on changed — worth surfacing, not * worth failing. */ /** * F8 advisory: step identity is (kind, name, occurrence) — arguments are NOT * part of it, so two same-named activities reordered by a code change replay * each other's memoized results with matching keys. When the checkpoint * carries the recorded input, compare it to the current call's args and warn * on drift. Soft by design: args that embed non-memoized values (timestamps) * legitimately differ between runs while the memoized result stays correct. */ export declare function warnOnActivityInputDrift(stepSeq: number, activityType: string, storedInput: Buffer | Uint8Array | undefined, args: unknown[], storedInputSha256?: string): void; export declare function warnOnUrlDrift(stepSeq: number, storedName: string, method: string, url: string): void;