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 ThreadStateApplication 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, andthread.changed; - active generation stream buffers, which carry ordered
generation.message.chunkevents containing exact AI SDKUIMessageChunkvalues 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
ThreadorRunHandleAPI.
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.