Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b91d1d4
feat(ai): cursor + chat() resume seam, shared locks capability, CUSTO…
AlemTuzlak Jun 18, 2026
aacddbf
feat(persistence): @tanstack/ai-persistence + SQL core + backends + s…
AlemTuzlak Jun 18, 2026
d2462d6
feat(ai-client): in-session auto-resume
AlemTuzlak Jun 18, 2026
fb45b01
docs(persistence): overview page, ai-persistence skill, changeset, kn…
AlemTuzlak Jun 18, 2026
b141e4b
ci: apply automated fixes
autofix-ci[bot] Jun 18, 2026
d0d8672
feat: finalize ai persistence
AlemTuzlak Jul 2, 2026
fc45b4c
feat: add mysql persistence chat example
AlemTuzlak Jul 3, 2026
3a75f11
fix: quote mysql usage column
AlemTuzlak Jul 3, 2026
0d661c8
feat: add cloudflare r2 artifact persistence
AlemTuzlak Jul 3, 2026
253a2eb
feat: add cloudflare blob persistence primitives
AlemTuzlak Jul 3, 2026
a46c9bc
docs: split persistence guide by scenario
AlemTuzlak Jul 3, 2026
c864d51
feat(persistence): client resume + framework use-chat persistence upd…
AlemTuzlak Jul 3, 2026
17e9b93
feat(sandbox): consume persistence without bridge
AlemTuzlak Jul 6, 2026
8d61c54
feat(persistence): add orm migration CLIs
AlemTuzlak Jul 6, 2026
a4470f5
Merge remote-tracking branch 'origin/main' into feat/persistence
AlemTuzlak Jul 6, 2026
39ce14a
feat(sandbox): persist managed workspace files
AlemTuzlak Jul 6, 2026
6ce5403
fix(ai): tolerate seed messages without parts
AlemTuzlak Jul 6, 2026
276faea
docs(persistence): type-check persistence examples
AlemTuzlak Jul 6, 2026
173fcc5
fix(client): preserve pending interrupts on mount
AlemTuzlak Jul 6, 2026
c94d5b8
ci: apply automated fixes
autofix-ci[bot] Jul 6, 2026
356188b
feat: add generation persistence and resumability
AlemTuzlak Jul 7, 2026
50b2314
ci: apply automated fixes
autofix-ci[bot] Jul 7, 2026
e3d928d
Merge remote-tracking branch 'origin/main' into feat/persistence
AlemTuzlak Jul 8, 2026
e46b990
Merge remote-tracking branch 'origin/feat/persistence' into feat/pers…
AlemTuzlak Jul 8, 2026
174eeb3
fix: address persistence ci failures
AlemTuzlak Jul 8, 2026
52e2b2e
ci: apply automated fixes
autofix-ci[bot] Jul 8, 2026
0271f9a
fix(persistence): default migrations to opt-in
AlemTuzlak Jul 8, 2026
c6cccfa
feat(persistence): add SQL migration CLI docs
AlemTuzlak Jul 8, 2026
9981da4
docs(persistence): reorganize persistence guides
AlemTuzlak Jul 8, 2026
d580018
fix(persistence): document internals and r2 prefixes
AlemTuzlak Jul 8, 2026
bbf2d2a
Use shared base64 utils
tombeckenham Jul 9, 2026
d681407
feat(persistence): split chat and generation middleware
tombeckenham Jul 9, 2026
39ea98f
ci: apply automated fixes
autofix-ci[bot] Jul 9, 2026
d5e8af7
feat: split persistence into state (middleware) + delivery (transport…
AlemTuzlak Jul 9, 2026
f9db0af
refactor(ai)!: group batch under durability option ({ adapter, batch })
AlemTuzlak Jul 9, 2026
d4361b1
Merge origin/main into feat/persistence
AlemTuzlak Jul 10, 2026
61026d5
feat: add persistence and delivery durability
AlemTuzlak Jul 10, 2026
659464a
feat(ai-persistence-drizzle): user-owned schemas via injection + sche…
tombeckenham Jul 14, 2026
004180c
feat(ai-persistence-prisma): renameable models via delegate mapping
tombeckenham Jul 15, 2026
090f86d
refactor: split resumable streams (delivery durability) out to #955
tombeckenham Jul 17, 2026
d2b6202
docs: review accuracy sweep + green kiira gate
tombeckenham Jul 18, 2026
c15285b
fix: address multi-agent review findings across persistence stack
tombeckenham Jul 18, 2026
f2483b3
refactor: split artifact/blob storage out to feat/persistence-media
tombeckenham Jul 18, 2026
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
20 changes: 20 additions & 0 deletions .changeset/persistence-orm-locks-naming-collation.md
Original file line number Diff line number Diff line change
@@ -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.
79 changes: 79 additions & 0 deletions .changeset/persistence-v2.md
Original file line number Diff line number Diff line change
@@ -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)
},
})
```
24 changes: 21 additions & 3 deletions docs/api/ai-angular.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -118,6 +124,15 @@ interface InjectChatResult {
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
// Correlation ids `{ threadId, runId }` for the interrupted run, or null.
resumeState: Signal<ChatResumeState | null>;
// Interrupts currently awaiting a resume response.
pendingInterrupts: Signal<ChatPendingInterrupt[]>;
// Resolve pending interrupts by sending AG-UI resume entries.
resumeInterrupts: (
resume: RunAgentResumeItem[],
state?: ChatResumeState,
) => Promise<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
Expand Down Expand Up @@ -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<T>`.

**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset` — all reactive state is a read-only `Signal<T>`.
For the end-to-end persistence setup, see
[Generation Persistence](../persistence/generation-persistence).

### `injectGenerateImage(options)`

Expand Down
34 changes: 33 additions & 1 deletion docs/api/ai-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[];
Expand All @@ -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<boolean>;
}
```

Expand Down Expand Up @@ -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
Expand Down
35 changes: 34 additions & 1 deletion docs/api/ai-solid.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<UIMessage[]>;
Expand All @@ -115,11 +120,39 @@ interface UseChatReturn {
error: Accessor<Error | undefined>;
setMessages: (messages: UIMessage[]) => void;
clear: () => void;
// Correlation ids `{ threadId, runId }` for the interrupted run, or null.
resumeState: Accessor<ChatResumeState | null>;
// Interrupts currently awaiting a resume response.
pendingInterrupts: Accessor<ChatPendingInterrupt[]>;
// Resolve pending interrupts by sending AG-UI resume entries.
resumeInterrupts: (
resume: RunAgentResumeItem[],
state?: ChatResumeState,
) => Promise<boolean>;
}
```

**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:
Expand Down
20 changes: 16 additions & 4 deletions docs/api/ai-svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<TContext = unknown> {
Expand Down Expand Up @@ -118,6 +118,15 @@ interface CreateChatReturn<TContext = unknown> {
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<boolean>;
/** @deprecated Use `updateForwardedProps` instead. */
updateBody: (body: Record<string, any>) => void;
updateForwardedProps: (forwardedProps: Record<string, any>) => void;
Expand Down Expand Up @@ -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)`

Expand Down
19 changes: 17 additions & 2 deletions docs/api/ai-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ import type {
MultimodalContent,
ChatClientState,
ConnectionStatus,
ChatResumeState,
ChatPendingInterrupt,
RunAgentResumeItem,
} from "@tanstack/ai-client";

interface UseChatReturn {
Expand Down Expand Up @@ -122,6 +125,15 @@ interface UseChatReturn {
sessionGenerating: DeepReadonly<ShallowRef<boolean>>;
setMessages: (messages: UIMessage[]) => void;
clear: () => void;
// Correlation ids `{ threadId, runId }` for the interrupted run, or null.
resumeState: DeepReadonly<ShallowRef<ChatResumeState | null>>;
// Interrupts currently awaiting a resume response.
pendingInterrupts: DeepReadonly<ShallowRef<ChatPendingInterrupt[]>>;
// Resolve pending interrupts by sending AG-UI resume entries.
resumeInterrupts: (
resume: RunAgentResumeItem[],
state?: ChatResumeState,
) => Promise<boolean>;
}
```

Expand Down Expand Up @@ -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<ShallowRef<T>>`.
**Returns:** `generate`, `result`, `isLoading`, `error`, `status`, `stop`, `reset`, `resumeSnapshot`, `resumeState`, `pendingArtifacts`, `resultArtifacts` -- all reactive state is `DeepReadonly<ShallowRef<T>>`. Read refs with `.value` in `<script setup>`.

For the end-to-end persistence setup, see
[Generation Persistence](../persistence/generation-persistence).

### `useGenerateImage(options)`

Expand Down
Loading