diff --git a/.changeset/persistence-orm-locks-naming-collation.md b/.changeset/persistence-orm-locks-naming-collation.md new file mode 100644 index 000000000..b8d9f16da --- /dev/null +++ b/.changeset/persistence-orm-locks-naming-collation.md @@ -0,0 +1,20 @@ +--- +'@tanstack/ai-persistence-drizzle': patch +'@tanstack/ai-persistence-prisma': patch +'@tanstack/ai-persistence-cloudflare': patch +--- + +Persistence ORM backends: safer locks and clearer SQLite naming. + +- `drizzlePersistence` and `prismaPersistence` no longer return a `locks` store. + Bundling an `InMemoryLockStore` silently handed multi-instance deployments a + lock that does not lock across instances. Consumers that need a lock (e.g. + `withSandbox`) transparently fall back to an in-process lock; use a + distributed backend such as the Cloudflare Durable Object lock for + cross-instance locking. +- `@tanstack/ai-persistence-drizzle` renames `TanstackAiSchema` → + `TanstackAiSqliteSchema` and `DrizzleDb` → `DrizzleSqliteDb` to make the + SQLite-only surface explicit. The old names remain as `@deprecated` aliases. +- `metadata.set` with a nullish value now throws a clear error on both SQL + backends (a NOT NULL JSON column cannot store `undefined`/`null`) instead of a + cryptic driver failure — use `delete` to clear a value. diff --git a/.changeset/persistence-v2.md b/.changeset/persistence-v2.md new file mode 100644 index 000000000..8c12f8161 --- /dev/null +++ b/.changeset/persistence-v2.md @@ -0,0 +1,79 @@ +--- +'@tanstack/ai': minor +'@tanstack/ai-client': minor +'@tanstack/ai-persistence': patch +'@tanstack/ai-persistence-cloudflare': patch +'@tanstack/ai-persistence-drizzle': patch +'@tanstack/ai-persistence-prisma': patch +'@tanstack/ai-sandbox': minor +'@tanstack/ai-event-client': patch +'@tanstack/ai-angular': minor +'@tanstack/ai-preact': minor +'@tanstack/ai-react': minor +'@tanstack/ai-solid': minor +'@tanstack/ai-svelte': minor +'@tanstack/ai-vue': minor +--- + +Persistence v2: durable state via middleware and a pluggable store contract. + +State durability lives on the middleware layer as `withChatPersistence` / +`withGenerationPersistence`, backed by a store contract (messages, runs, +interrupts, metadata, locks). The `@tanstack/ai-persistence` package ships a +state-only middleware, an in-core memory backend, and a shared conformance +suite; `@tanstack/ai-persistence-drizzle` and `@tanstack/ai-persistence-prisma` +provide ORM state backends, while `@tanstack/ai-persistence-cloudflare` +provides first-class D1 and Durable Object backed stores. + +Durable media artifact/blob storage (artifact metadata plus blob bytes, and +the sandbox workspace-file checkpoints built on it) is split out to a +follow-up release; persisted generation results reference provider media URLs, +which may expire. + +Replaying a disconnected/reloaded stream (resumable streams) is a separate +transport-level feature and ships independently — the persistence middleware +owns zero delivery-event storage. + +The home-grown delivery/event-log subsystem is removed: the in-band `cursor` on +`StreamChunk`, the `cursor` param on `chat()`, the public/internal event stores, +the `ResumeSource` seam, cursor-replay client machinery, and the deprecated +approval-store shim are all deleted (this feature is unreleased — no back-compat +shims). Interrupt resume (approvals / client-tool results via +`RunAgentInput.resume[]`) is preserved as state durability, and the client +(`@tanstack/ai-client`) plus every framework binding (`@tanstack/ai-react`, +`-solid`, `-vue`, `-svelte`, `-angular`, `-preact`) switch to interrupt-only +resume. + +The delivery-store / SQL-driver packages that only existed to back the removed +event log — `@tanstack/ai-persistence-sql`, +`@tanstack/ai-persistence-sqlite`, and `@tanstack/ai-persistence-postgres` — are +removed. They were never published (part of this same unreleased persistence +work), so no npm tombstone release is needed. + +Generation events no longer expose transport cursors; they retain their thread +and run identifiers for correlation. + +**Breaking (`@tanstack/ai-client` and every framework binding):** the +`persistence` option changed shape. It was a single flat message-storage adapter +and is now a `{ client?, server? }` object — `client` stores the rendered +`UIMessage[]` (keyed by the chat `id`) and `server` stores the resume snapshot +`{ resumeState, pendingInterrupts }` (keyed by `threadId`). This is a hard +rename with no bridging union, so a flat adapter no longer type-checks. Migrate +by moving your existing message adapter under `client`: + +```ts +// Before +useChat({ + connection, + persistence: localStoragePersistence(), +}) + +// After +useChat({ + connection, + persistence: { + client: localStoragePersistence(), // messages (chat id) + // server: localStoragePersistence(), // optional: resume snapshot (threadId) + }, +}) +``` diff --git a/docs/api/ai-angular.md b/docs/api/ai-angular.md index 291ad2cc5..00bf98619 100644 --- a/docs/api/ai-angular.md +++ b/docs/api/ai-angular.md @@ -90,7 +90,13 @@ import type { DeepPartial, } from "@tanstack/ai-angular"; import type { ModelMessage, InferSchemaType } from "@tanstack/ai/client"; -import type { ChatClientState, ConnectionStatus } from "@tanstack/ai-client"; +import type { + ChatClientState, + ConnectionStatus, + ChatResumeState, + ChatPendingInterrupt, + RunAgentResumeItem, +} from "@tanstack/ai-client"; type TSchema = any; interface InjectChatResult { @@ -118,6 +124,15 @@ interface InjectChatResult { isSubscribed: Signal; connectionStatus: Signal; sessionGenerating: Signal; + // Correlation ids `{ threadId, runId }` for the interrupted run, or null. + resumeState: Signal; + // Interrupts currently awaiting a resume response. + pendingInterrupts: Signal; + // Resolve pending interrupts by sending AG-UI resume entries. + resumeInterrupts: ( + resume: RunAgentResumeItem[], + state?: ChatResumeState, + ) => Promise; // Only present when outputSchema is supplied: partial: Signal>>; final: Signal | null>; @@ -380,9 +395,12 @@ export class CustomGenerationComponent { } ``` -**Options:** `connection?`, `fetcher?`, `id?`, `body?` (reactive), `devtools?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` +**Options:** `connection?`, `fetcher?`, `id?`, `body?` (reactive), `persistence?`, `initialResumeSnapshot?`, `devtools?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` + +**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `resumeSnapshot`, `resumeState` (reactive), `pendingArtifacts`, `resultArtifacts` — all reactive state is a read-only `Signal`. -**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset` — all reactive state is a read-only `Signal`. +For the end-to-end persistence setup, see +[Generation Persistence](../persistence/generation-persistence). ### `injectGenerateImage(options)` diff --git a/docs/api/ai-react.md b/docs/api/ai-react.md index 01eb1587b..f2e5c7bd0 100644 --- a/docs/api/ai-react.md +++ b/docs/api/ai-react.md @@ -85,7 +85,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: - `initialMessages?` - Initial messages array - `id?` - Unique identifier for this chat instance - `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted -- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`) +- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.5' }`) - `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire - `context?` - Typed client-local runtime context passed to client tool implementations. This value is not serialized to the server - `onResponse?` - Callback when response is received @@ -101,6 +101,11 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: ```typescript import type { UIMessage } from "@tanstack/ai-react"; import type { ModelMessage } from "@tanstack/ai"; +import type { + ChatResumeState, + ChatPendingInterrupt, + RunAgentResumeItem, +} from "@tanstack/ai-client"; interface UseChatReturn { messages: UIMessage[]; @@ -123,6 +128,15 @@ interface UseChatReturn { error: Error | undefined; setMessages: (messages: UIMessage[]) => void; clear: () => void; + // Correlation ids `{ threadId, runId }` for the interrupted run, or null. + resumeState: ChatResumeState | null; + // Interrupts currently awaiting a resume response. + pendingInterrupts: ChatPendingInterrupt[]; + // Resolve pending interrupts by sending AG-UI resume entries. + resumeInterrupts: ( + resume: RunAgentResumeItem[], + state?: ChatResumeState, + ) => Promise; } ``` @@ -158,6 +172,24 @@ function that resolves per request. For error narrowing, import `UnsupportedResponseStreamError` and `StreamTruncatedError` from `@tanstack/ai-client`. +## Generation Hooks + +React also exports `useGeneration`, `useGenerateImage`, `useGenerateAudio`, +`useGenerateSpeech`, `useTranscription`, `useSummarize`, and +`useGenerateVideo`. + +Generation hook options include `connection` or `fetcher`, `id`, `body`, +`persistence`, `initialResumeSnapshot`, `devtools`, `onResult`, `onError`, +`onProgress`, and `onChunk`. + +Generation hook returns include `generate`, `result`, `isLoading`, `error`, +`status`, `stop`, `reset`, `resumeSnapshot`, `resumeState`, +`pendingArtifacts`, and `resultArtifacts`. Video generation also returns +`jobId` and `videoStatus`. + +For the end-to-end persistence setup, see +[Generation Persistence](../persistence/generation-persistence). + ## Example: Basic Chat ```tsx diff --git a/docs/api/ai-solid.md b/docs/api/ai-solid.md index 46de176a9..ed3b6e01e 100644 --- a/docs/api/ai-solid.md +++ b/docs/api/ai-solid.md @@ -76,7 +76,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: - `initialMessages?` - Initial messages array - `id?` - Unique identifier for this chat instance - `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted -- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`) +- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.5' }`) - `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire - `context?` - Typed client-local runtime context passed to client tool implementations. This value is not serialized to the server - `onResponse?` - Callback when response is received @@ -93,6 +93,11 @@ Extends `ChatClientOptions` from `@tanstack/ai-client`: import type { Accessor } from "solid-js"; import type { UIMessage } from "@tanstack/ai-solid"; import type { ModelMessage } from "@tanstack/ai/client"; +import type { + ChatResumeState, + ChatPendingInterrupt, + RunAgentResumeItem, +} from "@tanstack/ai-client"; interface UseChatReturn { messages: Accessor; @@ -115,11 +120,39 @@ interface UseChatReturn { error: Accessor; setMessages: (messages: UIMessage[]) => void; clear: () => void; + // Correlation ids `{ threadId, runId }` for the interrupted run, or null. + resumeState: Accessor; + // Interrupts currently awaiting a resume response. + pendingInterrupts: Accessor; + // Resolve pending interrupts by sending AG-UI resume entries. + resumeInterrupts: ( + resume: RunAgentResumeItem[], + state?: ChatResumeState, + ) => Promise; } ``` **Note:** Unlike React, `messages`, `isLoading`, and `error` are SolidJS `Accessor` functions, so you need to call them to get their values (e.g., `messages()` instead of just `messages`). +## Generation Hooks + +Solid also exports `useGeneration`, `useGenerateImage`, `useGenerateAudio`, +`useGenerateSpeech`, `useTranscription`, `useSummarize`, and +`useGenerateVideo`. + +Generation hook options include `connection` or `fetcher`, `id`, `body`, +`persistence`, `initialResumeSnapshot`, `devtools`, `onResult`, `onError`, +`onProgress`, and `onChunk`. + +Generation hook returns include `generate`, `result`, `isLoading`, `error`, +`status`, `stop`, `reset`, `resumeSnapshot`, `resumeState`, +`pendingArtifacts`, and `resultArtifacts`. Reactive values are Solid accessors, +so read `resumeState()` and `pendingArtifacts()`. Video generation also returns +`jobId` and `videoStatus`. + +For the end-to-end persistence setup, see +[Generation Persistence](../persistence/generation-persistence). + ## Connection Adapters Re-exported from `@tanstack/ai-client` for convenience: diff --git a/docs/api/ai-svelte.md b/docs/api/ai-svelte.md index 8e7768b87..a2ed59f3f 100644 --- a/docs/api/ai-svelte.md +++ b/docs/api/ai-svelte.md @@ -74,7 +74,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client` (minus internal state cal - `initialMessages?` - Initial messages array - `id?` - Unique identifier for this chat instance - `threadId?` - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted -- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-4o' }`) +- `forwardedProps?` - Arbitrary client-controlled JSON forwarded to the server in the AG-UI `RunAgentInput.forwardedProps` field (e.g., `{ provider: 'openai', model: 'gpt-5.5' }`) - `body?` - **Deprecated.** Use `forwardedProps` instead. Still works for backward compatibility; values are merged into `forwardedProps` on the wire - `context?` - Typed client-local runtime context passed to client tool implementations. This value is not serialized to the server - `live?` - Enable live subscription mode (subscribes on creation) @@ -90,7 +90,7 @@ Extends `ChatClientOptions` from `@tanstack/ai-client` (minus internal state cal ### Returns ```typescript -import type { UIMessage, MultimodalContent, ChatClientState, ConnectionStatus } from "@tanstack/ai-client"; +import type { UIMessage, MultimodalContent, ChatClientState, ConnectionStatus, ChatResumeState, ChatPendingInterrupt, RunAgentResumeItem } from "@tanstack/ai-client"; import type { ModelMessage } from "@tanstack/ai"; interface CreateChatReturn { @@ -118,6 +118,15 @@ interface CreateChatReturn { readonly sessionGenerating: boolean; setMessages: (messages: UIMessage[]) => void; clear: () => void; + // Correlation ids `{ threadId, runId }` for the interrupted run, or null. + readonly resumeState: ChatResumeState | null; + // Interrupts currently awaiting a resume response. + readonly pendingInterrupts: ChatPendingInterrupt[]; + // Resolve pending interrupts by sending AG-UI resume entries. + resumeInterrupts: ( + resume: RunAgentResumeItem[], + state?: ChatResumeState, + ) => Promise; /** @deprecated Use `updateForwardedProps` instead. */ updateBody: (body: Record) => void; updateForwardedProps: (forwardedProps: Record) => void; @@ -297,9 +306,12 @@ const gen = createGeneration({ // gen.result, gen.isLoading, gen.error, gen.status ``` -**Options:** `connection?`, `fetcher?`, `id?`, `body?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` +**Options:** `connection?`, `fetcher?`, `id?`, `body?`, `persistence?`, `initialResumeSnapshot?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` -**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `updateBody` -- all state properties are reactive getters. +**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `updateBody`, `dispose`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` -- all state properties are reactive getters. + +For the end-to-end persistence setup, see +[Generation Persistence](../persistence/generation-persistence). ### `createGenerateImage(options)` diff --git a/docs/api/ai-vue.md b/docs/api/ai-vue.md index 06542576d..69a5dc93f 100644 --- a/docs/api/ai-vue.md +++ b/docs/api/ai-vue.md @@ -95,6 +95,9 @@ import type { MultimodalContent, ChatClientState, ConnectionStatus, + ChatResumeState, + ChatPendingInterrupt, + RunAgentResumeItem, } from "@tanstack/ai-client"; interface UseChatReturn { @@ -122,6 +125,15 @@ interface UseChatReturn { sessionGenerating: DeepReadonly>; setMessages: (messages: UIMessage[]) => void; clear: () => void; + // Correlation ids `{ threadId, runId }` for the interrupted run, or null. + resumeState: DeepReadonly>; + // Interrupts currently awaiting a resume response. + pendingInterrupts: DeepReadonly>; + // Resolve pending interrupts by sending AG-UI resume entries. + resumeInterrupts: ( + resume: RunAgentResumeItem[], + state?: ChatResumeState, + ) => Promise; } ``` @@ -314,9 +326,12 @@ const { generate, result, isLoading, error, status, stop, reset } = }); ``` -**Options:** `connection?`, `fetcher?`, `id?`, `body?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` +**Options:** `connection?`, `fetcher?`, `id?`, `body?`, `persistence?`, `initialResumeSnapshot?`, `onResult?`, `onError?`, `onProgress?`, `onChunk?` -**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset` -- all reactive state is `DeepReadonly>`. +**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` -- all reactive state is `DeepReadonly>`. Read refs with `.value` in `