Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ai-persistence-readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-persistence': patch
---

Add a package README: install, `withPersistence` on the server, `persistence: true` on the client, `reconstructChat` for reloads mid-answer, the smallest custom adapter, the `testkit` conformance suite, and links to the docs.
154 changes: 154 additions & 0 deletions packages/ai-persistence/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<div align="center">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://tanstack.com/api/readme/ai.png?theme=dark"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://tanstack.com/api/readme/ai.png"
/>
<img
src="https://tanstack.com/api/readme/ai.png"
alt="TanStack AI"
width="900"
/>
</picture>
</div>

<br />

# @tanstack/ai-persistence

Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks

A conversation that only lives in memory is gone on reload and absent on a second device. This package stores it in your own database: one middleware on the server writes the transcript β€” and, with the matching stores configured, run status and pending approvals β€” through an adapter you define, and the client asks the server for the thread on mount. The client half ships in the framework package you already use (`@tanstack/ai-react`, `-vue`, `-solid`, `-svelte`, `-angular`, or `@tanstack/ai-client`).

## Installation

```bash
npm install @tanstack/ai-persistence
# or
pnpm add @tanstack/ai-persistence
# or
yarn add @tanstack/ai-persistence
```

## Usage

### Server: store the conversation

`withPersistence` writes the transcript into your `messages` store, plus run status and pending approvals when the adapter also provides `runs` and `interrupts`. Start with `memoryPersistence()` for local development and swap in your own adapter later:

```typescript
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { withPersistence } from '@tanstack/ai-persistence'
import { persistence } from './persistence'

export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream)
}
```

### Client: bring it back

`persistence: true` puts the server in charge: the browser caches nothing and fetches the thread on mount. Best for multi-user and multi-device apps.

```tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'

function Chat() {
const { messages, sendMessage } = useChat({
threadId: 'support-chat',
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
})
return <button onClick={() => sendMessage('hi')}>{messages.length}</button>
}
```

Prefer the browser to own the history? Pass a storage adapter instead β€” `localStoragePersistence()`, `sessionStoragePersistence()`, or `indexedDBPersistence()` β€” and no server store is needed.

### Survive a reload mid-answer

Add a `GET` on the same route. `reconstructChat` returns the stored thread plus a cursor to any run still generating; `useChat` tails that run so the reply finishes in place.

```typescript
import { chatParamsFromRequest } from '@tanstack/ai'
import { reconstructChat } from '@tanstack/ai-persistence'
import { persistence } from './persistence'

export function GET(request: Request) {
return reconstructChat(persistence, request, {
// WITHOUT this, anyone who guesses a thread id gets the whole transcript.
authorize: async (threadId, req) => ownsThread(req, threadId),
})
}
```

## Build your own adapter

An adapter is a plain object of store functions. The core never looks at your tables, so the schema stays yours. One store, `messages`, is enough for `withPersistence`:

```typescript
import {
defineAIPersistence,
defineMessageStore,
} from '@tanstack/ai-persistence'
import { db } from './db'

export const persistence = defineAIPersistence({
stores: {
messages: defineMessageStore({
// Return [] for a thread that was never saved, never null.
loadThread: (threadId) => db.threads.messages(threadId),
// The full transcript, not a delta. Overwrite what you had.
saveThread: (threadId, messages) => db.threads.save(threadId, messages),
}),
},
})
```

The other stores are optional and add capabilities as you provide them: `runs`, `interrupts`, and `metadata` for chat state; `generationRuns`, `artifacts`, and `blobs` for image, video, speech, and transcription runs. `composePersistence` layers overrides on top of a base backend.

### Prove it with the conformance suite

The same suite every packaged backend runs is shipped for yours:

```typescript
import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit'
import { sqlitePersistence } from './sqlite-persistence'

runPersistenceConformance('my sqlite adapter', () =>
sqlitePersistence({ url: ':memory:', migrate: true }),
)
```

Stores you do not provide go in `skip`. The testkit is a Vitest suite; `vitest` is an optional peer dependency, so install it in your project before importing `@tanstack/ai-persistence/testkit`.

## Documentation

- [Overview](https://tanstack.com/ai/latest/docs/persistence/overview): the three steps, and which setup you want
- [Chat persistence](https://tanstack.com/ai/latest/docs/persistence/chat-persistence): the server middleware in full, including durable interrupts
- [Client persistence](https://tanstack.com/ai/latest/docs/persistence/client-persistence): the modes, storage backends, and what a reload restores
- [Build your own adapter](https://tanstack.com/ai/latest/docs/persistence/build-your-own-adapter)
- [Controls](https://tanstack.com/ai/latest/docs/persistence/controls): compose backends per store
- [Store reference](https://tanstack.com/ai/latest/docs/persistence/store-reference): every store's methods and how the records relate

## License

MIT
Loading