@tanstack/ai 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/activities/chat/adapter.js +23 -16
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/agent-loop-strategies.d.ts +5 -36
- package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
- package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
- package/dist/esm/activities/chat/cancel.d.ts +40 -0
- package/dist/esm/activities/chat/cancel.js +54 -0
- package/dist/esm/activities/chat/cancel.js.map +1 -0
- package/dist/esm/activities/chat/index.d.ts +28 -21
- package/dist/esm/activities/chat/index.js +2100 -1813
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
- package/dist/esm/activities/chat/mcp/manager.js +90 -77
- package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
- package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
- package/dist/esm/activities/chat/messages.js +397 -346
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.js +17 -15
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
- package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
- package/dist/esm/activities/chat/middleware/compose.js +623 -531
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.js +12 -5
- package/dist/esm/activities/chat/middleware/define.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
- package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
- package/dist/esm/activities/chat/middleware/locks.js +71 -0
- package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
- package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
- package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
- package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
- package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
- package/dist/esm/activities/chat/middleware/run-store.js +176 -0
- package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
- package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
- package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
- package/dist/esm/activities/chat/middleware/validate.js +23 -28
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
- package/dist/esm/activities/chat/stream/json-parser.js +39 -25
- package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
- package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
- package/dist/esm/activities/chat/stream/processor.js +1341 -1542
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/stream/strategies.js +69 -53
- package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
- package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
- package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
- package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
- package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
- package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
- package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.js +85 -47
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/activities/generateAudio/adapter.js +22 -15
- package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +4 -0
- package/dist/esm/activities/generateAudio/index.js +141 -105
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.js +22 -15
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +4 -0
- package/dist/esm/activities/generateImage/index.js +155 -111
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/adapter.js +22 -15
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
- package/dist/esm/activities/generateSpeech/index.js +159 -110
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/adapter.js +22 -15
- package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
- package/dist/esm/activities/generateTranscription/index.js +159 -100
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.js +36 -29
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +143 -19
- package/dist/esm/activities/generateVideo/index.js +456 -279
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.js +60 -48
- package/dist/esm/activities/generateVideo/snap.js.map +1 -1
- package/dist/esm/activities/index.js +8 -34
- package/dist/esm/activities/middleware/index.d.ts +1 -1
- package/dist/esm/activities/middleware/run.d.ts +10 -0
- package/dist/esm/activities/middleware/run.js +53 -29
- package/dist/esm/activities/middleware/run.js.map +1 -1
- package/dist/esm/activities/middleware/types.d.ts +44 -6
- package/dist/esm/activities/stream-generation-result.d.ts +4 -1
- package/dist/esm/activities/stream-generation-result.js +79 -44
- package/dist/esm/activities/stream-generation-result.js.map +1 -1
- package/dist/esm/activities/summarize/adapter.js +22 -15
- package/dist/esm/activities/summarize/adapter.js.map +1 -1
- package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/activities/summarize/index.d.ts +27 -0
- package/dist/esm/activities/summarize/index.js +268 -102
- package/dist/esm/activities/summarize/index.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +2 -1
- package/dist/esm/adapter-internals.js +4 -11
- package/dist/esm/client.d.ts +25 -3
- package/dist/esm/client.js +131 -64
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/custom-events.d.ts +76 -0
- package/dist/esm/custom-events.js +37 -0
- package/dist/esm/custom-events.js.map +1 -0
- package/dist/esm/delivery-detach.d.ts +50 -0
- package/dist/esm/delivery-detach.js +71 -0
- package/dist/esm/delivery-detach.js.map +1 -0
- package/dist/esm/delivery-disconnect.d.ts +62 -0
- package/dist/esm/delivery-disconnect.js +81 -0
- package/dist/esm/delivery-disconnect.js.map +1 -0
- package/dist/esm/extend-adapter.js +19 -17
- package/dist/esm/extend-adapter.js.map +1 -1
- package/dist/esm/index.d.ts +24 -6
- package/dist/esm/index.js +30 -98
- package/dist/esm/interrupt-resume.d.ts +71 -0
- package/dist/esm/interrupt-resume.js +438 -0
- package/dist/esm/interrupt-resume.js.map +1 -0
- package/dist/esm/interrupt-serialization.d.ts +12 -0
- package/dist/esm/interrupt-serialization.js +178 -0
- package/dist/esm/interrupt-serialization.js.map +1 -0
- package/dist/esm/interrupts.d.ts +84 -0
- package/dist/esm/interrupts.js +31 -0
- package/dist/esm/interrupts.js.map +1 -0
- package/dist/esm/locks.d.ts +10 -0
- package/dist/esm/locks.js +2 -0
- package/dist/esm/logger/console-logger.js +101 -78
- package/dist/esm/logger/console-logger.js.map +1 -1
- package/dist/esm/logger/internal-logger.js +104 -89
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/logger/resolve.js +54 -49
- package/dist/esm/logger/resolve.js.map +1 -1
- package/dist/esm/logger/types.d.ts +1 -1
- package/dist/esm/middlewares/content-guard.js +142 -148
- package/dist/esm/middlewares/content-guard.js.map +1 -1
- package/dist/esm/middlewares/index.js +2 -6
- package/dist/esm/middlewares/otel.js +598 -732
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.js +47 -40
- package/dist/esm/middlewares/usage-attributes.js.map +1 -1
- package/dist/esm/realtime/event-emitter.js +24 -25
- package/dist/esm/realtime/event-emitter.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +5 -9
- package/dist/esm/realtime/index.js +29 -6
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/scope.d.ts +47 -0
- package/dist/esm/stream-durability.d.ts +171 -0
- package/dist/esm/stream-durability.js +295 -0
- package/dist/esm/stream-durability.js.map +1 -0
- package/dist/esm/stream-to-response.d.ts +178 -13
- package/dist/esm/stream-to-response.js +663 -115
- package/dist/esm/stream-to-response.js.map +1 -1
- package/dist/esm/strip-to-spec-middleware.js +30 -16
- package/dist/esm/strip-to-spec-middleware.js.map +1 -1
- package/dist/esm/system-prompts.js +27 -21
- package/dist/esm/system-prompts.js.map +1 -1
- package/dist/esm/tool-registry.js +72 -45
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/tools/provider-tool.js +14 -5
- package/dist/esm/tools/provider-tool.js.map +1 -1
- package/dist/esm/types.d.ts +321 -42
- package/dist/esm/types.js +2 -0
- package/dist/esm/utilities/ag-ui-wire.js +79 -93
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +26 -4
- package/dist/esm/utilities/chat-params.js +218 -92
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/errors.js +28 -18
- package/dist/esm/utilities/errors.js.map +1 -1
- package/dist/esm/utilities/media-prompt.js +46 -41
- package/dist/esm/utilities/media-prompt.js.map +1 -1
- package/dist/esm/utilities/numbers.js +13 -10
- package/dist/esm/utilities/numbers.js.map +1 -1
- package/dist/esm/utilities/provider-executed.js +20 -11
- package/dist/esm/utilities/provider-executed.js.map +1 -1
- package/dist/esm/utilities/sampling-keys.js +31 -19
- package/dist/esm/utilities/sampling-keys.js.map +1 -1
- package/dist/esm/utilities/tool-result.js +42 -30
- package/dist/esm/utilities/tool-result.js.map +1 -1
- package/dist/esm/utilities/usage.js +27 -9
- package/dist/esm/utilities/usage.js.map +1 -1
- package/dist/esm/utils.js +26 -18
- package/dist/esm/utils.js.map +1 -1
- package/package.json +10 -6
- package/skills/ai-core/SKILL.md +69 -18
- package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
- package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
- package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
- package/skills/ai-core/chat-experience/SKILL.md +98 -11
- package/skills/ai-core/client-persistence/SKILL.md +277 -0
- package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
- package/skills/ai-core/debug-logging/SKILL.md +1 -1
- package/skills/ai-core/locks/SKILL.md +143 -0
- package/skills/ai-core/media-generation/SKILL.md +144 -12
- package/skills/ai-core/middleware/SKILL.md +258 -33
- package/skills/ai-core/structured-outputs/SKILL.md +1 -1
- package/skills/ai-core/tool-calling/SKILL.md +54 -61
- package/src/activities/chat/agent-loop-strategies.ts +5 -39
- package/src/activities/chat/cancel.ts +81 -0
- package/src/activities/chat/index.ts +1091 -200
- package/src/activities/chat/mcp/manager.ts +4 -4
- package/src/activities/chat/mcp/types.ts +2 -2
- package/src/activities/chat/messages.ts +5 -3
- package/src/activities/chat/middleware/builder.ts +1 -1
- package/src/activities/chat/middleware/compose.ts +186 -9
- package/src/activities/chat/middleware/index.ts +26 -0
- package/src/activities/chat/middleware/locks.ts +102 -0
- package/src/activities/chat/middleware/pending-turn.ts +47 -0
- package/src/activities/chat/middleware/run-disconnect.ts +62 -0
- package/src/activities/chat/middleware/run-store.ts +412 -0
- package/src/activities/chat/middleware/types.ts +62 -1
- package/src/activities/chat/stream/processor.ts +189 -5
- package/src/activities/chat/tools/approval-schema.ts +205 -0
- package/src/activities/chat/tools/tool-calls.ts +106 -13
- package/src/activities/chat/tools/tool-definition.ts +210 -39
- package/src/activities/generateAudio/index.ts +20 -3
- package/src/activities/generateImage/index.ts +20 -3
- package/src/activities/generateSpeech/index.ts +25 -3
- package/src/activities/generateTranscription/index.ts +26 -3
- package/src/activities/generateVideo/index.ts +345 -82
- package/src/activities/middleware/index.ts +2 -0
- package/src/activities/middleware/run.ts +31 -0
- package/src/activities/middleware/types.ts +49 -5
- package/src/activities/stream-generation-result.ts +30 -2
- package/src/activities/summarize/chat-stream-summarize.ts +5 -0
- package/src/activities/summarize/index.ts +200 -10
- package/src/adapter-internals.ts +10 -1
- package/src/client.ts +244 -0
- package/src/custom-events.ts +107 -0
- package/src/delivery-detach.ts +72 -0
- package/src/delivery-disconnect.ts +84 -0
- package/src/index.ts +138 -1
- package/src/interrupt-resume.ts +824 -0
- package/src/interrupt-serialization.ts +183 -0
- package/src/interrupts.ts +146 -0
- package/src/locks.ts +17 -0
- package/src/logger/types.ts +1 -1
- package/src/middlewares/otel.ts +1 -0
- package/src/realtime/index.ts +5 -9
- package/src/scope.ts +47 -0
- package/src/stream-durability.ts +598 -0
- package/src/stream-to-response.ts +1051 -95
- package/src/strip-to-spec-middleware.ts +3 -3
- package/src/types.ts +405 -45
- package/src/utilities/chat-params.ts +245 -55
- package/dist/esm/activities/index.js.map +0 -1
- package/dist/esm/adapter-internals.js.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/middlewares/index.js.map +0 -1
|
@@ -9,13 +9,15 @@ description: >
|
|
|
9
9
|
NOT Vercel AI SDK — uses chat() not streamText().
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: tanstack-ai
|
|
12
|
-
library_version: '0.
|
|
12
|
+
library_version: '0.42.0'
|
|
13
13
|
sources:
|
|
14
14
|
- 'TanStack/ai:docs/getting-started/quick-start.md'
|
|
15
15
|
- 'TanStack/ai:docs/chat/streaming.md'
|
|
16
16
|
- 'TanStack/ai:docs/chat/connection-adapters.md'
|
|
17
17
|
- 'TanStack/ai:docs/chat/thinking-content.md'
|
|
18
18
|
- 'TanStack/ai:docs/advanced/multimodal-content.md'
|
|
19
|
+
- 'TanStack/ai:docs/resumable-streams/overview.md'
|
|
20
|
+
- 'TanStack/ai:docs/persistence/client-persistence.md'
|
|
19
21
|
---
|
|
20
22
|
|
|
21
23
|
# Chat Experience
|
|
@@ -41,7 +43,7 @@ export const Route = createFileRoute('/api/chat')({
|
|
|
41
43
|
const { messages } = body
|
|
42
44
|
|
|
43
45
|
const stream = chat({
|
|
44
|
-
adapter: openaiText('gpt-5.
|
|
46
|
+
adapter: openaiText('gpt-5.5'),
|
|
45
47
|
messages,
|
|
46
48
|
systemPrompts: ['You are a helpful assistant.'],
|
|
47
49
|
abortController,
|
|
@@ -148,6 +150,16 @@ const stream = chat({
|
|
|
148
150
|
return toServerSentEventsResponse(stream, { abortController })
|
|
149
151
|
```
|
|
150
152
|
|
|
153
|
+
To make the SSE response resumable (reconnect after a drop/refresh without
|
|
154
|
+
re-running the provider), pass a delivery-durability adapter:
|
|
155
|
+
`toServerSentEventsResponse(stream, { durability: { adapter: memoryStream(request) } })`
|
|
156
|
+
(`memoryStream` from `@tanstack/ai` is process-local, for dev/tests) or
|
|
157
|
+
`durableStream(request, { server })` from `@tanstack/ai-durable-stream`
|
|
158
|
+
(Durable Streams protocol, production). Each SSE event gets an opaque
|
|
159
|
+
adapter-owned `id:`; `fetchServerSentEvents` auto-reconnects with
|
|
160
|
+
`Last-Event-ID` and exposes `joinRun(runId)` to replay a run from the start.
|
|
161
|
+
See `docs/resumable-streams/overview.md`.
|
|
162
|
+
|
|
151
163
|
**Client:**
|
|
152
164
|
|
|
153
165
|
```typescript
|
|
@@ -317,7 +329,7 @@ import { chat, toHttpResponse } from '@tanstack/ai'
|
|
|
317
329
|
import { openaiText } from '@tanstack/ai-openai'
|
|
318
330
|
|
|
319
331
|
const stream = chat({
|
|
320
|
-
adapter: openaiText('gpt-5.
|
|
332
|
+
adapter: openaiText('gpt-5.5'),
|
|
321
333
|
messages,
|
|
322
334
|
abortController,
|
|
323
335
|
})
|
|
@@ -338,6 +350,13 @@ const { messages, sendMessage } = useChat({
|
|
|
338
350
|
The only difference is swapping `toServerSentEventsResponse` / `fetchServerSentEvents`
|
|
339
351
|
for `toHttpResponse` / `fetchHttpStream`. Everything else stays identical.
|
|
340
352
|
|
|
353
|
+
This includes resumability: pass the same `durability` adapter to
|
|
354
|
+
`toHttpResponse(stream, { durability: { adapter: memoryStream(request) } })` and
|
|
355
|
+
each NDJSON line becomes an `{ id, chunk }` envelope. `fetchHttpStream`
|
|
356
|
+
auto-reconnects with `Last-Event-ID`, de-dupes the replayed prefix, and exposes
|
|
357
|
+
`joinRun(runId)` — the same guarantees as resumable SSE. The XHR adapters
|
|
358
|
+
(`xhrServerSentEvents` / `xhrHttpStream`) are resumable too.
|
|
359
|
+
|
|
341
360
|
### 6. MCP Tool Discovery via `chat({ mcp })`
|
|
342
361
|
|
|
343
362
|
Pass `mcp` to let `chat()` own discovery **and** lifecycle for one or more MCP
|
|
@@ -467,6 +486,73 @@ to `sendMessage`:
|
|
|
467
486
|
sendMessage('Never mind, do this instead', { whenBusy: 'interrupt' })
|
|
468
487
|
```
|
|
469
488
|
|
|
489
|
+
### 8. Browser-Refresh Durability (client persistence)
|
|
490
|
+
|
|
491
|
+
By default a `ChatClient` / `useChat` keeps messages in memory only, so a full
|
|
492
|
+
page reload loses the conversation. The optional `persistence` option (a
|
|
493
|
+
`ChatClientPersistence` adapter) fixes this from the client side: it stores one
|
|
494
|
+
combined record — `{ messages, resume? }` (`ChatPersistedState`) — per chat `id`,
|
|
495
|
+
so a reload restores the transcript **and** rehydrates any pending interrupt /
|
|
496
|
+
rejoins a run that was still streaming. No manual `initialMessages` + `onFinish`
|
|
497
|
+
boilerplate.
|
|
498
|
+
|
|
499
|
+
Three storage adapters ship from `@tanstack/ai-client`:
|
|
500
|
+
`localStoragePersistence` (survives reloads and browser restarts),
|
|
501
|
+
`sessionStoragePersistence` (scoped to the tab), and `indexedDBPersistence`
|
|
502
|
+
(async, structured-clone storage — no codec needed for `Date`/`Map`/etc.).
|
|
503
|
+
Give the chat a stable `threadId` so the reload finds the same record.
|
|
504
|
+
Persistence keys on `threadId`; the storage adapters are re-exported from each
|
|
505
|
+
framework package, so a single import works:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import {
|
|
509
|
+
useChat,
|
|
510
|
+
fetchServerSentEvents,
|
|
511
|
+
localStoragePersistence,
|
|
512
|
+
} from '@tanstack/ai-react'
|
|
513
|
+
|
|
514
|
+
// Defaults to the ChatPersistedState shape and a JSON codec, so no type
|
|
515
|
+
// argument or serialize/deserialize is needed. indexedDBPersistence stores via
|
|
516
|
+
// structured clone (a Date round-trips exactly).
|
|
517
|
+
const persistence = localStoragePersistence()
|
|
518
|
+
|
|
519
|
+
function Chat() {
|
|
520
|
+
const { messages, sendMessage } = useChat({
|
|
521
|
+
threadId: 'support-chat',
|
|
522
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
523
|
+
persistence,
|
|
524
|
+
})
|
|
525
|
+
// ...render messages, call sendMessage(text)
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Keep large transcripts off the client.** `persistence` also accepts `true`
|
|
530
|
+
(server-authoritative): the client caches nothing, and on mount it hydrates the
|
|
531
|
+
thread from the server by `threadId` (transcript plus a cursor to any run still
|
|
532
|
+
generating). An adapter is client-authoritative; `true` leaves history on the
|
|
533
|
+
server and needs a connection with a `hydrate` handler plus a server GET
|
|
534
|
+
endpoint (`reconstructChat`), since the delivery log only holds one run.
|
|
535
|
+
|
|
536
|
+
**Mid-stream reload rejoin.** If the run was still streaming when the page
|
|
537
|
+
reloaded, the client re-attaches instead of showing a frozen half-reply — but
|
|
538
|
+
only when the connection is **resumable**: a delivery-durability-backed route
|
|
539
|
+
that records the stream and exposes a GET replay handler (see
|
|
540
|
+
`docs/resumable-streams/overview.md` and Pattern 1's `durability` adapter). Given
|
|
541
|
+
that, `useChat` finds the persisted in-flight run on load and auto-rejoins it via
|
|
542
|
+
`joinRun`, replaying from the server's log so the reply finishes where it left
|
|
543
|
+
off. No extra client code beyond the resumable connection.
|
|
544
|
+
|
|
545
|
+
**Every framework, no extra code.** Durability rides the existing `persistence`
|
|
546
|
+
option, so it works identically in `@tanstack/ai-react`, `-solid`, `-vue`,
|
|
547
|
+
`-svelte`, `-angular`, and `-preact` — pass `persistence` (and a stable
|
|
548
|
+
`threadId`, which is the chat's identity) to the framework's `useChat` /
|
|
549
|
+
`createChat` / `injectChat`; nothing is framework-specific.
|
|
550
|
+
|
|
551
|
+
> **Client vs. server durability.** This is the client (per-browser) half.
|
|
552
|
+
> The authoritative, multi-user, server-side copy is the `withPersistence`
|
|
553
|
+
> middleware — see ai-core/middleware/SKILL.md. The two are independent; use
|
|
554
|
+
> both for instant reload restore plus a durable record of record.
|
|
555
|
+
|
|
470
556
|
## Common Mistakes
|
|
471
557
|
|
|
472
558
|
### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
|
|
@@ -475,12 +561,12 @@ sendMessage('Never mind, do this instead', { whenBusy: 'interrupt' })
|
|
|
475
561
|
// WRONG
|
|
476
562
|
import { streamText } from 'ai'
|
|
477
563
|
import { openai } from '@ai-sdk/openai'
|
|
478
|
-
const result = streamText({ model: openai('gpt-
|
|
564
|
+
const result = streamText({ model: openai('gpt-5.5'), messages })
|
|
479
565
|
|
|
480
566
|
// CORRECT
|
|
481
567
|
import { chat } from '@tanstack/ai'
|
|
482
568
|
import { openaiText } from '@tanstack/ai-openai'
|
|
483
|
-
const stream = chat({ adapter: openaiText('gpt-5.
|
|
569
|
+
const stream = chat({ adapter: openaiText('gpt-5.5'), messages })
|
|
484
570
|
```
|
|
485
571
|
|
|
486
572
|
### b. CRITICAL: Using Vercel createOpenAI() provider pattern
|
|
@@ -489,12 +575,12 @@ const stream = chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
|
489
575
|
// WRONG
|
|
490
576
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
491
577
|
const openai = createOpenAI({ apiKey })
|
|
492
|
-
streamText({ model: openai('gpt-
|
|
578
|
+
streamText({ model: openai('gpt-5.5'), messages })
|
|
493
579
|
|
|
494
580
|
// CORRECT
|
|
495
581
|
import { openaiText } from '@tanstack/ai-openai'
|
|
496
582
|
import { chat } from '@tanstack/ai'
|
|
497
|
-
chat({ adapter: openaiText('gpt-5.
|
|
583
|
+
chat({ adapter: openaiText('gpt-5.5'), messages })
|
|
498
584
|
```
|
|
499
585
|
|
|
500
586
|
### c. CRITICAL: Using monolithic openai() instead of openaiText()
|
|
@@ -502,11 +588,11 @@ chat({ adapter: openaiText('gpt-5.2'), messages })
|
|
|
502
588
|
```typescript
|
|
503
589
|
// WRONG
|
|
504
590
|
import { openai } from '@tanstack/ai-openai'
|
|
505
|
-
chat({ adapter: openai(), model: 'gpt-5.
|
|
591
|
+
chat({ adapter: openai(), model: 'gpt-5.5', messages })
|
|
506
592
|
|
|
507
593
|
// CORRECT
|
|
508
594
|
import { openaiText } from '@tanstack/ai-openai'
|
|
509
|
-
chat({ adapter: openaiText('gpt-5.
|
|
595
|
+
chat({ adapter: openaiText('gpt-5.5'), messages })
|
|
510
596
|
```
|
|
511
597
|
|
|
512
598
|
The monolithic `openai()` adapter is deprecated. Use tree-shakeable adapters:
|
|
@@ -528,10 +614,10 @@ return toServerSentEventsResponse(stream, { abortController })
|
|
|
528
614
|
|
|
529
615
|
```typescript
|
|
530
616
|
// WRONG
|
|
531
|
-
chat({ adapter: openaiText(), model: 'gpt-5.
|
|
617
|
+
chat({ adapter: openaiText(), model: 'gpt-5.5', messages })
|
|
532
618
|
|
|
533
619
|
// CORRECT
|
|
534
|
-
chat({ adapter: openaiText('gpt-5.
|
|
620
|
+
chat({ adapter: openaiText('gpt-5.5'), messages })
|
|
535
621
|
```
|
|
536
622
|
|
|
537
623
|
The model is passed to the adapter factory, not to `chat()`.
|
|
@@ -678,3 +764,4 @@ If not handled, the UI appears to hang with no feedback.
|
|
|
678
764
|
- See also: **ai-core/tool-calling/SKILL.md** -- Most chats include tools
|
|
679
765
|
- See also: **ai-core/adapter-configuration/SKILL.md** -- Adapter choice affects available features
|
|
680
766
|
- See also: **ai-core/middleware/SKILL.md** -- Use middleware for analytics and lifecycle events
|
|
767
|
+
- See also: **`@tanstack/ai-persistence` skills** (`skills/ai-persistence/SKILL.md` in that package) -- Server + client state persistence, store contracts, adapter recipes (deeper than Pattern 8)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/client-persistence
|
|
3
|
+
description: >
|
|
4
|
+
Browser chat persistence on useChat / ChatClient: localStoragePersistence,
|
|
5
|
+
sessionStoragePersistence, indexedDBPersistence. Client-authoritative
|
|
6
|
+
(adapter, full transcript) vs server-authoritative (persistence: true, no
|
|
7
|
+
client cache).
|
|
8
|
+
Reload restore, pending interrupts, mid-stream rejoin with delivery
|
|
9
|
+
durability. Use for SPA reload durability — NOT server history alone.
|
|
10
|
+
Also covers generation hooks (useGenerateImage etc.), which take only the
|
|
11
|
+
server-driven mode: persistence: true hydrates the last generation for the
|
|
12
|
+
(REQUIRED) threadId from the server on mount and repaints status/result/error,
|
|
13
|
+
nothing is cached in the browser.
|
|
14
|
+
No extra package: the adapters ship in the framework packages.
|
|
15
|
+
type: sub-skill
|
|
16
|
+
library: tanstack-ai
|
|
17
|
+
library_version: '0.42.0'
|
|
18
|
+
sources:
|
|
19
|
+
- 'TanStack/ai:docs/persistence/client-persistence.md'
|
|
20
|
+
- 'TanStack/ai:docs/persistence/overview.md'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Client Persistence
|
|
24
|
+
|
|
25
|
+
> Builds on ai-core, and on `ai-core/chat-experience` for `useChat` itself.
|
|
26
|
+
>
|
|
27
|
+
> **No extra package.** The adapters below ship in the **framework** packages
|
|
28
|
+
> (`@tanstack/ai-react` and friends, re-exported from `@tanstack/ai-client`),
|
|
29
|
+
> so browser persistence needs nothing installed beyond what a chat UI already
|
|
30
|
+
> has. The **server** half is a separate package — see
|
|
31
|
+
> `@tanstack/ai-persistence` and its `ai-persistence/server` skill.
|
|
32
|
+
|
|
33
|
+
A `ChatClient` / `useChat` keeps messages in memory. The `persistence` option
|
|
34
|
+
stores one record per `threadId` so a reload can repaint the transcript,
|
|
35
|
+
restore a pending interrupt, and rejoin an in-flight run.
|
|
36
|
+
|
|
37
|
+
Import adapters from the **framework package** (not `@tanstack/ai-client`
|
|
38
|
+
unless vanilla JS):
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import {
|
|
42
|
+
useChat,
|
|
43
|
+
fetchServerSentEvents,
|
|
44
|
+
localStoragePersistence,
|
|
45
|
+
sessionStoragePersistence,
|
|
46
|
+
indexedDBPersistence,
|
|
47
|
+
} from '@tanstack/ai-react'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Adapters
|
|
51
|
+
|
|
52
|
+
| Adapter | Survives | Notes |
|
|
53
|
+
| ----------------------------- | -------------------------- | --------------------------------------------------------------- |
|
|
54
|
+
| `localStoragePersistence()` | Reloads + browser restarts | Sync hydrate; quota-bound; JSON codec default |
|
|
55
|
+
| `sessionStoragePersistence()` | Reloads in the same tab | Cleared when tab/session ends |
|
|
56
|
+
| `indexedDBPersistence()` | Reloads + restarts | Async open (first paint may be empty briefly); structured clone |
|
|
57
|
+
|
|
58
|
+
All default to the chat persisted-state shape — no type argument or codec
|
|
59
|
+
required for normal use.
|
|
60
|
+
|
|
61
|
+
## Mode A — cache everything (client-authoritative)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
function Chat() {
|
|
65
|
+
const { messages, sendMessage } = useChat({
|
|
66
|
+
threadId: 'support-chat', // stable — required
|
|
67
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
68
|
+
persistence: localStoragePersistence(),
|
|
69
|
+
})
|
|
70
|
+
// ...
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Bare adapter ≡ full transcript + resume pointer. Browser owns history; server
|
|
75
|
+
(if any) mirrors when you post non-empty `messages`.
|
|
76
|
+
|
|
77
|
+
Best for: SPA, offline-first, single device, moderate conversation size.
|
|
78
|
+
|
|
79
|
+
## Mode B — server-authoritative (`persistence: true`)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
function Chat({ threadId }: { threadId: string }) {
|
|
83
|
+
const { messages, sendMessage } = useChat({
|
|
84
|
+
threadId,
|
|
85
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
86
|
+
persistence: true,
|
|
87
|
+
})
|
|
88
|
+
// ...
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Nothing is cached client-side: no transcript, no resume pointer.
|
|
93
|
+
|
|
94
|
+
On mount, `useChat` hydrates the thread from the **server** by `threadId`
|
|
95
|
+
(paint + tail active run). Same path for another device. Pair with server
|
|
96
|
+
`withPersistence` + a hydrate route (`reconstructChat` or equivalent).
|
|
97
|
+
|
|
98
|
+
Best for: large transcripts, multi-device, compliance (no message bodies in
|
|
99
|
+
browser storage).
|
|
100
|
+
|
|
101
|
+
## What a reload restores
|
|
102
|
+
|
|
103
|
+
1. **Finished run** — transcript from the adapter (mode A) or server (mode B).
|
|
104
|
+
2. **Paused on interrupt** — approval UI restored (from the adapter in mode A,
|
|
105
|
+
the server hydrate in mode B).
|
|
106
|
+
3. **Still streaming** — needs **delivery durability** on the route
|
|
107
|
+
(`toServerSentEventsResponse(stream, { durability: … })`) so the client can
|
|
108
|
+
`joinRun` and finish the reply. Persistence alone is not enough.
|
|
109
|
+
|
|
110
|
+
## Stable `threadId` is the identity
|
|
111
|
+
|
|
112
|
+
Persistence keys on `threadId`. The hooks have **no separate `id` option** — a
|
|
113
|
+
chat's identity _is_ its `threadId`. Without a stable one, each load is a new
|
|
114
|
+
chat. Generate it server-side or from a route param the user owns; do not
|
|
115
|
+
randomize per mount.
|
|
116
|
+
|
|
117
|
+
## Generation hooks: server-driven only
|
|
118
|
+
|
|
119
|
+
The generation hooks (`useGenerateImage`, `useGenerateVideo`, `useGeneration`,
|
|
120
|
+
`useSummarize`, `useTranscription`, …) take a `persistence` option too, but it is
|
|
121
|
+
**boolean only** — there is no storage-adapter mode, and the browser caches
|
|
122
|
+
nothing. **The hooks are transparent, mirroring `useChat`:** a reload repaints the
|
|
123
|
+
hook's
|
|
124
|
+
**normal** fields — `status` (`'idle'` / `'generating'` / `'success'` /
|
|
125
|
+
`'error'`), `error`, and `result` — as if the run had just finished. There is
|
|
126
|
+
**no** `resumeSnapshot`, `resumeState`, `pendingArtifacts`, or `resultArtifacts`
|
|
127
|
+
field. The one extra field is `runId`: the id of the generation job currently
|
|
128
|
+
running, or `null` when nothing is in flight. The persisted record holds run
|
|
129
|
+
identity, status, error, and result metadata (ids, model, a provider video job
|
|
130
|
+
id), **never the generated media bytes**.
|
|
131
|
+
|
|
132
|
+
The hook return is exactly `generate` / `result` / `isLoading` / `error` /
|
|
133
|
+
`status` / `stop` / `reset` / `runId`.
|
|
134
|
+
|
|
135
|
+
### Turning it on (`persistence: true`)
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
const image = useGenerateImage({
|
|
139
|
+
threadId, // REQUIRED — the scope the last generation is hydrated under
|
|
140
|
+
connection: fetchServerSentEvents('/api/generate/image'),
|
|
141
|
+
persistence: true,
|
|
142
|
+
})
|
|
143
|
+
// After a reload: image.status / image.result / image.error are the last
|
|
144
|
+
// generation for `threadId`, fetched from the server — nothing was cached.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The server half — the same route handles the run and the hydration `GET`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import {
|
|
151
|
+
generateImage,
|
|
152
|
+
generationParamsFromRequest,
|
|
153
|
+
toServerSentEventsResponse,
|
|
154
|
+
} from '@tanstack/ai'
|
|
155
|
+
import { openaiImage } from '@tanstack/ai-openai'
|
|
156
|
+
import {
|
|
157
|
+
memoryPersistence,
|
|
158
|
+
reconstructGeneration,
|
|
159
|
+
withGenerationPersistence,
|
|
160
|
+
} from '@tanstack/ai-persistence'
|
|
161
|
+
|
|
162
|
+
// Needs `stores.generationRuns`; `memoryPersistence()` ships one.
|
|
163
|
+
const persistence = memoryPersistence()
|
|
164
|
+
|
|
165
|
+
export async function POST(request: Request) {
|
|
166
|
+
const { input, threadId } = await generationParamsFromRequest(
|
|
167
|
+
'image',
|
|
168
|
+
request,
|
|
169
|
+
)
|
|
170
|
+
if (typeof input.prompt !== 'string') {
|
|
171
|
+
throw new Error('This endpoint accepts text image prompts only.')
|
|
172
|
+
}
|
|
173
|
+
if (threadId === undefined) {
|
|
174
|
+
throw new Error('Generation persistence requires a `threadId`.')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return toServerSentEventsResponse(
|
|
178
|
+
generateImage({
|
|
179
|
+
adapter: openaiImage('gpt-image-2'),
|
|
180
|
+
prompt: input.prompt,
|
|
181
|
+
// The stable slot this run fills. Required by persistence: the run record
|
|
182
|
+
// is filed under it, and the client hydrates by it on mount.
|
|
183
|
+
threadId,
|
|
184
|
+
stream: true,
|
|
185
|
+
middleware: [withGenerationPersistence(persistence)],
|
|
186
|
+
}),
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Mount-time hydration: resolves `?runId=` (preferred) or the latest run linked
|
|
191
|
+
// to `?threadId=`, and returns `{ resumeSnapshot, activeRun }`.
|
|
192
|
+
export function GET(request: Request) {
|
|
193
|
+
return reconstructGeneration(persistence, request, {
|
|
194
|
+
// Multi-user routes MUST authorize: the ids come from the caller. Derive
|
|
195
|
+
// identity from server-side session state, then check ownership.
|
|
196
|
+
authorize: async (id, req) => {
|
|
197
|
+
// const user = await auth(req)
|
|
198
|
+
// return user != null && (await db.threadOwnedBy(user.id, id))
|
|
199
|
+
void id
|
|
200
|
+
void req
|
|
201
|
+
return true
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- Nothing is cached client-side. On mount the client hydrates the **last
|
|
208
|
+
generation** for its `threadId` from the server via the connection's
|
|
209
|
+
`hydrateGeneration` handler (the SSE/HTTP adapters issue a `GET` with
|
|
210
|
+
`?threadId=` to the same endpoint URL) and repaints it into the normal fields.
|
|
211
|
+
- The server `GET` returns `reconstructGeneration(persistence, request)` from
|
|
212
|
+
`@tanstack/ai-persistence` — it resolves the run by `?runId=` (preferred) or
|
|
213
|
+
the latest run linked to `?threadId=`, and needs `stores.generationRuns`. Pair it with
|
|
214
|
+
`withGenerationPersistence` on the generation route. See
|
|
215
|
+
`ai-core/media-generation` and `ai-persistence`.
|
|
216
|
+
- Best for multi-device / compliance (no generation metadata in browser
|
|
217
|
+
storage), exactly like chat's server-authoritative mode.
|
|
218
|
+
|
|
219
|
+
### Restoring media: byte storage + `artifactUrl`
|
|
220
|
+
|
|
221
|
+
`result` comes back with its media only when the **server** persists the bytes
|
|
222
|
+
(`stores.artifacts` + `stores.blobs`) AND `withGenerationPersistence` is given an
|
|
223
|
+
`artifactUrl` mapper:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
withGenerationPersistence(persistence, {
|
|
227
|
+
artifactUrl: (ref) => `/api/generate/image/artifact?id=${ref.artifactId}`,
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`artifactUrl` stamps a durable app-origin URL onto each persisted ref and
|
|
232
|
+
rewrites the live result's media to it, so live and restored results match. The
|
|
233
|
+
durable refs travel on `result.artifacts`; on restore the hook rebuilds `result`
|
|
234
|
+
from them, so `result.images[i].url` (or a video's `result.url`) serves from your
|
|
235
|
+
own origin. `result.artifacts` is the whole artifact surface on the hook.
|
|
236
|
+
Without byte storage, a reload restores `status` / `error` and `result` stays
|
|
237
|
+
`null`.
|
|
238
|
+
|
|
239
|
+
Also worth knowing:
|
|
240
|
+
|
|
241
|
+
- `stop()` marks the record no longer resumable; `reset()` clears the in-memory
|
|
242
|
+
snapshot.
|
|
243
|
+
- Nothing auto-runs from a hydrated record — `generate(...)` is always explicit.
|
|
244
|
+
- Use `status` / `result` for a finished run; use `runId` to tell that a run was
|
|
245
|
+
still generating when the page closed, and to name it to your own server (to
|
|
246
|
+
cancel or poll the provider job — `stop()` only aborts the local stream).
|
|
247
|
+
|
|
248
|
+
## Common mistakes
|
|
249
|
+
|
|
250
|
+
### HIGH: No `threadId`
|
|
251
|
+
|
|
252
|
+
Record cannot be found after reload.
|
|
253
|
+
|
|
254
|
+
### HIGH: Passing `id` to `useChat`
|
|
255
|
+
|
|
256
|
+
Removed — `threadId` is the identity. (`ChatClient` still accepts `id` directly
|
|
257
|
+
as a lower-level escape hatch for keying storage separately from the wire
|
|
258
|
+
thread; the framework hooks do not.)
|
|
259
|
+
|
|
260
|
+
### HIGH: `persistence: true` without server history
|
|
261
|
+
|
|
262
|
+
Empty chat after reload unless the server can reconstruct by `threadId`.
|
|
263
|
+
|
|
264
|
+
### MEDIUM: Huge transcripts in `localStorage`
|
|
265
|
+
|
|
266
|
+
Quota and main-thread cost. Prefer `persistence: true` + server store, or
|
|
267
|
+
IndexedDB with care.
|
|
268
|
+
|
|
269
|
+
### MEDIUM: Expecting multi-device sync from client storage alone
|
|
270
|
+
|
|
271
|
+
`localStorage` is per-browser. Use server persistence for multi-device.
|
|
272
|
+
|
|
273
|
+
## Cross-references
|
|
274
|
+
|
|
275
|
+
- **ai-persistence/server** (`@tanstack/ai-persistence`) — authoritative server half
|
|
276
|
+
- **ai-core/chat-experience** — `useChat`, resumable connections
|
|
277
|
+
- Resumable streams docs — mid-stream rejoin
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ai-core/locks
|
|
3
|
+
description: >
|
|
4
|
+
LockStore, InMemoryLockStore, LocksCapability and withLocks for
|
|
5
|
+
multi-instance coordination in TanStack AI. Ships in @tanstack/ai — NOT in
|
|
6
|
+
@tanstack/ai-persistence. Separate from AIPersistence state stores — not a
|
|
7
|
+
stores key, not composable. InMemoryLockStore vs a distributed (e.g.
|
|
8
|
+
Cloudflare Durable Object) lock, lease recovery, AbortSignal in critical
|
|
9
|
+
sections. Use when sandbox or other middleware needs cross-worker mutual
|
|
10
|
+
exclusion — NOT for storing messages/runs (use withPersistence).
|
|
11
|
+
type: sub-skill
|
|
12
|
+
library: tanstack-ai
|
|
13
|
+
library_version: '0.42.0'
|
|
14
|
+
sources:
|
|
15
|
+
- 'TanStack/ai:docs/advanced/locks.md'
|
|
16
|
+
- 'TanStack/ai:packages/ai/src/activities/chat/middleware/locks.ts'
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Locks (coordination — not persistence)
|
|
20
|
+
|
|
21
|
+
> **Dependency note:** This skill builds on ai-core and ai-core/middleware.
|
|
22
|
+
> `withLocks` is a ChatMiddleware that provides a capability. Locks are **not**
|
|
23
|
+
> part of `AIPersistence.stores` and are **not** composed with
|
|
24
|
+
> `composePersistence` — they ship in `@tanstack/ai`, independent of
|
|
25
|
+
> `@tanstack/ai-persistence`.
|
|
26
|
+
|
|
27
|
+
## Why separate?
|
|
28
|
+
|
|
29
|
+
State stores answer "what is durable chat data?"
|
|
30
|
+
Locks answer "who may run this critical section right now?"
|
|
31
|
+
|
|
32
|
+
`withPersistence` does **not** automatically lock a whole turn. Take a
|
|
33
|
+
per-thread (or other) lock yourself when multi-writer races matter.
|
|
34
|
+
|
|
35
|
+
## Wire locks
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
|
|
39
|
+
|
|
40
|
+
middleware: [
|
|
41
|
+
withLocks(new InMemoryLockStore()), // single process
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Alongside persistence — optional, locks do not require it:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
|
|
49
|
+
import { withPersistence } from '@tanstack/ai-persistence'
|
|
50
|
+
|
|
51
|
+
middleware: [withPersistence(persistence), withLocks(new InMemoryLockStore())]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`withLocks` provides `LocksCapability` for downstream middleware (e.g.
|
|
55
|
+
sandbox). Order: usually state first, locks alongside or after depending on
|
|
56
|
+
who consumes the capability.
|
|
57
|
+
|
|
58
|
+
## The contract
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
interface LockStore {
|
|
62
|
+
withLock<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T>
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`InMemoryLockStore` ships in **`@tanstack/ai/locks`**: a per-key promise chain,
|
|
67
|
+
correct **within a single process only**. Multi-instance deployments need a
|
|
68
|
+
distributed implementation — you write it. The Cloudflare Durable Object recipe
|
|
69
|
+
is in **ai-persistence/build-cloudflare-adapter** (`@tanstack/ai-persistence`).
|
|
70
|
+
|
|
71
|
+
Type your own store with `defineLock` (autocomplete, no `: LockStore`
|
|
72
|
+
annotation), then hand it to `withLocks`. Acquire the key, run `fn`, release when
|
|
73
|
+
`fn` settles:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { defineLock, withLocks } from '@tanstack/ai/locks'
|
|
77
|
+
import { acquire } from './my-lock-backend'
|
|
78
|
+
|
|
79
|
+
const locks = defineLock({
|
|
80
|
+
async withLock(key, fn) {
|
|
81
|
+
const { release, signal } = await acquire(key)
|
|
82
|
+
try {
|
|
83
|
+
return await fn(signal)
|
|
84
|
+
} finally {
|
|
85
|
+
release()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
middleware: [withLocks(locks)]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Lease semantics
|
|
94
|
+
|
|
95
|
+
A good `LockStore`:
|
|
96
|
+
|
|
97
|
+
- Serializes owners per key,
|
|
98
|
+
- Uses **leases** (or equivalent) so a crashed owner cannot block forever,
|
|
99
|
+
- Passes an `AbortSignal` into the critical section via `withLock`; when the
|
|
100
|
+
lease is lost, abort so work stops starting external mutations.
|
|
101
|
+
|
|
102
|
+
Callbacks must honor the signal and pass it to cancellable dependencies.
|
|
103
|
+
`InMemoryLockStore` never aborts its signal — within one process, ownership
|
|
104
|
+
cannot be lost.
|
|
105
|
+
|
|
106
|
+
## Capability identity
|
|
107
|
+
|
|
108
|
+
The `'locks'` capability token lives in `@tanstack/ai/locks`. Capability identity
|
|
109
|
+
is by **object reference**, so one shared token means a `withLocks` in the chain
|
|
110
|
+
reaches `withSandbox` automatically.
|
|
111
|
+
|
|
112
|
+
## Common mistakes
|
|
113
|
+
|
|
114
|
+
### HIGH: Importing locks from `@tanstack/ai-persistence`
|
|
115
|
+
|
|
116
|
+
They are not exported there. Use `@tanstack/ai`.
|
|
117
|
+
|
|
118
|
+
### HIGH: Putting `locks` on `AIPersistence.stores`
|
|
119
|
+
|
|
120
|
+
Not supported. `stores` accepts only `messages`, `runs`, `interrupts`,
|
|
121
|
+
`metadata` — never `locks`. Use `withLocks`.
|
|
122
|
+
|
|
123
|
+
### HIGH: Passing `locks` to `composePersistence` overrides
|
|
124
|
+
|
|
125
|
+
Same rejection, at the override layer. Locks are not state.
|
|
126
|
+
|
|
127
|
+
### HIGH: Passing `'locks'` to the conformance testkit's `skip`
|
|
128
|
+
|
|
129
|
+
`skip` accepts only chat state store keys. The suite does not cover locks
|
|
130
|
+
at all — test lease expiry and abort separately.
|
|
131
|
+
|
|
132
|
+
### HIGH: `InMemoryLockStore` across multiple processes
|
|
133
|
+
|
|
134
|
+
No mutual exclusion between machines — use a distributed lock store.
|
|
135
|
+
|
|
136
|
+
### MEDIUM: Ignoring lease abort
|
|
137
|
+
|
|
138
|
+
Continuing work after losing the lease races other owners.
|
|
139
|
+
|
|
140
|
+
## Cross-references
|
|
141
|
+
|
|
142
|
+
- See also: **ai-core/middleware/SKILL.md** -- the middleware chain and capability plumbing
|
|
143
|
+
- See also: **`@tanstack/ai-persistence` skills** (`skills/ai-persistence/SKILL.md` in that package) -- `ai-persistence/server` (state middleware) and `ai-persistence/build-cloudflare-adapter` (Durable Object lock recipe)
|