# @memoflowio/memoflow-sdk

> **Alpha.** This is the reference SDK, not a production-stable package. Read
> [the current status](../../docs/STATUS.md) before relying on durability, embedded recovery,
> worker versioning, streaming or multi-tenant behavior.

TypeScript SDK for [memoflow](https://github.com/evigasoft/memoflow) — a lightweight,
durable workflow engine with a checkpoint/memoization durability model (not replay).

Write long-running, crash-safe workflows as **plain TypeScript classes**. Activities,
timers, and signals are checkpointed automatically; on recovery, completed steps are
restored from a memoized cache. Control flow that reaches durable effects must remain
reproducible; a mismatch is reported as non-determinism. There is no V8 workflow sandbox.

## Install

This package is published publicly on npm:

```bash
npm install @memoflowio/memoflow-sdk
# or: pnpm add @memoflowio/memoflow-sdk
```

No repository-specific registry configuration or package token is required to install it.

The package includes its generated protocol declarations and runtime descriptor; consumers do not
need protoc. Single-file bundlers must preserve `generated/memoflow-v1.protoset` and
`generated/proto-loader-options.json` as filesystem assets beside the installed package. Ordinary
npm/pnpm installs are verified from packed tarballs in CI.

SDK and integration authors can import generated gRPC service types from the stable, type-only
`@memoflowio/memoflow-sdk/protocol` subpath. The generator's `dist/generated/` layout is internal.

Remote calls have a bounded 30-second default gRPC deadline. Set
`defaultRpcTimeoutMs` on `WorkflowClient` when a deployment needs a different finite bound;
long-running result waits retain their explicit operation timeout. The namespace is mandatory—there
is no implicit privileged/default routing choice. Supported unary methods accept `RpcCallOptions`
for an `AbortSignal`, per-call timeout, and opt-in bounded retries driven only by structured
`retryable=true` server details.

The remote package does not install or load a native addon. Node clients may use the explicit
`@memoflowio/memoflow-sdk/client` subpath. The browser-safe
`@memoflowio/memoflow-sdk/browser` subpath exports the dependency-free `BrowserWorkflowClient` over
the generated authenticated HTTP control-plane contract; it never bundles the Node gRPC client.

## Local SQLite durability

Install the separate native adapter for local SQLite execution:

```bash
npm install @memoflowio/memoflow-sdk @memoflowio/memoflow-sdk-embedded
```

`WorkerLite` is exported only by `@memoflowio/memoflow-sdk-embedded` and can select the shared SQLite
kernel explicitly:

```typescript
import { WorkerLite } from '@memoflowio/memoflow-sdk-embedded';

const worker = new WorkerLite({
  dataDir: './memoflow-data',
  backend: 'sqlite',
  durabilityProfile: 'strict', // required: WAL + synchronous=FULL
});
```

`relaxed` uses `synchronous=NORMAL` and can lose a recently acknowledged suffix after an operating
system or power failure. The selected profile is persisted in the database. Opening with a
different profile fails unless that one open also passes `allowProfileChange: true`; this flag is
an explicit operational change, not a normal startup setting.

## Quick look

```typescript
import { Workflow, Run, Signal, Query, type WorkflowContext } from '@memoflowio/memoflow-sdk';

@Workflow({ taskQueue: 'orders' })
export class OrderWorkflow {
  declare ctx: WorkflowContext;

  @Run()
  async execute(order: Order): Promise<OrderResult> {
    // Each ctx.activity call is checkpointed; on recovery it returns from cache.
    const validated = await this.ctx.activity<ValidatedOrder>('OrderActivities.validateOrder', [order]);
    const payment = await this.ctx.activity<PaymentResult>('OrderActivities.chargePayment', [validated]);

    // Durable wait — survives worker restarts.
    const shipment = await this.ctx.waitForSignal<ShipConfirmation>('shipmentConfirmed', { timeout: '7d' });

    return { orderId: order.orderId, status: 'complete', trackingId: shipment.trackingId };
  }

  @Signal('shipmentConfirmed')
  onShipmentConfirmed(data: ShipConfirmation) { /* ... */ }
}
```

## Streaming output

Activities can emit best-effort output chunks — e.g. LLM tokens as they arrive
from a provider's streaming API — with `ctx.stream()`, observed live in the
dashboard's "Output" panel while the workflow is `RUNNING`:

```typescript
import { Activity, activityContext } from '@memoflowio/memoflow-sdk';

@Activity({ taskQueue: 'orders' })
export class LlmActivities {
  @Activity()
  async summarize(prompt: string): Promise<string> {
    const ctx = activityContext();
    let full = '';
    for await (const token of callLlmStreaming(prompt)) {
      full += token;
      ctx?.stream(token); // relayed live; never awaited, never throws
    }
    return full; // the checkpointed, durable result
  }
}
```

**The exact semantic — read this before depending on it for anything but a
live UI:**

- **Best-effort observability only.** `ctx.stream()` chunks are relayed
  in-process, on a single server, to whatever dashboard/SSE subscribers
  happen to be connected at that instant. They are never persisted, never
  replayed on recovery, and never considered for step identity or
  memoization (step identity is unaffected).
- **The checkpoint is the durable truth.** The activity's normal return value
  — written to the checkpoint when it completes — is the only thing recovery,
  memoization, and downstream workflow code ever see. Streamed chunks have no
  bearing on workflow correctness.
- **Chunks may be lost, silently.** A chunk emitted with no subscriber
  connected, emitted before a dashboard tab opens the live panel, or emitted
  while a subscriber is lagging behind, is dropped and never redelivered —
  not on retry of the RPC, not on activity retry, not on server restart.
- **Lite mode:** `ctx.stream()` exists and is a safe no-op — there is no
  server-side relay or dashboard SSE to reach in-process — so identical user
  code runs unmodified on both lite and the server backend.

For product output that must survive disconnects and server restarts, use the separate awaited
`await activityContext()!.appendOutput(chunk, { stream, contentType, end })` API. Appends are
serialized per stream, use deterministic run/stream/offset idempotency keys, and resolve only after
the server's durable acknowledgement. Retried activities may replay identical chunks safely;
changed bytes at an already-committed offset fail loudly. This API intentionally does not degrade
to the best-effort `ctx.stream()` path.

AI provider calls can use the server-authoritative budget fence:

```ts
const ctx = activityContext()!;
const budget = await ctx.reserveAiBudget({ totalTokens: 8_000, modelCalls: 1 });
// Start provider work only after the reservation promise resolves.
const invocation = await ctx.startAiInvocation(invocationInput);
// ...record the terminal invocation transition...
await budget.settle(invocation.invocationId);
```

The SDK never sends a policy snapshot, and settlement never accepts caller-authored usage totals.

Administrative applications can use `AiBudgetClient` to create/version, read and list namespace,
workflow and run policies. The client preserves protobuf `uint64` values as JavaScript `bigint`,
validates hierarchy-override reasons before transport, and never lets worker code supply a policy
snapshot to reservation or settlement calls.
The server resolves the applicable run/workflow/namespace policy and derives actual usage from its
terminal invocation/tool ledger. `AiBudgetDeniedError` means the provider effect must not start.
Operational tooling can call `listReconciliations()` and then `reconcile()` with a stable decision
UUID, timestamp, reason, and evidence digest. Release is permitted only for a provider call proven
not to have started; settlement requires a terminal invocation ID.

Retention operators use the HTTP control-plane client independently of the workflow gRPC address:

```ts
import { RetentionClient } from '@memoflowio/memoflow-sdk/client';

const retention = new RetentionClient({ serverAddress: 'https://control.example', apiKey });
const review = await retention.previewReconciliation({
  namespace: 'production', scopeKind: 'NAMESPACE', limit: 1_000,
});
// Persist and review `review` before the explicit mutation call.
await retention.applyReconciliation({
  namespace: 'production', preview: review,
  auditId: crypto.randomUUID(), occurredAt: new Date(), reason: 'approved DATA-004 review',
});
```

`RetentionClient` also manages versioned policies and legal holds and lists deletion jobs. It
preserves `uint64` policy versions as `bigint`, returns `APPLIED` versus `DUPLICATE`, and refuses
empty, truncated, or cross-namespace reconciliation artifacts before transport. Keep the same
`auditId` and `occurredAt` when retrying an uncertain mutation.

See `examples/order-workflow/src/llm-streaming-activity.ts` for a runnable
example that streams fake LLM tokens.

## OpenTelemetry

The SDK depends on `@opentelemetry/api` — a standard **no-op implementation**
until an application registers a real SDK. memoflow does **not** ship or start
the OpenTelemetry Node SDK for you; bring your own. With no SDK registered,
every span/propagation call in the SDK is a safe no-op and `trace_context`
sent to the server is empty (Law 6: observability only, no semantic claims —
nothing about durability, retries, or memoization depends on tracing being
enabled).

To enable it, register a `NodeSDK` (or an equivalent manual
`BasicTracerProvider` + `ContextManager` + propagator) **before** creating your
`Worker` or `WorkflowClient`:

```typescript
// tracing.ts — imported first, before any memoflow SDK usage.
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: 'http://localhost:4317' }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
```

Once registered:

- `WorkflowClient.start()` injects the caller's active span context into
  `StartWorkflowRequest.trace_context` — the logical "run" trace begins there
  and continues through every server-side dispatch (see the server's
  OpenTelemetry docs for the propagation model — there is no single
  long-lived "run" span; spans are per-operation, linked by the propagated
  W3C `traceparent`/`tracestate` plus a consistent
  `memoflow.namespace`/`memoflow.workflow_id`/`memoflow.run_id` attribute
  namespace).
- The `Worker` wraps each dispatched workflow task in a `memoflow.workflow_task`
  span and each activity execution in a `memoflow.activity {activityType}`
  span, parented to whatever `trace_context` the server attached to the task
  (empty when the server has OTel off — see the server's
  `MEMOFLOW_OTEL__ENDPOINT`).

Exception telemetry is fail-closed. Worker spans export a static redacted exception and status;
worker operational logs retain only a canonical numeric gRPC status when one exists. Exception
messages, stacks, causes, response bodies, payloads, prompts, credentials, and arbitrary string
codes are never copied into logs or spans. Application code still receives the original thrown
value unchanged. The repository telemetry-policy gate rejects direct raw-error logging or span
recording in the SDK runtime.

This is purely additive instrumentation: activities remain at-least-once and
tracing does not change workflow semantics.

## Transactional steps (Postgres tier only)

`ctx.transaction()` (via `activityContext().transaction()`) can commit a step's own database
writes and its checkpoint in the same Postgres transaction. This applies only when the step
writes to the Postgres database that stores MemoFlow checkpoints and when the worker is trusted
with MemoFlow's schema. Requires `WorkerOptions.transactionalStep`
and the optional peer dependency `pg` (`npm install pg` — never required unless you call
`ctx.transaction()`). Postgres tier only: embedded backends throw
`TransactionalStepUnsupportedError`. This is a narrow same-database atomicity mechanism, not a
general exactly-once guarantee; workers using it are trusted with MemoFlow's schema. See the
[security model](https://github.com/evigasoft/memoflow/blob/main/docs/SECURITY-MODEL.md).

## Exports

- **Decorators** — `@Workflow`, `@Run`, `@Activity`, `@Signal`, `@Query`
- **Runtime** — `Worker` and `WorkflowContext`; the separate embedded package exports `WorkerLite`
  and `WorkflowContextLite`
- **Client** — `WorkflowClient`; the separate embedded package exports `ClientLite`

## Documentation

- [Project README](https://github.com/evigasoft/memoflow#readme)
- [Architecture](https://github.com/evigasoft/memoflow/blob/main/docs/ARCHITECTURE.md)
- [Running guide](https://github.com/evigasoft/memoflow/blob/main/docs/RUNNING.md)

## License

[MIT](https://github.com/evigasoft/memoflow/blob/main/LICENSE)
