You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Persistence + resumable runs as composable `chat()` middleware.
20
15
21
-
`withPersistence(...)` makes any run durable: it loads/saves thread message history (server-authoritative), creates/updates run records, persists every AG-UI `StreamChunk` to an append-only event log, and persists usage. It is fully **optional** — a `chat()` with no persistence middleware is byte-for-byte unchanged, and it works for both non-sandbox and sandbox (agent-mode) runs.
16
+
`withPersistence(...)` makes any run durable: it loads/saves thread message
17
+
history (server-authoritative), creates/updates run records, persists every
18
+
AG-UI `StreamChunk` to an append-only public event log, and persists usage. It
19
+
is fully **optional** - a `chat()` with no persistence middleware is unchanged.
20
+
The primary API is `AIPersistence` / `defineAIPersistence`; `ChatPersistence` /
**Resume.** Each persisted chunk carries an in-band, opaque `cursor` (a monotonic per-run sequence). A client that disconnects mid-run reconnects with the run's `runId` + last `cursor`; `chat({ cursor })` replays the persisted event tail after that cursor, then — for harness adapters that re-attach to their still-running in-sandbox process — continues live. The headless `ChatClient` tracks the cursor and exposes `resume()` / `getResumeState()` / `maybeAutoResume()` with an `autoResume` opt-out.
23
+
**Resume.** Each persisted chunk carries an in-band, opaque `cursor` (a
24
+
monotonic per-run sequence). A client that disconnects mid-run reconnects with
25
+
`{ threadId, runId, cursor }`; `chat({ cursor })` replays the persisted public
26
+
event tail after that cursor. The public chat hooks expose resume state and a
27
+
manual `resume()` helper, with automatic resume behavior enabled unless
28
+
`autoResume` is opted out.
24
29
25
-
**Event model.** The persisted log is the AG-UI `StreamChunk` stream itself (no parallel event type); agent activity (file changes, process output, approvals, artifacts, sandbox lifecycle) rides on well-known `CUSTOM` events catalogued in `@tanstack/ai`.
30
+
**Interrupts.** Actionable waits are represented by
31
+
`RUN_FINISHED.outcome.type === 'interrupt'` and resumed with AG-UI
32
+
`RunAgentInput.resume[]`. Pending user-actionable interrupts block normal new
33
+
input on the same thread by default. Approval custom events are legacy
34
+
compatibility/projection only.
26
35
27
-
**Backends (shared SQL core + thin adapters).** One SQL implementation behind a minimal `SqlDriver` (`@tanstack/ai-persistence-sql`), with backends for SQLite (`-sqlite`, node:sqlite/better-sqlite3), Postgres (`-postgres`, pg), Cloudflare D1 (`-cloudflare`), and bring-your-own Drizzle (`-drizzle`) and Prisma (`-prisma`). Raw drivers auto-migrate (versioned, opt-out); ORMs own their schema. `memoryPersistence()` ships in core for tests/examples.
36
+
**Event model.** Public stream events are separate from internal CAS/checkpoint
37
+
events. `PublicEventStore` replays the AG-UI `StreamChunk` stream itself;
38
+
`InternalEventStore` stores package-owned checkpoints that must not leak into
39
+
public replay. Optional feature validation fails loudly when a requested feature
40
+
is missing its required stores.
28
41
29
-
**Agent mode.**`@tanstack/ai-sandbox-persistence` bridges a durable SQL-backed `SandboxStore` and the durable `LockStore` into `withSandbox`, so sandbox resume and ensure-locking survive across processes. The shared `locks` capability now lives in `@tanstack/ai` (one token across the sandbox and persistence layers); `@tanstack/ai-sandbox` re-exports it for back-compat.
42
+
**Backends (shared SQL core + thin adapters).** One SQL implementation behind a
43
+
minimal `SqlDriver` (`@tanstack/ai-persistence-sql`), with backends for SQLite
`_tanstack_ai_migrations`); ORMs own their schema. `memoryPersistence()` ships
49
+
in core for tests/examples.
30
50
31
-
Approvals are persisted and a durable approval controller feeds decisions back into the existing deny-and-replay flow. Cloudflare is compile-verified (Workers runtime), Postgres runtime-verification is via Docker, and live harness re-attach is verified with the real CLIs; everything else is unit/integration-tested. The Playwright E2E suite is a follow-up.
51
+
**Sandboxes, MCP, and workflows.**`@tanstack/ai-sandbox-persistence` bridges a
52
+
durable SQL-backed `SandboxStore` and the durable `LockStore` into
53
+
`withSandbox`, so sandbox resume and ensure-locking survive across processes.
54
+
MCP persistence is app-owned metadata plus raw stream replay only. Workflow
55
+
extensions are deferred to optional packages and should reuse these primitives
|**ChatClient**| Exposes `addToolApprovalResponse()`→ updates message state → triggers `checkForContinuation()` → sends new stream|
64
+
|**Server (TextEngine)**|`chat()` detects `needsApproval`-> emits a `RUN_FINISHED` interrupt outcome and may project legacy `approval-requested` events|
@@ -213,11 +228,11 @@ import { useChat } from "@tanstack/ai-react";
213
228
import { chatFn } from"./server/chat.server";
214
229
215
230
const { messages, sendMessage } =useChat({
216
-
fetcher: ({ messages }, { signal }) =>chatFn({ data: { messages }, signal }),
231
+
fetcher: (input, { signal }) =>chatFn({ data: input, signal }),
217
232
});
218
233
```
219
234
220
-
The fetcher receives `{ messages, data, threadId, runId}` plus an `AbortSignal` (triggered by `stop()` or when a send is superseded). Return a `Response` — whose SSE body the chat client parses for you — **or** an `AsyncIterable<StreamChunk>`, which is yielded directly. If your server function returns the stream itself (instead of wrapping it in a `Response`), the fetcher handles that too. Sync and `Promise`-wrapped returns are both accepted.
235
+
The fetcher receives `{ messages, data, threadId, runId, cursor, resume }` plus an `AbortSignal` (triggered by `stop()` or when a send is superseded). Forward `cursor` when replaying persisted public events, and forward `resume` when responding to pending AG-UI interrupts. Return a `Response` — whose SSE body the chat client parses for you — **or** an `AsyncIterable<StreamChunk>`, which is yielded directly. If your server function returns the stream itself (instead of wrapping it in a `Response`), the fetcher handles that too. Sync and `Promise`-wrapped returns are both accepted.
221
236
222
237
> **Tip:** The choice between `fetcher` and [`stream()`](#server-functions-and-direct-async-iterables) is about **async vs sync**, not `Response`-vs-iterable — both can yield an `AsyncIterable<StreamChunk>`. `stream()`'s factory must return that iterable **synchronously**, so a server-function call (which returns a `Promise`) won't typecheck there — that's the gap `fetcher` fills ([issue #509](https://github.com/TanStack/ai/issues/509)). Use `stream()` when you can hand back an async iterable synchronously (in-process `chat()`, an RPC client, tests); use `fetcher` for anything you have to `await`. Both normalize to the same request-scoped adapter, so `stop()`/abort, error handling, and tool calls behave identically.
223
238
@@ -315,6 +330,8 @@ function websocketConnection(url: string): SubscribeConnectionAdapter {
`runContext` carries `threadId`, `runId`, `clientTools`, and `forwardedProps`. Include them in your request payload so the server can build an AG-UI-compliant response. If your `connect` stream completes without emitting `RUN_FINISHED`, the runtime synthesizes one for you; if it throws, a `RUN_ERROR` is synthesized.
437
+
`runContext` carries `threadId`, `runId`, `cursor`, `resume`, `clientTools`, and `forwardedProps`. Include them in your request payload so the server can build an AG-UI-compliant response, replay from `{ threadId, runId, cursor }`, and validate `RunAgentInput.resume[]` for pending interrupts. If your `connect` stream completes without emitting `RUN_FINISHED`, the runtime synthesizes one for you; if it throws, a `RUN_ERROR` is synthesized.
419
438
420
439
## The Adapter Interface
421
440
422
441
A `ConnectionAdapter` is a union — provide **either**`connect`, **or** both `subscribe` and `send`. Never both modes.
0 commit comments