PAIPAI

Realtime

Optional cross-process provider for wakeups and low-latency live.

Realtime is optional.

It is runtime infrastructure, not the browser/client API.

runtime creates an active generation stream and writes frames
runtime publishes sparse thread events such as generation.started, queue-item-added, or thread.changed
  -> realtime provider notifies interested runtime instances and stores active stream buffers
  -> HTTP/SSE, WebSocket, IPC, or direct watch implementation forwards/reduces updates
  -> @pai/client yields ThreadState

Application code still consumes thread.watch(), run.watch(), run.waitUntilBlocked(), and run.waitUntilIdle(). Realtime only changes how quickly those APIs receive updates across processes.

Every runtime already has a local in-process live bus. A realtime provider extends that bus across backend runtime instances. It does not replace the local bus.

import { createProductionThreadLiveProvider } from "./realtime-provider";

export const realtime = createProductionThreadLiveProvider({
  url: process.env.REALTIME_URL!,
});

Use it with createAgentRuntime():

export const runtime = createAgentRuntime({
  agent,
  storage,
  realtime,
  scopeKey: (identity) => identity.workspaceId,
  live: { mode: "distributed" },
});

Realtime improves:

  • watcher latency;
  • smooth cross-runtime open-step streaming;
  • active generation replay after reconnect;
  • faster watcher notification across runtime instances;
  • cross-process notifications.

It does not own correctness. If realtime drops, clients recover through snapshots and polling.

What It Carries

Realtime providers carry backend events for a scoped thread. There are two categories:

  • sparse thread events, such as run.admitted, run.updated, generation.started, generation.completed, queue-item-added, queue-item-removed, and thread.changed;
  • active generation stream buffers, which carry ordered generation.message.chunk events containing exact AI SDK UIMessageChunk values plus PAI's run, message, version, and sequence identity.

Generation frames are for live UX and normal cross-process watch delivery inside the backend. They are compact, ordered, replayable while active, and ephemeral. They do not introduce a second text, reasoning, or tool grammar. The watch handler removes only provider routing fields such as streamId and generationId; it forwards the native chunk event to the private client reducer. The client still repairs from snapshots after reconnects, sequence mismatches, or unsupported events.

Queue events are typed because queueing is ordinary UI state. thread.changed is the repair-friendly fallback. It does not carry a full state payload. It tells the watcher that durable state changed and that it should refresh a snapshot, usually with debouncing.

run.updated carries the complete client-safe run header, including typed metadata. The runtime publishes it after every committed client-readable metadata change on an admitted run. A cancelled-before-admission run has no admitted run header or transcript message. Server-only privateMetadata never enters the realtime provider or browser watch projection.

Every state-bearing sparse event and generation frame carries the owning ThreadVersion. Its threadInstanceId prevents delayed live data from an old, deleted incarnation from being applied to a recreated thread id. Its revision orders durable changes within the current incarnation.

The public watch event vocabulary and the internal frame mapping are documented in Watch Stream Protocol.

What It Does Not Do

Realtime does not:

  • authorize users or resolve scopes;
  • store canonical messages;
  • replace the runtime's local in-process live bus;
  • replace storage leases or queued run records;
  • force every client to keep a persistent connection open;
  • change the public Thread or RunHandle API.

SSE and WebSocket are client transports. A realtime provider is the runtime event source those transports can use when runtime work may happen in another process.

Provider Contract

Provider authors implement ThreadLiveProvider.

It should support subscribe-before-snapshot or equivalent repair behavior so clients do not miss frames between initial load and live watch.

The provider's subscribe() API carries sparse future thread events. It has no reconnect cursor and should not carry every token or tool-input delta. A reconnecting watcher reads a fresh snapshot, then attaches the active generation stream when one exists. Providers that cannot implement an ordered active stream buffer can still publish thread.changed, but distributed watchers fall back to snapshot repair rather than smooth token replay.

Provider events are not the exact same thing as client transport events. A watch handler maps provider thread.changed to client state.changed, forwards authorized transcript-visible message.appended messages, forwards supported sparse events with their ThreadVersion, attaches generation streams when lifecycle events arrive, unwraps each frame's native generation event, and keeps routing signals such as runner.wake inside the runtime.

The provider may be implemented with a shared in-process emitter for tests, Redis Streams, a Postgres outbox, Kafka partitions, Redis pub/sub, Postgres notify/listen, or a managed realtime service. Snapshot recovery is still required because realtime systems can lose connections or drop best-effort events.

Do not configure an external realtime provider just to get same-process push. The runtime's local bus already provides that. Configure realtime when another process may need to observe or wake from this process's frames.

On this page