Skip to content

Commit 9981da4

Browse files
committed
docs(persistence): reorganize persistence guides
1 parent c6cccfa commit 9981da4

27 files changed

Lines changed: 1154 additions & 1065 deletions

docs/api/ai-angular.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export class CustomGenerationComponent {
386386
**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `resume`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` — all reactive state is a read-only `Signal<T>`.
387387

388388
For the end-to-end persistence setup, see
389-
[Resumable Generations](../persistence/resumable-generations).
389+
[Generation Persistence](../persistence/generation-persistence).
390390

391391
### `injectGenerateImage(options)`
392392

docs/api/ai-react.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Generation hook returns include `generate`, `result`, `isLoading`, `error`,
175175
`jobId` and `videoStatus`.
176176

177177
For the end-to-end persistence setup, see
178-
[Resumable Generations](../persistence/resumable-generations).
178+
[Generation Persistence](../persistence/generation-persistence).
179179

180180
## Example: Basic Chat
181181

docs/api/ai-solid.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ so read `resumeState()` and `pendingArtifacts()`. Video generation also returns
138138
`jobId` and `videoStatus`.
139139

140140
For the end-to-end persistence setup, see
141-
[Resumable Generations](../persistence/resumable-generations).
141+
[Generation Persistence](../persistence/generation-persistence).
142142

143143
## Connection Adapters
144144

docs/api/ai-svelte.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ const gen = createGeneration({
304304
**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `updateBody`, `dispose`, `resume`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` -- all state properties are reactive getters.
305305

306306
For the end-to-end persistence setup, see
307-
[Resumable Generations](../persistence/resumable-generations).
307+
[Generation Persistence](../persistence/generation-persistence).
308308

309309
### `createGenerateImage(options)`
310310

docs/api/ai-vue.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ const { generate, result, isLoading, error, status, stop, reset } =
321321
**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `resume`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` -- all reactive state is `DeepReadonly<ShallowRef<T>>`. Read refs with `.value` in `<script setup>`.
322322

323323
For the end-to-end persistence setup, see
324-
[Resumable Generations](../persistence/resumable-generations).
324+
[Generation Persistence](../persistence/generation-persistence).
325325

326326
### `useGenerateImage(options)`
327327

docs/chat/connection-adapters.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ const { messages, sendMessage } = useChat({
235235
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.
236236

237237
For the server persistence path that produces those replay cursors, see
238-
[Resumable Chat](../persistence/resumable-chat).
238+
[Chat Persistence](../persistence/chat-persistence).
239239

240240
> **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.
241241
@@ -441,7 +441,7 @@ const { messages } = useChat({ connection: myAdapter });
441441

442442
If the request-scoped adapter talks to a persisted `chat()` endpoint, those
443443
fields are the durable replay identity described in
444-
[Resumable Chat](../persistence/resumable-chat).
444+
[Chat Persistence](../persistence/chat-persistence).
445445

446446
## The Adapter Interface
447447

docs/chat/persistence.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ This is especially useful for SPAs, Electron apps, and offline-first setups wher
1919

2020
If your server owns the transcript and you need durable event replay after a
2121
reconnect or reload, use server-side persistence instead. See
22-
[Resumable Chat](../persistence/resumable-chat) for the full `withPersistence`
23-
server/client wiring.
22+
[Chat Persistence](../persistence/chat-persistence) for the full
23+
`withPersistence` server/client wiring.
2424

2525
## The adapter interface
2626

docs/config.json

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -410,50 +410,63 @@
410410
"updatedAt": "2026-07-08"
411411
},
412412
{
413-
"label": "Resumable Chat",
414-
"to": "persistence/resumable-chat",
415-
"addedAt": "2026-07-03",
416-
"updatedAt": "2026-07-08"
413+
"label": "Persistence Controls",
414+
"to": "persistence/controls",
415+
"addedAt": "2026-07-08"
417416
},
418417
{
419-
"label": "Resumable Generations",
420-
"to": "persistence/resumable-generations",
421-
"addedAt": "2026-07-07",
422-
"updatedAt": "2026-07-08"
418+
"label": "Migrations",
419+
"to": "persistence/migrations",
420+
"addedAt": "2026-07-08"
423421
},
424422
{
425-
"label": "Interrupts and Approvals",
426-
"to": "persistence/interrupts-and-approvals",
423+
"label": "Chat Persistence",
424+
"to": "persistence/chat-persistence",
427425
"addedAt": "2026-07-03",
428426
"updatedAt": "2026-07-08"
429427
},
430428
{
431-
"label": "SQL Backends",
432-
"to": "persistence/sql-backends",
429+
"label": "Sandbox Persistence",
430+
"to": "persistence/sandbox-persistence",
433431
"addedAt": "2026-07-03",
434432
"updatedAt": "2026-07-08"
435433
},
434+
{
435+
"label": "Generation Persistence",
436+
"to": "persistence/generation-persistence",
437+
"addedAt": "2026-07-07",
438+
"updatedAt": "2026-07-08"
439+
},
440+
{
441+
"label": "MCP Persistence",
442+
"to": "persistence/mcp-persistence",
443+
"addedAt": "2026-07-08"
444+
},
436445
{
437446
"label": "Cloudflare",
438447
"to": "persistence/cloudflare",
439448
"addedAt": "2026-07-03",
440449
"updatedAt": "2026-07-08"
441450
},
442451
{
443-
"label": "Files and Artifacts",
444-
"to": "persistence/files-and-artifacts",
445-
"addedAt": "2026-07-06",
446-
"updatedAt": "2026-07-08"
452+
"label": "Prisma",
453+
"to": "persistence/prisma",
454+
"addedAt": "2026-07-08"
447455
},
448456
{
449-
"label": "Custom Stores",
450-
"to": "persistence/custom-stores",
457+
"label": "Drizzle",
458+
"to": "persistence/drizzle",
459+
"addedAt": "2026-07-08"
460+
},
461+
{
462+
"label": "SQL Backends",
463+
"to": "persistence/sql-backends",
451464
"addedAt": "2026-07-03",
452465
"updatedAt": "2026-07-08"
453466
},
454467
{
455-
"label": "Sandbox Runs",
456-
"to": "persistence/sandbox-runs",
468+
"label": "Custom Stores",
469+
"to": "persistence/custom-stores",
457470
"addedAt": "2026-07-03",
458471
"updatedAt": "2026-07-08"
459472
}

docs/media/generation-hooks.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ The same pattern applies to all generation types -- swap `generateImage` for `ge
6363

6464
For refresh-safe generation, wrap the server call with `withPersistence(...)`
6565
and parse hook requests with `generationParamsFromRequest(...)`. See
66-
[Resumable Generations](../persistence/resumable-generations) for the complete
66+
[Generation Persistence](../persistence/generation-persistence) for the complete
6767
server and client flow.
6868

6969
## useGenerateImage
@@ -434,7 +434,7 @@ Generation hooks do not store generated image, audio, speech, transcription, or
434434
video bytes in browser persistence. The client snapshot keeps only
435435
`{ threadId, runId, cursor }` plus lightweight artifact refs. Use server
436436
persistence artifacts when generated media must survive refresh:
437-
[Resumable Generations](../persistence/resumable-generations) shows the full
437+
[Generation Persistence](../persistence/generation-persistence) shows the full
438438
pattern.
439439

440440
### Result Transforms
@@ -519,4 +519,4 @@ All framework packages re-export `fetchServerSentEvents`, `fetchHttpStream`, and
519519
- [Transcription](./transcription) -- File formats, language detection, and word-level timestamps
520520
- [Video Generation](./video-generation) -- Job lifecycle, polling, and provider setup
521521
- [Generations Overview](./generations) -- Architecture and server-side streaming patterns
522-
- [Resumable Generations](../persistence/resumable-generations) -- Refresh-safe generation and persisted artifact refs
522+
- [Generation Persistence](../persistence/generation-persistence) -- Refresh-safe generation and persisted artifact refs
Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
---
2-
title: Resumable Chat
3-
id: resumable-chat
2+
title: Chat Persistence
3+
id: chat-persistence
44
---
55

6-
Use server persistence when the server should be authoritative for a chat
7-
thread. The client may still keep local UI state, but the durable transcript,
8-
run status, and replayable event log live behind `withPersistence(...)`.
6+
Use chat persistence when the server should be authoritative for a thread. The
7+
client may keep local UI state, but the durable transcript, run status,
8+
replayable event log, and pending user decisions live behind
9+
`withPersistence(...)`.
910

1011
By the end, your endpoint accepts `{ threadId, runId, cursor, resume }`, writes
11-
each streamed chunk to durable storage, and lets the client resume after an
12+
streamed chunks to durable storage, and lets the client resume after an
1213
in-session disconnect or full page reload.
1314

1415
## Install a backend
@@ -51,17 +52,17 @@ export async function POST(request: Request) {
5152
}
5253
```
5354

54-
`withPersistence` loads the stored thread history, saves the resulting
55-
transcript, records run status, and appends every public AG-UI event with a
56-
cursor. When `cursor` is present, the run replays persisted events after that
57-
cursor instead of re-running the adapter.
55+
`withPersistence(...)` loads stored thread history, saves the resulting
56+
transcript, records run status, and appends every public AG-UI event with an
57+
opaque cursor. When `cursor` is present, the run replays persisted events after
58+
that cursor instead of re-running the adapter.
5859

5960
## Wire the client
6061

6162
The chat client forwards the resume fields through its connection adapter. Keep
6263
a stable `threadId` per conversation so a reload returns to the same server
6364
thread. Use `persistence.server` to store the client's latest resume snapshot
64-
under that `threadId` so reloads can continue the same durable run.
65+
under that `threadId`.
6566

6667
```tsx
6768
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
@@ -118,26 +119,27 @@ known `{ threadId, runId, cursor }`. Opt out with `autoResume: false`, or call
118119
`chat.resumeState` contains the active resume identity, or `null` when there is
119120
nothing to continue. `chat.pendingInterrupts` contains the client-side
120121
descriptors needed to answer pending user decisions. `persistence.server`
121-
stores them together and hydrates them on the next client construction. The
122-
server remains authoritative: the stored snapshot only tells the client which
123-
durable run to reconnect to and which pending interrupts it can answer.
122+
stores them together and hydrates them on the next client construction.
124123

125-
## Client message storage is separate
124+
## Choose the controls you need
126125

127-
Client-side chat persistence stores rendered `UIMessage` history in
128-
`localStorage`, IndexedDB, or another browser-side adapter. Server persistence
129-
stores model messages, run records, and replayable public events. You can use
130-
both with `persistence: { client, server }`, but they solve different problems.
131-
See [Chat Persistence](../chat/persistence) for client-only message storage.
126+
Use [Persistence Controls](./controls) when you want the decision table. For
127+
chat, these are the common combinations:
128+
129+
| Goal | Controls |
130+
| --- | --- |
131+
| Browser-only drafts | `persistence.client` on the chat client. |
132+
| Server-owned transcript | `stores.messages` and `features: ['messages']`. |
133+
| Reconnect without re-running the model | `stores.runs`, `stores.publicEvents`, and `durable-replay`. |
134+
| Pending approvals or human input | `interrupts`, which also requires run and public-event stores. |
135+
| Multi-worker resume safety | Add `stores.locks` when a backend supports it. |
132136

133137
## Resume pending decisions
134138

135139
If the server finishes with `RUN_FINISHED.outcome.type === 'interrupt'`, the
136140
thread has a pending user-actionable wait. Resolve those waits with
137141
`chat.resumeInterrupts(...)`; the client forwards them as AG-UI
138-
`RunAgentInput.resume[]` entries on the next request. If the page reloads
139-
before the user answers, `persistence.server` restores the pending interrupts
140-
the client needs to render and resume.
142+
`RunAgentInput.resume[]` entries on the next request.
141143

142144
```ts
143145
import type { UseChatReturn } from '@tanstack/ai-react'
@@ -153,5 +155,22 @@ await chat.resumeInterrupts([
153155
])
154156
```
155157

156-
For approval-specific UI and compatibility details, see
157-
[Interrupts and Approvals](./interrupts-and-approvals).
158+
Normal new input on the same thread is rejected by default while pending
159+
interrupts exist. That keeps the server from accidentally creating a second
160+
conversation branch before the existing decision has been resolved or
161+
cancelled.
162+
163+
Tool approvals are the common UI shape for interrupts. A tool with
164+
`needsApproval: true` can pause the run, surface an approval request, and resume
165+
after the user approves or denies it. For basic approval rendering without
166+
server persistence, see [Tool Approval Flow](../tools/tool-approval).
167+
168+
## Keep client and server persistence separate
169+
170+
Client-side chat persistence stores rendered `UIMessage` history in
171+
`localStorage`, IndexedDB, or another browser-side adapter. Server persistence
172+
stores model messages, run records, interrupts, and replayable public events.
173+
You can use both with `persistence: { client, server }`, but they solve
174+
different problems.
175+
176+
For a narrow client-only page, see [Chat & Streaming Persistence](../chat/persistence).

0 commit comments

Comments
 (0)