@tanstack/ai 0.41.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.
Files changed (272) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/activities/chat/adapter.js +23 -16
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/agent-loop-strategies.d.ts +10 -4
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -17
  6. package/dist/esm/activities/chat/agent-loop-strategies.js.map +1 -1
  7. package/dist/esm/activities/chat/cancel.d.ts +40 -0
  8. package/dist/esm/activities/chat/cancel.js +54 -0
  9. package/dist/esm/activities/chat/cancel.js.map +1 -0
  10. package/dist/esm/activities/chat/index.d.ts +28 -16
  11. package/dist/esm/activities/chat/index.js +2100 -1744
  12. package/dist/esm/activities/chat/index.js.map +1 -1
  13. package/dist/esm/activities/chat/mcp/manager.d.ts +2 -2
  14. package/dist/esm/activities/chat/mcp/manager.js +90 -77
  15. package/dist/esm/activities/chat/mcp/manager.js.map +1 -1
  16. package/dist/esm/activities/chat/mcp/types.d.ts +2 -2
  17. package/dist/esm/activities/chat/messages.js +397 -346
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/builder.js +17 -15
  20. package/dist/esm/activities/chat/middleware/builder.js.map +1 -1
  21. package/dist/esm/activities/chat/middleware/capabilities.js +78 -43
  22. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -1
  23. package/dist/esm/activities/chat/middleware/compose.d.ts +94 -1
  24. package/dist/esm/activities/chat/middleware/compose.js +623 -531
  25. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  26. package/dist/esm/activities/chat/middleware/define.js +12 -5
  27. package/dist/esm/activities/chat/middleware/define.js.map +1 -1
  28. package/dist/esm/activities/chat/middleware/index.d.ts +5 -1
  29. package/dist/esm/activities/chat/middleware/locks.d.ts +50 -0
  30. package/dist/esm/activities/chat/middleware/locks.js +71 -0
  31. package/dist/esm/activities/chat/middleware/locks.js.map +1 -0
  32. package/dist/esm/activities/chat/middleware/pending-turn.d.ts +15 -0
  33. package/dist/esm/activities/chat/middleware/pending-turn.js +35 -0
  34. package/dist/esm/activities/chat/middleware/pending-turn.js.map +1 -0
  35. package/dist/esm/activities/chat/middleware/run-disconnect.d.ts +23 -0
  36. package/dist/esm/activities/chat/middleware/run-disconnect.js +42 -0
  37. package/dist/esm/activities/chat/middleware/run-disconnect.js.map +1 -0
  38. package/dist/esm/activities/chat/middleware/run-store.d.ts +283 -0
  39. package/dist/esm/activities/chat/middleware/run-store.js +176 -0
  40. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -0
  41. package/dist/esm/activities/chat/middleware/sandbox-runtime.js +14 -8
  42. package/dist/esm/activities/chat/middleware/sandbox-runtime.js.map +1 -1
  43. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +79 -70
  44. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -1
  45. package/dist/esm/activities/chat/middleware/types.d.ts +59 -2
  46. package/dist/esm/activities/chat/middleware/validate.js +23 -28
  47. package/dist/esm/activities/chat/middleware/validate.js.map +1 -1
  48. package/dist/esm/activities/chat/stream/json-parser.js +39 -25
  49. package/dist/esm/activities/chat/stream/json-parser.js.map +1 -1
  50. package/dist/esm/activities/chat/stream/message-updaters.js +275 -234
  51. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  52. package/dist/esm/activities/chat/stream/processor.d.ts +24 -4
  53. package/dist/esm/activities/chat/stream/processor.js +1341 -1542
  54. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  55. package/dist/esm/activities/chat/stream/strategies.js +69 -53
  56. package/dist/esm/activities/chat/stream/strategies.js.map +1 -1
  57. package/dist/esm/activities/chat/tools/approval-schema.d.ts +19 -0
  58. package/dist/esm/activities/chat/tools/approval-schema.js +117 -0
  59. package/dist/esm/activities/chat/tools/approval-schema.js.map +1 -0
  60. package/dist/esm/activities/chat/tools/lazy-tool-manager.js +164 -191
  61. package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
  62. package/dist/esm/activities/chat/tools/lazy-tools.js +24 -12
  63. package/dist/esm/activities/chat/tools/lazy-tools.js.map +1 -1
  64. package/dist/esm/activities/chat/tools/schema-converter.js +293 -146
  65. package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
  66. package/dist/esm/activities/chat/tools/tool-calls.d.ts +18 -2
  67. package/dist/esm/activities/chat/tools/tool-calls.js +522 -531
  68. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  69. package/dist/esm/activities/chat/tools/tool-definition.d.ts +75 -16
  70. package/dist/esm/activities/chat/tools/tool-definition.js +95 -23
  71. package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
  72. package/dist/esm/activities/error-payload.js +85 -47
  73. package/dist/esm/activities/error-payload.js.map +1 -1
  74. package/dist/esm/activities/generateAudio/adapter.js +22 -15
  75. package/dist/esm/activities/generateAudio/adapter.js.map +1 -1
  76. package/dist/esm/activities/generateAudio/index.d.ts +4 -0
  77. package/dist/esm/activities/generateAudio/index.js +141 -105
  78. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  79. package/dist/esm/activities/generateImage/adapter.js +22 -15
  80. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  81. package/dist/esm/activities/generateImage/index.d.ts +4 -0
  82. package/dist/esm/activities/generateImage/index.js +155 -111
  83. package/dist/esm/activities/generateImage/index.js.map +1 -1
  84. package/dist/esm/activities/generateSpeech/adapter.js +22 -15
  85. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  86. package/dist/esm/activities/generateSpeech/index.d.ts +4 -0
  87. package/dist/esm/activities/generateSpeech/index.js +159 -110
  88. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  89. package/dist/esm/activities/generateTranscription/adapter.js +22 -15
  90. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  91. package/dist/esm/activities/generateTranscription/index.d.ts +4 -0
  92. package/dist/esm/activities/generateTranscription/index.js +159 -100
  93. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  94. package/dist/esm/activities/generateVideo/adapter.js +36 -29
  95. package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
  96. package/dist/esm/activities/generateVideo/index.d.ts +143 -19
  97. package/dist/esm/activities/generateVideo/index.js +456 -279
  98. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  99. package/dist/esm/activities/generateVideo/snap.js +60 -48
  100. package/dist/esm/activities/generateVideo/snap.js.map +1 -1
  101. package/dist/esm/activities/index.js +8 -34
  102. package/dist/esm/activities/middleware/index.d.ts +1 -1
  103. package/dist/esm/activities/middleware/run.d.ts +10 -0
  104. package/dist/esm/activities/middleware/run.js +53 -29
  105. package/dist/esm/activities/middleware/run.js.map +1 -1
  106. package/dist/esm/activities/middleware/types.d.ts +44 -6
  107. package/dist/esm/activities/stream-generation-result.d.ts +4 -1
  108. package/dist/esm/activities/stream-generation-result.js +79 -44
  109. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  110. package/dist/esm/activities/summarize/adapter.js +22 -15
  111. package/dist/esm/activities/summarize/adapter.js.map +1 -1
  112. package/dist/esm/activities/summarize/chat-stream-summarize.js +252 -202
  113. package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
  114. package/dist/esm/activities/summarize/index.d.ts +27 -0
  115. package/dist/esm/activities/summarize/index.js +268 -102
  116. package/dist/esm/activities/summarize/index.js.map +1 -1
  117. package/dist/esm/adapter-internals.d.ts +2 -1
  118. package/dist/esm/adapter-internals.js +4 -11
  119. package/dist/esm/client.d.ts +25 -3
  120. package/dist/esm/client.js +131 -64
  121. package/dist/esm/client.js.map +1 -1
  122. package/dist/esm/custom-events.d.ts +76 -0
  123. package/dist/esm/custom-events.js +37 -0
  124. package/dist/esm/custom-events.js.map +1 -0
  125. package/dist/esm/delivery-detach.d.ts +50 -0
  126. package/dist/esm/delivery-detach.js +71 -0
  127. package/dist/esm/delivery-detach.js.map +1 -0
  128. package/dist/esm/delivery-disconnect.d.ts +62 -0
  129. package/dist/esm/delivery-disconnect.js +81 -0
  130. package/dist/esm/delivery-disconnect.js.map +1 -0
  131. package/dist/esm/extend-adapter.js +19 -17
  132. package/dist/esm/extend-adapter.js.map +1 -1
  133. package/dist/esm/index.d.ts +23 -5
  134. package/dist/esm/index.js +30 -97
  135. package/dist/esm/interrupt-resume.d.ts +71 -0
  136. package/dist/esm/interrupt-resume.js +438 -0
  137. package/dist/esm/interrupt-resume.js.map +1 -0
  138. package/dist/esm/interrupt-serialization.d.ts +12 -0
  139. package/dist/esm/interrupt-serialization.js +178 -0
  140. package/dist/esm/interrupt-serialization.js.map +1 -0
  141. package/dist/esm/interrupts.d.ts +84 -0
  142. package/dist/esm/interrupts.js +31 -0
  143. package/dist/esm/interrupts.js.map +1 -0
  144. package/dist/esm/locks.d.ts +10 -0
  145. package/dist/esm/locks.js +2 -0
  146. package/dist/esm/logger/console-logger.js +101 -78
  147. package/dist/esm/logger/console-logger.js.map +1 -1
  148. package/dist/esm/logger/internal-logger.js +104 -89
  149. package/dist/esm/logger/internal-logger.js.map +1 -1
  150. package/dist/esm/logger/resolve.js +54 -49
  151. package/dist/esm/logger/resolve.js.map +1 -1
  152. package/dist/esm/logger/types.d.ts +1 -1
  153. package/dist/esm/middlewares/content-guard.js +142 -148
  154. package/dist/esm/middlewares/content-guard.js.map +1 -1
  155. package/dist/esm/middlewares/index.js +2 -6
  156. package/dist/esm/middlewares/otel.js +598 -732
  157. package/dist/esm/middlewares/otel.js.map +1 -1
  158. package/dist/esm/middlewares/usage-attributes.js +47 -40
  159. package/dist/esm/middlewares/usage-attributes.js.map +1 -1
  160. package/dist/esm/realtime/event-emitter.js +24 -25
  161. package/dist/esm/realtime/event-emitter.js.map +1 -1
  162. package/dist/esm/realtime/index.d.ts +5 -9
  163. package/dist/esm/realtime/index.js +29 -6
  164. package/dist/esm/realtime/index.js.map +1 -1
  165. package/dist/esm/scope.d.ts +47 -0
  166. package/dist/esm/stream-durability.d.ts +171 -0
  167. package/dist/esm/stream-durability.js +295 -0
  168. package/dist/esm/stream-durability.js.map +1 -0
  169. package/dist/esm/stream-to-response.d.ts +178 -13
  170. package/dist/esm/stream-to-response.js +663 -115
  171. package/dist/esm/stream-to-response.js.map +1 -1
  172. package/dist/esm/strip-to-spec-middleware.js +30 -16
  173. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  174. package/dist/esm/system-prompts.js +27 -21
  175. package/dist/esm/system-prompts.js.map +1 -1
  176. package/dist/esm/tool-registry.js +72 -45
  177. package/dist/esm/tool-registry.js.map +1 -1
  178. package/dist/esm/tools/provider-tool.js +14 -5
  179. package/dist/esm/tools/provider-tool.js.map +1 -1
  180. package/dist/esm/types.d.ts +332 -21
  181. package/dist/esm/types.js +2 -0
  182. package/dist/esm/utilities/ag-ui-wire.js +79 -93
  183. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  184. package/dist/esm/utilities/chat-params.d.ts +26 -4
  185. package/dist/esm/utilities/chat-params.js +218 -92
  186. package/dist/esm/utilities/chat-params.js.map +1 -1
  187. package/dist/esm/utilities/errors.js +28 -18
  188. package/dist/esm/utilities/errors.js.map +1 -1
  189. package/dist/esm/utilities/media-prompt.js +46 -41
  190. package/dist/esm/utilities/media-prompt.js.map +1 -1
  191. package/dist/esm/utilities/numbers.js +13 -10
  192. package/dist/esm/utilities/numbers.js.map +1 -1
  193. package/dist/esm/utilities/provider-executed.js +20 -11
  194. package/dist/esm/utilities/provider-executed.js.map +1 -1
  195. package/dist/esm/utilities/sampling-keys.js +31 -19
  196. package/dist/esm/utilities/sampling-keys.js.map +1 -1
  197. package/dist/esm/utilities/tool-result.js +42 -30
  198. package/dist/esm/utilities/tool-result.js.map +1 -1
  199. package/dist/esm/utilities/usage.js +27 -9
  200. package/dist/esm/utilities/usage.js.map +1 -1
  201. package/dist/esm/utils.js +26 -18
  202. package/dist/esm/utils.js.map +1 -1
  203. package/package.json +10 -6
  204. package/skills/ai-core/SKILL.md +69 -18
  205. package/skills/ai-core/adapter-configuration/SKILL.md +44 -21
  206. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +1 -3
  207. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +148 -0
  208. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -6
  209. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +2 -6
  210. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +1 -3
  211. package/skills/ai-core/ag-ui-protocol/SKILL.md +1 -1
  212. package/skills/ai-core/chat-experience/SKILL.md +156 -11
  213. package/skills/ai-core/client-persistence/SKILL.md +277 -0
  214. package/skills/ai-core/custom-backend-integration/SKILL.md +1 -1
  215. package/skills/ai-core/debug-logging/SKILL.md +1 -1
  216. package/skills/ai-core/locks/SKILL.md +143 -0
  217. package/skills/ai-core/media-generation/SKILL.md +144 -12
  218. package/skills/ai-core/middleware/SKILL.md +258 -33
  219. package/skills/ai-core/structured-outputs/SKILL.md +1 -1
  220. package/skills/ai-core/tool-calling/SKILL.md +54 -59
  221. package/src/activities/chat/agent-loop-strategies.ts +10 -4
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1152 -153
  224. package/src/activities/chat/mcp/manager.ts +4 -4
  225. package/src/activities/chat/mcp/types.ts +2 -2
  226. package/src/activities/chat/messages.ts +5 -3
  227. package/src/activities/chat/middleware/builder.ts +1 -1
  228. package/src/activities/chat/middleware/compose.ts +186 -9
  229. package/src/activities/chat/middleware/index.ts +26 -0
  230. package/src/activities/chat/middleware/locks.ts +102 -0
  231. package/src/activities/chat/middleware/pending-turn.ts +47 -0
  232. package/src/activities/chat/middleware/run-disconnect.ts +62 -0
  233. package/src/activities/chat/middleware/run-store.ts +412 -0
  234. package/src/activities/chat/middleware/types.ts +62 -1
  235. package/src/activities/chat/stream/processor.ts +189 -5
  236. package/src/activities/chat/tools/approval-schema.ts +205 -0
  237. package/src/activities/chat/tools/tool-calls.ts +106 -13
  238. package/src/activities/chat/tools/tool-definition.ts +210 -39
  239. package/src/activities/generateAudio/index.ts +20 -3
  240. package/src/activities/generateImage/index.ts +20 -3
  241. package/src/activities/generateSpeech/index.ts +25 -3
  242. package/src/activities/generateTranscription/index.ts +26 -3
  243. package/src/activities/generateVideo/index.ts +345 -82
  244. package/src/activities/middleware/index.ts +2 -0
  245. package/src/activities/middleware/run.ts +31 -0
  246. package/src/activities/middleware/types.ts +49 -5
  247. package/src/activities/stream-generation-result.ts +30 -2
  248. package/src/activities/summarize/chat-stream-summarize.ts +5 -0
  249. package/src/activities/summarize/index.ts +200 -10
  250. package/src/adapter-internals.ts +10 -1
  251. package/src/client.ts +244 -0
  252. package/src/custom-events.ts +107 -0
  253. package/src/delivery-detach.ts +72 -0
  254. package/src/delivery-disconnect.ts +84 -0
  255. package/src/index.ts +138 -0
  256. package/src/interrupt-resume.ts +824 -0
  257. package/src/interrupt-serialization.ts +183 -0
  258. package/src/interrupts.ts +146 -0
  259. package/src/locks.ts +17 -0
  260. package/src/logger/types.ts +1 -1
  261. package/src/middlewares/otel.ts +1 -0
  262. package/src/realtime/index.ts +5 -9
  263. package/src/scope.ts +47 -0
  264. package/src/stream-durability.ts +598 -0
  265. package/src/stream-to-response.ts +1051 -95
  266. package/src/strip-to-spec-middleware.ts +3 -3
  267. package/src/types.ts +416 -24
  268. package/src/utilities/chat-params.ts +245 -55
  269. package/dist/esm/activities/index.js.map +0 -1
  270. package/dist/esm/adapter-internals.js.map +0 -1
  271. package/dist/esm/index.js.map +0 -1
  272. 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.10.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.2'),
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.2'),
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
@@ -409,6 +428,131 @@ export const Route = createFileRoute('/api/chat')({
409
428
  })
410
429
  ```
411
430
 
431
+ ### 7. Queueing Messages Sent While Streaming
432
+
433
+ By default, a `sendMessage` call that arrives while a stream is in flight is
434
+ **queued** and sent automatically once the run settles **successfully** —
435
+ this is a behavior change: such sends used to be silently dropped. Configure
436
+ it with the `queue` option on `useChat`:
437
+
438
+ ```typescript
439
+ import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
440
+
441
+ const { messages, queue, sendMessage, cancelQueued, isLoading } = useChat({
442
+ connection: fetchServerSentEvents('/api/chat'),
443
+ queue: { whenBusy: 'queue', drain: 'fifo', maxSize: 5, onOverflow: 'reject' },
444
+ })
445
+ ```
446
+
447
+ - **`whenBusy`** — `'queue'` (default) holds the message until a successful
448
+ settle; `'drop'` ignores the send (never appears in `queue`/`messages`);
449
+ `'interrupt'` aborts the current stream and sends immediately (unlike
450
+ `stop()`, does **not** flush already-queued items — they drain after the
451
+ interrupting send **succeeds**).
452
+ - **`drain`** — `'fifo'` (default) sends queued items one at a time in
453
+ order; `'batch'` merges everything queued into a single send once the
454
+ run settles successfully.
455
+ - **`maxSize`** / **`onOverflow`** — cap the queue length; `'reject'`
456
+ (default) silently ignores overflow sends (does not throw),
457
+ `'drop-oldest'` evicts the oldest queued item to make room.
458
+
459
+ The top-level `queue` option also accepts a plain `WhenBusy` string
460
+ shorthand (e.g. `queue: 'interrupt'`) or a `QueueStrategy` function for
461
+ per-send action control. Strategy form always drains FIFO; actions are
462
+ `'queue' | 'drop' | 'interrupt'`.
463
+
464
+ **Drain vs flush:** queued messages auto-send only after a **successful**
465
+ settle. They are **discarded** on stream error/abort of the active
466
+ generation, `stop()`, `clear()`, `unsubscribe()`, and `reload()`.
467
+ `interrupt` does not flush.
468
+
469
+ `queue: Array<QueuedMessage>` (`{ id, content, createdAt }`) is separate
470
+ from `messages` — render pending sends distinctly and cancel with
471
+ `cancelQueued(id)`:
472
+
473
+ ```typescript
474
+ {queue.map((q) => (
475
+ <div key={q.id}>
476
+ {typeof q.content === 'string' ? q.content : '[attachment]'}
477
+ <button onClick={() => cancelQueued(q.id)}>Cancel</button>
478
+ </div>
479
+ ))}
480
+ ```
481
+
482
+ Override the configured policy for a single send with the second argument
483
+ to `sendMessage`:
484
+
485
+ ```typescript
486
+ sendMessage('Never mind, do this instead', { whenBusy: 'interrupt' })
487
+ ```
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
+
412
556
  ## Common Mistakes
413
557
 
414
558
  ### a. CRITICAL: Using Vercel AI SDK patterns (streamText, generateText)
@@ -417,12 +561,12 @@ export const Route = createFileRoute('/api/chat')({
417
561
  // WRONG
418
562
  import { streamText } from 'ai'
419
563
  import { openai } from '@ai-sdk/openai'
420
- const result = streamText({ model: openai('gpt-4o'), messages })
564
+ const result = streamText({ model: openai('gpt-5.5'), messages })
421
565
 
422
566
  // CORRECT
423
567
  import { chat } from '@tanstack/ai'
424
568
  import { openaiText } from '@tanstack/ai-openai'
425
- const stream = chat({ adapter: openaiText('gpt-5.2'), messages })
569
+ const stream = chat({ adapter: openaiText('gpt-5.5'), messages })
426
570
  ```
427
571
 
428
572
  ### b. CRITICAL: Using Vercel createOpenAI() provider pattern
@@ -431,12 +575,12 @@ const stream = chat({ adapter: openaiText('gpt-5.2'), messages })
431
575
  // WRONG
432
576
  import { createOpenAI } from '@ai-sdk/openai'
433
577
  const openai = createOpenAI({ apiKey })
434
- streamText({ model: openai('gpt-4o'), messages })
578
+ streamText({ model: openai('gpt-5.5'), messages })
435
579
 
436
580
  // CORRECT
437
581
  import { openaiText } from '@tanstack/ai-openai'
438
582
  import { chat } from '@tanstack/ai'
439
- chat({ adapter: openaiText('gpt-5.2'), messages })
583
+ chat({ adapter: openaiText('gpt-5.5'), messages })
440
584
  ```
441
585
 
442
586
  ### c. CRITICAL: Using monolithic openai() instead of openaiText()
@@ -444,11 +588,11 @@ chat({ adapter: openaiText('gpt-5.2'), messages })
444
588
  ```typescript
445
589
  // WRONG
446
590
  import { openai } from '@tanstack/ai-openai'
447
- chat({ adapter: openai(), model: 'gpt-5.2', messages })
591
+ chat({ adapter: openai(), model: 'gpt-5.5', messages })
448
592
 
449
593
  // CORRECT
450
594
  import { openaiText } from '@tanstack/ai-openai'
451
- chat({ adapter: openaiText('gpt-5.2'), messages })
595
+ chat({ adapter: openaiText('gpt-5.5'), messages })
452
596
  ```
453
597
 
454
598
  The monolithic `openai()` adapter is deprecated. Use tree-shakeable adapters:
@@ -470,10 +614,10 @@ return toServerSentEventsResponse(stream, { abortController })
470
614
 
471
615
  ```typescript
472
616
  // WRONG
473
- chat({ adapter: openaiText(), model: 'gpt-5.2', messages })
617
+ chat({ adapter: openaiText(), model: 'gpt-5.5', messages })
474
618
 
475
619
  // CORRECT
476
- chat({ adapter: openaiText('gpt-5.2'), messages })
620
+ chat({ adapter: openaiText('gpt-5.5'), messages })
477
621
  ```
478
622
 
479
623
  The model is passed to the adapter factory, not to `chat()`.
@@ -620,3 +764,4 @@ If not handled, the UI appears to hang with no feedback.
620
764
  - See also: **ai-core/tool-calling/SKILL.md** -- Most chats include tools
621
765
  - See also: **ai-core/adapter-configuration/SKILL.md** -- Adapter choice affects available features
622
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
@@ -9,7 +9,7 @@ description: >
9
9
  not @tanstack/ai-client.
10
10
  type: composition
11
11
  library: tanstack-ai
12
- library_version: '0.10.0'
12
+ library_version: '0.42.0'
13
13
  sources:
14
14
  - 'TanStack/ai:docs/chat/connection-adapters.md'
15
15
  ---
@@ -9,7 +9,7 @@ description: >
9
9
  log by default even when `debug` is omitted; silence with `debug: false`.
10
10
  type: sub-skill
11
11
  library: tanstack-ai
12
- library_version: '0.10.0'
12
+ library_version: '0.42.0'
13
13
  sources:
14
14
  - 'TanStack/ai:docs/advanced/debug-logging.md'
15
15
  ---