Skip to main content

ai-persistence

Durability and state persistence for TanStack AI chats with @tanstack/ai-persistence. Routes to server chat persistence (withPersistence), client persistence (localStorage/IndexedDB), the store contracts, and adapter recipes. Distinguishes delivery durability (resumable streams) from conversation state. Use when conversations must survive reloads, multi-device, approvals, or server restarts — NOT for stream reconnect alone.

معلومات المصدر

المستودع
TanStack/ai
آخر نشاط في المصدر
٦ أكتوبر ٢٠٢٦ في ٠٩:٤١
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٬١٩٩
التفرعات
٣٦٦

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
8 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
ai-persistence
description
Durability and state persistence for TanStack AI chats with @tanstack/ai-persistence. Routes to server chat persistence (withPersistence), client persistence (localStorage/IndexedDB), the store contracts, and adapter recipes. Distinguishes delivery durability (resumable streams) from conversation state. Use when conversations must survive reloads, multi-device, approvals, or server restarts — NOT for stream reconnect alone.
type
core
library
tanstack-ai
library_version
0.0.0
sources
["TanStack/ai:docs/persistence/overview.md","TanStack/ai:docs/persistence/chat-persistence.md","TanStack/ai:docs/persistence/client-persistence.md","TanStack/ai:docs/persistence/controls.md","TanStack/ai:docs/persistence/build-your-own-adapter.md"]
# TanStack AI Persistence > Builds on the `ai-core` skill in `@tanstack/ai`, and usually > `ai-core/chat-experience`. TanStack AI splits **delivery durability** from **state persistence**. They share no code and solve different problems. | Layer | Answers | Package / API | | ----------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------- | | **Delivery durability** | Reconnect to a stream still running | `memoryStream` / `@tanstack/ai-durable-stream` on the response; see resumable streams docs | | **State persistence** | What is the conversation, later? | Client `persistence` on `useChat` + server `withPersistence` from `@tanstack/ai-persistence` | A replayable stream is **not** a saved conversation. A saved conversation is **not** a live stream. Production apps often use both. ## Persistence is a contract, not a database `@tanstack/ai-persistence` ships the **store interfaces**, the middleware that drives them, an in-memory reference backend, and a conformance testkit. It does **not** ship a backend for your database, and you do not need one: implement the stores against whatever you already run — Postgres, SQLite, D1, Mongo — and hand the result to `withPersistence`. The core never inspects your tables. | Ships in the package | What it is | | --------------------------------------------------------------------------- | ----------------------------------------------------------- | | `MessageStore` / `RunStore` / `InterruptStore` / `MetadataStore` | The four **chat** state contracts | | `GenerationRunStore` / `ArtifactStore` / `BlobStore` | The **generation** contracts (job lifecycle + bytes) | | `withPersistence` / `withGenerationPersistence` | Chat + generation middleware | | `memoryPersistence()` | In-process reference backend, all seven stores (dev, tests) | | `reconstructChat` / `reconstructGeneration` | Server hydrate route helpers (chat / generation) | | `retrieveArtifact` / `retrieveBlob` / `resolveArtifactBlobKey` | Serve persisted generation-media bytes back | | `LockStore` / `withLocks` / `InMemoryLockStore` (from `@tanstack/ai/locks`) | Coordination, **not** this package — see ai-core/locks | | `@tanstack/ai-persistence/testkit` | `runPersistenceConformance` gate (chat state stores) | **Chat vs generation stores.** Chat persistence keys on `threadId` and uses `messages` + optional `runs` / `interrupts` / `metadata`. Generation persistence keys on its own `runId` and uses `generationRuns` (required by `withGenerationPersistence`) plus an optional `artifacts` + `blobs` **pair** — provide both or neither — to store the generated media bytes at blob key `artifacts/<runId>/<artifactId>`. A generation run's identity is its own `runId`, but `threadId` is **required** on the record: it is the stable slot successive runs fill, and `findLatestForThread` — the only query that hydrates a run — keys on it. To build the R2/D1-backed byte stores for a Worker, see **ai-persistence/build-cloudflare-artifact-store**. **Where bytes land.** Default blob key is `artifacts/<runId>/<artifactId>`. Pass `storageKey` to `withGenerationPersistence` for your own folder structure — it receives `{ artifactId, runId, threadId, role, activity, path, mimeType, name }` and returns the key. Server-side only (a browser-supplied key is path traversal + cross-tenant writes). The resolved key is recorded on `ArtifactRecord.blobKey` because it is no longer derivable; read through `resolveArtifactBlobKey(record)`, never by recomputing. Records predating `blobKey` fall back to the default convention — which is why that convention can never be changed retroactively. A non-unique key overwrites, so include `artifactId` unless that is intended. **Byte storage stores generated output, not prompt URLs.** Provider result URLs expire, so they are downloaded and kept. Prompt media sent as base64 (`source: { type: 'data' }`) is stored too. Prompt media sent as a **URL** is NOT fetched — that URL is caller-supplied, so downloading it server-side is an SSRF vector, and the bytes are redundant. Apps that genuinely need a durable copy opt in with `allowInputUrl`, a predicate so the check can't be skipped: `allowInputUrl: ({ url }) => url.hostname.endsWith('.cdn.example.com')`. Never suggest `() => true`. All artifact fetches are http/https-only, timed out (`artifactFetchTimeoutMs`) and size-capped (`maxArtifactBytes`); input fetches also block loopback/private/link-local hosts and refuse redirects. `artifactFetch` injects the `fetch`, for routing through an egress-restricted proxy. Two related route-level rules: a `GET` that serves artifact bytes by id MUST authorize the caller against `ArtifactRecord.threadId` before serving (404, not 403, so valid ids aren't confirmed), and `reconstructGeneration` MUST be given `authorize` on any multi-user route. Both take ids straight from the caller. Portable sandbox snapshots use the same `messages`, `artifacts`, and `blobs` stores. Their artifact reader checks the checkpoint thread, but it does not authenticate a caller. Authorize the thread before any route reads a snapshot artifact. The snapshot checkpoint store also needs atomic append and fork operations. A SQLite adapter must write a checkpoint, its head, and blob reference counts in one transaction. ## Sub-skills | Need to... | Read | | ----------------------------------------------- | ----------------------------------------------------- | | Wire server-side chat history, runs, interrupts | ai-persistence/server/SKILL.md | | Survive reloads in the browser | ai-core/client-persistence/SKILL.md in `@tanstack/ai` | | Implement the store interfaces for your DB | ai-persistence/stores/SKILL.md | | Multi-instance locks (separate from state) | ai-core/locks/SKILL.md in `@tanstack/ai` | Adding persistence to an app? Pick the recipe that matches what it already runs — each one writes a single `chat-persistence.ts` against the app's existing database client and schema: | The app runs... | Read | | ---------------------------------------------------- | ------------------------------------------------------- | | Drizzle ORM (SQLite / Postgres / MySQL) | ai-persistence/build-drizzle-adapter/SKILL.md | | Prisma | ai-persistence/build-prisma-adapter/SKILL.md | | Cloudflare Workers + D1 (± Durable Object locks) | ai-persistence/build-cloudflare-adapter/SKILL.md | | Cloudflare Workers + R2/D1 for generated media bytes | ai-persistence/build-cloudflare-artifact-store/SKILL.md | | Anything else — raw `pg`, Kysely, SQLite, Mongo | ai-persistence/build-custom-adapter/SKILL.md | ## State persistence has two halves | Half | Stores | Survives | Typical use | | ---------- | ----------------------------------------------- | -------------------------------- | ---------------------------------------- | | **Client** | transcript ± resume pointer in browser storage | reload / tab close (per browser) | SPA restore, offline-first | | **Server** | messages, runs, interrupts, metadata in your DB | restart + multi-device | authoritative history, durable approvals | They are independent. Use either alone or both. ## Identity: `threadId` and `Scope` Server stores key on **`threadId`** (same as `chat({ threadId })` / `ChatMiddlewareContext.threadId` / `Scope.threadId` from `@tanstack/ai`). - Store methods take bare `threadId` strings for adapter simplicity. - Multi-user isolation is **your** job: derive `userId` / `tenantId` from session server-side; authorize before load/save / `reconstructChat`. - Never treat a client-supplied thread id alone as ownership — ids are guessable. ## Authoritative-history contract When both halves run, ownership per turn is decided by request `messages`: | Client sends | Meaning | On finish | | ------------------------ | --------------------------------- | ----------------------------------- | | **Non-empty** `messages` | Full transcript (source of truth) | Server **overwrites** stored thread | | **Empty** `messages` | Continue from server copy | Server **loads** stored thread | Never post a delta as `messages` — that wipes history down to the delta. **Client-authoritative:** always send full transcript; browser is truth, server mirrors. **Server-authoritative:** send empty `messages` (or hydrate via server load); server is truth, multi-device works. ## Recommended production stack 1. **Client:** `persistence: true` — server-authoritative, no client cache. 2. **Server:** `withPersistence(backend)` — messages + runs + interrupts. 3. **Route:** delivery durability if mid-stream reconnect matters. 4. **Optional:** `withLocks(distributedLockStore)` from `@tanstack/ai/locks` when other middleware needs multi-instance coordination (not part of the state bag). ## Minimal end-to-end sketch **Server** ```ts import { chat, chatParamsFromRequest, toServerSentEventsResponse, } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { withPersistence } from '@tanstack/ai-persistence' // Your adapter — see ai-persistence/stores. 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 (server-authoritative)** ```tsx import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' function Chat({ threadId }: { threadId: string }) { const { messages, sendMessage } = useChat({ threadId, connection: fetchServerSentEvents('/api/chat'), persistence: true, }) // ... } ``` With `persistence: true`, the client caches nothing and hydrates the transcript from the server on mount (thread id is the key). Pair with a server load path such as `reconstructChat` for the GET. ## Critical rules 1. **Not Vercel AI SDK.** Persistence is `@tanstack/ai-persistence` + middleware, not Vercel `useChat` storage hacks. 2. **`saveThread` is full overwrite**, never append. 3. **`createOrResume` is insert-if-absent** for the same `runId`. 4. **Interrupt `create` is insert-if-absent** — never clobber resolved → pending.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub