PAIPAI

Live Updates

Snapshots, polling, SSE, request streams, and realtime.

PAI separates work from live state delivery.

The runtime processes thread operations and commits durable thread state whether a client is connected or not.

These terms refer to different layers:

TermLayerMeaning
Snapshot refreshClient APIFetch the latest durable state.
PollingClient behaviorRepeated snapshot refresh.
thread.watch()Client APISubscribe to ThreadState updates.
HTTP/SSEClient-server transportCarry requests, responses, and watch streams.
Request streamTransport featureStream events for one request/run.
Realtime providerRuntime infrastructureCross-runtime generation replay and thread-change notifications.

Only snapshots and refreshed thread state are authoritative. Everything else improves latency or delivery.

Consumer APIs use the best live update path available. Applications can call thread.watch(), run.waitUntilBlocked(), or run.waitUntilIdle() without knowing whether the server has a realtime provider. With local or distributed push they update quickly; without a watch transport they fall back to snapshot refresh/polling.

Realtime is not the same thing as SSE or WebSockets. SSE and WebSockets are client transports. A realtime provider is the runtime event source that lets one runtime instance learn about live events produced by another runtime instance. In web deployments those runtimes usually live inside server or worker processes, but the boundary is the runtime, not HTTP.

Every runtime has a local in-process live bus. External realtime extends that bus across runtime instances; it does not replace it.

Runtime live updates are configured by topology:

Runtime ModeMeaning
autoDistributed when realtime is configured, otherwise single-runtime.
singleRuntimeLocal bus only. Correct when one runtime instance owns all writes/runs for the served threads.
distributedLocal bus plus external realtime. If no realtime is configured, fallbackPollIntervalMs must be explicit.

Polling is a fallback wake source. It is useful for degraded distributed operation or transports without watch support, but it is not the normal distributed realtime architecture.

Live Update Contract

APIWhat It ReturnsWithout External RealtimeWith External Realtime
thread.refresh()Latest ThreadState.One snapshot request.Same.
thread.watch()Async iterable of ThreadState.Uses the transport/runtime watch implementation, or client snapshot polling if no watch transport exists.Uses watch events and snapshot repair.
run.watch()Async iterable of ThreadState scoped to one run.Uses the transport/runtime watch implementation, or client snapshot polling if no watch transport exists.Uses watch events and snapshot repair.
run.waitUntilBlocked()Latest ThreadState when the run is waiting or terminal.Waits from watch updates or explicit snapshot refresh.Waits from live updates when available.
run.waitUntilIdle()Final ThreadState.Waits from watch updates or explicit snapshot refresh.Waits from live updates when available.
React hooksProjected ThreadState.Refresh/poll on demand.Live updates when the client is watching.

ThreadState updates drive RunHandle state. If a watch loop receives a new state, run.getState() reflects the latest known status for that run.

Snapshot First

const state = await thread.refresh();

Snapshots are the recovery path. If a watch stream drops, refresh the snapshot and continue from the latest state.

Watch

for await (const state of thread.watch()) {
  render(state);
}

watch() emits ThreadState, not raw protocol frames.

If no live watch transport is available, watch() still works by polling snapshots and yielding when ThreadVersion changes. If a watch transport is available, the server/runtime owns any polling fallback required by its live mode. Realtime changes latency, not the API contract.

Run Updates

Use thread.watch() for live app-facing updates while a run is active.

const run = await thread.send("Draft the report");

for await (const state of thread.watch()) {
  render(state);

  if (state.error?.runId === run.runId) {
    showRunError(state.error.message, {
      reference: state.error.errorId,
      retryable: state.error.retryable,
    });
  }

  const runState = await run.getState();
  if (runState?.status === "completed") break;
  if (runState?.status === "cancelled") break;
  if (runState?.status === "failed") {
    break;
  }
}

Request-scoped streams are an advanced transport feature. They are not durable history. After any stream finishes, use thread state as the source of truth.

Failed runs do not keep a thread busy. The runtime marks the run failed, releases the active run lease, sets state.error, and returns the thread to idle so the user can retry or continue. Queued work behind a failed run is removed with the run-failed queue removal reason instead of being processed against a failed context.

state.error is safe persisted state. Its optional errorId correlates to a trusted server diagnostic and survives refreshes; it does not contain the original exception.

Realtime Is Optional

Without realtime:

runtime still processes work
storage still changes
client catches up by snapshot or poll
runner still finds work by claiming storage

With realtime:

generation event -> notify watch handlers
durable state changed -> refresh latest state

On this page