import type { DurableOutputChunk, ForkExecutionPolicy, LlmStepMeta } from '../types.js'; import { type AiInvocationHandle, type AiMutationSender, type StartAiInvocation } from '../ai/invocation.js'; import { type AiBudgetEstimate, type AiBudgetMutationSender, type AiBudgetReservationHandle, type RecoveredAiBudgetSettlement, type RecoveredAiBudgetSettlementSender } from '../ai/budget.js'; import { TransactionalStepUnsupportedError, type PgQueryable, type ServerSqlContractHandshake } from './transactional-step.js'; export { TransactionalStepUnsupportedError }; export type { PgQueryable }; /** Thrown by `throwIfCancelled()` once cancellation has been observed. */ export declare class CancelledError extends Error { constructor(message?: string); } export declare class AiInvocationLedgerUnsupportedError extends Error { constructor(); } export declare class AiBudgetReservationsUnsupportedError extends Error { constructor(); } export declare class AiRecoveredBudgetSettlementUnsupportedError extends Error { constructor(); } export declare class ForkEffectBlockedError extends Error { constructor(effect: 'model' | 'tool'); } /** * Throw from inside an activity to signal a rate-limit (e.g. HTTP 429) * response that carries a server-supplied `Retry-After` hint. `retryAfter` * overrides the activity's retry policy's computed backoff for the delay * before the NEXT attempt — `maxAttempts` and `nonRetryableErrors` are * unaffected (this attempt still counts, and if `RateLimitError` is listed * in `nonRetryableErrors` it still terminally fails, same as any other * error type). * * `retryAfter` accepts either milliseconds (number) or a duration string * (e.g. `"5s"`, `"500ms"` — same grammar as `ActivityOptions.retry.initialBackoff`, * parsed via `parseDuration`). Internally normalized to milliseconds and * carried to the server as `ActivityFailed.retry_after_ms`. * * ```typescript * import { RateLimitError } from '@memoflowio/memoflow-sdk'; * * async function callApi() { * const res = await fetch(url); * if (res.status === 429) { * const retryAfterSeconds = Number(res.headers.get('retry-after') ?? '5'); * throw new RateLimitError('rate limited', `${retryAfterSeconds}s`); * } * // ... * } * ``` */ export declare class RateLimitError extends Error { /** Retry delay in milliseconds, normalized from the constructor's `retryAfter`. */ readonly retryAfterMs: number; constructor(message: string, retryAfter: number | string); } export interface ActivityExecutionIdentity { readonly namespace: string; readonly workflowId: string; readonly runId: string; readonly stepSeq: number; } export interface ActivityContext { readonly workflowId: string; readonly activityType: string; readonly attempt: number; /** Stable identity of the logical activity across attempt retries. */ readonly executionIdentity?: ActivityExecutionIdentity; readonly forkExecutionPolicy?: ForkExecutionPolicy; /** Called by official provider/tool adapters immediately before dispatch. */ assertForkEffectAllowed(effect: 'model' | 'tool'): void; /** True once any heartbeat response reported cancellation. */ readonly cancelled: boolean; /** * Send a heartbeat: refreshes the task token, extends the server-side * timeout (when `heartbeatTimeout` is set), and returns whether * cancellation was requested. Also latches `cancelled`. */ heartbeat(): Promise; /** Throw `CancelledError` if cancellation has been observed. */ throwIfCancelled(): void; /** * Record structured LLM observability metadata (model, token counts, cost * estimate, finish reason — P3.1 step.llm) for this activity attempt. Call * from inside the activity implementation after the model call returns. * * Last call wins if invoked more than once. Only attached to the * checkpoint on a SUCCESSFUL completion — the worker reads it back via * `activityContext()` right before reporting CompleteActivity, so it must * be called before the activity function returns. Purely observational: * never consulted for memoization or step-identity decisions (Law 1). */ recordLlmMeta(meta: LlmStepMeta): void; /** * Durably start a provider-neutral model invocation for this exact activity * attempt. The promise resolves only after the invocation row and canonical * execution event commit. Raw provider payloads must be stored elsewhere; * this contract accepts digests and protected references only. */ startAiInvocation(input: StartAiInvocation): Promise; /** * Reserve server-authoritative budget capacity before any provider effect. * The promise resolves only after admission commits; a rejection means the * provider call must not start. Settle the returned handle with the terminal * invocation ID so actual usage is derived from the server ledger. */ reserveAiBudget(estimate: AiBudgetEstimate): Promise; /** * Close one prior attempt's RECONCILE reservation after a receiver-owned * durable task proves the original tool effect conclusively. This preserves * the original OUTCOME_UNKNOWN ledger and requires a later attempt of the * same logical activity plus digest-only evidence. */ settleRecoveredAiBudget(input: RecoveredAiBudgetSettlement): Promise; /** * P3.5 — emit a best-effort output chunk (e.g. an LLM token) for live * observability. Relayed by the server to `StreamWorkflowOutput` * subscribers / the dashboard's SSE panel while this activity is running. * * Semantics (read before relying on this for anything but a live UI): * - **NOT durable.** Never checkpointed, never memoized, never replayed on * recovery, never consulted for step identity (Design Law 1). The * activity's eventual return value — written via the normal checkpoint * path — remains the sole source of truth. * - **Best-effort delivery.** Fire-and-forget: `stream()` returns * immediately and NEVER throws or rejects, even if the server is * unreachable, the activity has no server-backed worker (bare function * call in a test), or nobody is subscribed. A chunk emitted with no * subscriber connected, or while the relay is temporarily saturated, is * simply lost — there is no redelivery, no backfill, no queue that * survives a restart. * - **Bounded in-flight.** Excess concurrent sends beyond the internal * queue limit are dropped (oldest-favored: already-inflight sends are * left to finish; a new call past the bound is skipped) rather than * applying backpressure to the activity — streaming must never slow * down or fail the workflow. * - **Lite mode:** a local no-op unless something has installed an * internal sink (see `context-lite.ts`) — this keeps identical user code * valid across lite/server (Law 3) without lite gaining a network * dependency. */ stream(chunk: string | Uint8Array): void; /** * Append product output durably. Calls on the same stream are serialized, * offsets are deterministic from zero, and the promise resolves only after * the server commits the chunk. A retried activity replays identical * offsets idempotently; changed bytes at an existing offset fail loudly. */ appendOutput(chunk: string | Uint8Array, options?: { stream?: string; contentType?: string; end?: boolean; errorType?: string; errorMessage?: string; }): Promise; /** * Read a durable output stream belonging to this exact workflow run. This * is the recovery-side companion to `appendOutput`: activities can restore * a protected recorded result after their process died before completion. */ readOutput(stream: string, options?: { startOffset?: number; pageSize?: number; tail?: boolean; signal?: AbortSignal; }): AsyncIterable; /** * P6.3 transactional step (Postgres tier only) — runs `fn` inside a real * Postgres transaction against the worker's configured * `transactionalStep.connectionString` (the USER's database — see * `WorkerOptions.transactionalStep`), and, when that database is confirmed * to be the SAME physical Postgres database that stores memoflow's own * checkpoints, writes this step's COMPLETED checkpoint atomically in that * SAME transaction. * * **The guarantee, stated precisely (never broader than this):** * exactly-once for this step's database writes, ONLY when the step writes * to the same Postgres database that stores memoflow checkpoints. This * mirrors DBOS's strongest guarantee under the identical precondition — * see the README's Execution guarantees section and * `packages/sdk/src/runtime/sql-contract.ts`'s module doc. * * Throws `TransactionalStepUnsupportedError` when: * - the worker never configured `transactionalStep` at all, * - the server isn't running the Postgres tier ("Postgres tier only"), * - this step has no `step_key` (pre-P0.1 keyless checkpoint — Design Law 1 * requires an identity to write), * - or the databases differ and `degrade` is `'refuse'` (the default). * * On the lite/embedded engine and any non-Postgres server this always * throws the same error — `ctx.transaction()` is Postgres-tier only, * documented, never a silent no-op (Design Law 3 honesty). */ transaction(fn: (tx: PgQueryable) => Promise): Promise; } /** * P6.3 transactional step (Postgres tier only) — everything `transaction()` * needs about this activity attempt and this worker's configuration. * `undefined` (the default, passed by lite mode and any worker that never * configured `WorkerOptions.transactionalStep`) makes `transaction()` throw * `TransactionalStepUnsupportedError` unconditionally — never a silent * no-op (Design Law 3 honesty: identical code, identical loud failure, on * every unsupported target). */ export interface TransactionalStepContext { namespace: string; workflowId: string; runId: string; stepSeq: number; /** Empty means "no step_key" (pre-P0.1 keyless step) — transaction() refuses. */ stepKey: string; activityType: string; connectionString: string; sameDatabase?: boolean; degrade: 'refuse' | 'at-least-once'; handshake: ServerSqlContractHandshake | undefined; } /** Internal implementation handed to the ALS by the Worker. */ export declare class ActivityContextImpl implements ActivityContext { readonly workflowId: string; readonly activityType: string; readonly attempt: number; /** RPC closure supplied by the Worker; resolves to cancel_requested. */ private readonly sendHeartbeat; /** * RPC closure supplied by the Worker for `stream()` (P3.5). `undefined` * (the default) makes `stream()` a pure no-op — used by lite mode and * any caller that doesn't wire streaming support, so identical user code * (Law 3) never throws for calling `stream()` where there's nowhere to * send it. */ private readonly sendChunk?; private readonly sendDurableChunk?; /** P6.3 — see `TransactionalStepContext`'s doc. */ private readonly txContext?; private readonly aiSender?; private readonly newAiId?; private readonly aiBudgetSender?; private readonly readDurableOutput?; readonly executionIdentity?: ActivityExecutionIdentity | undefined; readonly forkExecutionPolicy?: ForkExecutionPolicy | undefined; private readonly recoveredAiBudgetSender?; private _cancelled; private _llmMeta; private _streamSeq; private _inflightStreamSends; /** Latches true after the first stream-send error is logged, so a broken * connection doesn't spam logs for every subsequent chunk. */ private _streamErrorLogged; private readonly _durableOffsets; private readonly _durableChains; /** Set by the Worker when this attempt's checkpoint was already written * transactionally (see `transaction()` below) — read back by the Worker * so it can skip the ordinary CompleteActivity checkpoint write... in * practice CompleteActivity is still called (the server tolerates the * already-COMPLETED checkpoint per P6.3's idempotency contract), so this * is purely informational/for tests. */ private _checkpointedInTx; constructor(workflowId: string, activityType: string, attempt: number, /** RPC closure supplied by the Worker; resolves to cancel_requested. */ sendHeartbeat: () => Promise, /** * RPC closure supplied by the Worker for `stream()` (P3.5). `undefined` * (the default) makes `stream()` a pure no-op — used by lite mode and * any caller that doesn't wire streaming support, so identical user code * (Law 3) never throws for calling `stream()` where there's nowhere to * send it. */ sendChunk?: ((chunk: Uint8Array, seq: number) => Promise) | undefined, sendDurableChunk?: ((streamName: string, offset: number, chunk: Uint8Array, options: { contentType: string; endMarker: boolean; errorType?: string; errorMessage?: string; }) => Promise) | undefined, /** P6.3 — see `TransactionalStepContext`'s doc. */ txContext?: TransactionalStepContext | undefined, aiSender?: AiMutationSender | undefined, newAiId?: (() => string) | undefined, aiBudgetSender?: AiBudgetMutationSender | undefined, readDurableOutput?: ((streamName: string, options: { startOffset?: number; pageSize?: number; tail?: boolean; signal?: AbortSignal; }) => AsyncIterable) | undefined, executionIdentity?: ActivityExecutionIdentity | undefined, forkExecutionPolicy?: ForkExecutionPolicy | undefined, recoveredAiBudgetSender?: RecoveredAiBudgetSettlementSender | undefined); get cancelled(): boolean; heartbeat(): Promise; throwIfCancelled(): void; assertForkEffectAllowed(effect: 'model' | 'tool'): void; recordLlmMeta(meta: LlmStepMeta): void; startAiInvocation(input: StartAiInvocation): Promise; reserveAiBudget(estimate: AiBudgetEstimate): Promise; settleRecoveredAiBudget(input: RecoveredAiBudgetSettlement): Promise; /** Worker-internal: read back whatever was recorded, if anything. */ takeLlmMeta(): LlmStepMeta | undefined; /** True iff the last `transaction()` call wrote its checkpoint in-tx. Test/observability only. */ get checkpointedInTx(): boolean; transaction(fn: (tx: PgQueryable) => Promise): Promise; stream(chunk: string | Uint8Array): void; appendOutput(chunk: string | Uint8Array, options?: { stream?: string; contentType?: string; end?: boolean; errorType?: string; errorMessage?: string; }): Promise; readOutput(stream: string, options?: { startOffset?: number; pageSize?: number; tail?: boolean; signal?: AbortSignal; }): AsyncIterable; } /** * The context of the currently-executing activity, or undefined when called * outside an activity (e.g. from workflow code or plain tests). */ export declare function activityContext(): ActivityContext | undefined; /** Worker-internal: execute an activity function inside its context. */ export declare function runWithActivityContext(ctx: ActivityContextImpl, fn: () => Promise): Promise;