@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.
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 +5 -36
  5. package/dist/esm/activities/chat/agent-loop-strategies.js +75 -21
  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 -21
  11. package/dist/esm/activities/chat/index.js +2100 -1813
  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 +24 -6
  134. package/dist/esm/index.js +30 -98
  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 +321 -42
  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 +98 -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 -61
  221. package/src/activities/chat/agent-loop-strategies.ts +5 -39
  222. package/src/activities/chat/cancel.ts +81 -0
  223. package/src/activities/chat/index.ts +1091 -200
  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 -1
  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 +405 -45
  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
@@ -2,16 +2,16 @@
2
2
  name: ai-core/media-generation
3
3
  description: >
4
4
  Image, audio, video, speech (TTS), and transcription generation using
5
- activity-specific adapters: generateImage() with openaiImage/geminiImage,
5
+ activity-specific adapters: generateImage() with openaiImage/geminiImage/byteplusImage,
6
6
  generateAudio() with geminiAudio/falAudio, generateVideo() with async
7
- polling (openaiVideo/geminiVideo/grokVideo/falVideo, per-model typed
8
- durations), generateSpeech() with openaiSpeech, generateTranscription()
9
- with openaiTranscription. React hooks: useGenerateImage, useGenerateAudio,
7
+ polling (openaiVideo/geminiVideo/grokVideo/falVideo/byteplusVideo, per-model typed
8
+ durations), generateSpeech() with openaiSpeech/byteplusSpeech, generateTranscription()
9
+ with openaiTranscription/byteplusTranscription. React hooks: useGenerateImage, useGenerateAudio,
10
10
  useGenerateSpeech, useTranscription, useGenerateVideo.
11
11
  TanStack Start server function integration with toServerSentEventsResponse.
12
12
  type: sub-skill
13
13
  library: tanstack-ai
14
- library_version: '0.10.0'
14
+ library_version: '0.42.0'
15
15
  sources:
16
16
  - 'TanStack/ai:docs/media/generations.md'
17
17
  - 'TanStack/ai:docs/media/generation-hooks.md'
@@ -150,8 +150,16 @@ function ImageGenerator() {
150
150
  ### 1. Image Generation
151
151
 
152
152
  Supported adapters: `openaiImage` (dall-e-2, dall-e-3, gpt-image-1,
153
- gpt-image-1-mini, gpt-image-2) and `geminiImage` (gemini-3.1-flash-image-preview,
154
- gemini-3.1-flash-lite-image, imagen-4.0-generate-001, etc.).
153
+ gpt-image-1-mini, gpt-image-2), `geminiImage` (gemini-3.1-flash-image-preview,
154
+ gemini-3.1-flash-lite-image, imagen-4.0-generate-001, etc.) and `byteplusImage`
155
+ (Seedream — `seedream-4-0-250828`, `seedream-4-5-251128`, the 5.0 family).
156
+
157
+ > **Seedream quirks:** `watermark` defaults to **`true`** (pass
158
+ > `modelOptions: { watermark: false }` for a clean image), `size` is a token
159
+ > (`'1K'` | `'2K'` | `'4K'`) **or** explicit `'2048x2048'` pixels but never a
160
+ > mix, and `numberOfImages` is an **upper bound** — Seedream has no `n`, so it
161
+ > maps onto group-image mode and the model may return fewer. Reads
162
+ > `ARK_API_KEY`.
155
163
 
156
164
  ```typescript
157
165
  import { generateImage } from '@tanstack/ai'
@@ -333,7 +341,19 @@ const { generate, result, isLoading } = useGenerateAudio({
333
341
 
334
342
  ### 3. Text-to-Speech
335
343
 
336
- Adapter: `openaiSpeech` (tts-1, tts-1-hd, gpt-4o-audio-preview).
344
+ Adapters: `openaiSpeech` (tts-1, tts-1-hd, gpt-4o-audio-preview) and
345
+ `byteplusSpeech` (`seed-audio-1.0`).
346
+
347
+ > **BytePlus Seed Speech is a separate product from ModelArk** — it reads
348
+ > **`BYTEPLUS_VOICE_API_KEY`**, not `ARK_API_KEY`, and an Ark key there fails
349
+ > with `45000010 Invalid X-Api-Key`. Output is capped at **120 seconds**.
350
+ > There is no top-level `speaker` field — `voice` is sent as
351
+ > `references: [{ speaker }]`, and `modelOptions.references` **replaces** that
352
+ > array rather than merging, so passing `references` for voice cloning silently
353
+ > drops `voice`. Voice ids ending `_uranus_bigtts` are TTS 2.0,
354
+ > `_mars_bigtts` / `_moon_bigtts` are TTS 1.0, and `*_emo_v2_*` are the 1.0
355
+ > voices that accept emotion tags. Formats: `wav`, `mp3`, `pcm`, `ogg_opus`;
356
+ > `watermark` is also available on `modelOptions`.
337
357
 
338
358
  ```typescript
339
359
  import { generateSpeech } from '@tanstack/ai'
@@ -367,8 +387,10 @@ const { generate, result, isLoading } = useGenerateSpeech({
367
387
 
368
388
  ### 4. Audio Transcription
369
389
 
370
- Adapter: `openaiTranscription` (whisper-1, gpt-4o-transcribe,
371
- gpt-4o-mini-transcribe, gpt-4o-transcribe-diarize).
390
+ Adapters: `openaiTranscription` (whisper-1, gpt-4o-transcribe,
391
+ gpt-4o-mini-transcribe, gpt-4o-transcribe-diarize) and `byteplusTranscription`
392
+ (`seed-asr` — synchronous, no polling; audio up to 2 hours / 100 MB; also reads
393
+ **`BYTEPLUS_VOICE_API_KEY`**).
372
394
 
373
395
  > **Capturing audio in the browser:** Use `useAudioRecorder` from `@tanstack/ai-react` to record directly in the browser, then pass the recording as the `audio` input to `generate()`, or use `recording.part` as a prompt part in chat/generation calls. No transcoding or extra dependencies required — the recorder returns the native browser format (`audio/webm` or `audio/mp4`). For transcription, wrap it as a `data:` URL so the provider gets the real content type; passing raw `recording.base64` makes the adapter assume `audio/mpeg` and mislabel the webm/mp4 bytes.
374
396
  >
@@ -517,7 +539,20 @@ durations 4/8/12s, single `input_reference` image prompt part), `grokVideo(...)`
517
539
  (`grok-imagine-video` does text-to-video + image-to-video; `grok-imagine-video-1.5` is
518
540
  image-to-video only — needs an `image` prompt part as the starting frame, text-only throws;
519
541
  aspect-ratio size template like `'16:9_720p'`, integer durations 1-15s, reports
520
- `usage.unitsBilled` seconds and exact `usage.cost`), and `falVideo(...)` (hosted models, see cost tracking below).
542
+ `usage.unitsBilled` seconds and exact `usage.cost`), `byteplusVideo(...)` (Seedance —
543
+ aspect-ratio size template like `'16:9_720p'`, durations 4-15s on the 2.0 family,
544
+ 4-12s on 1.5-pro, 2-12s on the 1.0-pro models; reads `ARK_API_KEY`), and
545
+ `falVideo(...)` (hosted models, see cost tracking below).
546
+
547
+ > **Seedance option applicability is per model and enforced server-side** —
548
+ > Ark returns a 400 for an inapplicable field rather than ignoring it.
549
+ > `service_tier` / `camera_fixed` are Seedance 1.x only, `frames` is
550
+ > 1-0-pro + 1-0-pro-fast only, `draft` is 1-5-pro only, `priority` is the 2.0
551
+ > family only, and `duration: -1` works on 2.0 + 1-5-pro. There is no 2K tier
552
+ > on any model and `4k` exists only on `dreamina-seedance-2-0-260128`.
553
+ > **Video URLs expire 24 hours after the task completes** (task record kept 7
554
+ > days). Seedance is also reachable via `falVideo` — `byteplusVideo` is the
555
+ > direct-to-BytePlus path.
521
556
 
522
557
  Client hook with job tracking:
523
558
 
@@ -563,6 +598,94 @@ if (result.usage?.unitsBilled != null) {
563
598
  For video, the units arrive with the completed result: `getVideoJobStatus()`
564
599
  returns `usage` and emits a `video:usage` devtools event when fal reports it.
565
600
 
601
+ ### 7. Durable persistence (job lifecycle + artifact bytes)
602
+
603
+ To make generations survive a server restart and be re-served later, add
604
+ `withGenerationPersistence` from `@tanstack/ai-persistence` as generation
605
+ middleware. It requires `stores.generationRuns` (a `GenerationRunStore`, keyed on
606
+ the run's own `runId`, with a required `threadId` naming the stable slot the run
607
+ fills — that is what a client hydrates by) and, when you also pass an `stores.artifacts` +
608
+ `stores.blobs` **pair** (both or neither), it persists the generated media bytes
609
+ at blob key `artifacts/<runId>/<artifactId>` with an `ArtifactRecord` per file.
610
+ `memoryPersistence()` ships all three for dev/tests.
611
+
612
+ ```typescript
613
+ import { generateImage, toServerSentEventsResponse } from '@tanstack/ai'
614
+ import { openaiImage } from '@tanstack/ai-openai'
615
+ import {
616
+ withGenerationPersistence,
617
+ memoryPersistence,
618
+ retrieveArtifact,
619
+ retrieveBlob,
620
+ reconstructGeneration,
621
+ } from '@tanstack/ai-persistence'
622
+
623
+ const persistence = memoryPersistence() // swap for your DB/object-store adapter
624
+
625
+ export async function POST(req: Request) {
626
+ const { prompt, threadId } = await req.json()
627
+ return toServerSentEventsResponse(
628
+ generateImage({
629
+ adapter: openaiImage('gpt-image-1'),
630
+ prompt,
631
+ threadId, // the slot recorded on the job + artifacts
632
+ stream: true,
633
+ middleware: [
634
+ withGenerationPersistence(persistence, {
635
+ // Stamp a durable app-origin serve URL (the GET route below) onto
636
+ // each persisted artifact ref, and rewrite the live result's media to
637
+ // it. Both live and restored results then render from your origin.
638
+ artifactUrl: (ref) => `/api/artifacts?id=${ref.artifactId}`,
639
+ }),
640
+ ],
641
+ }),
642
+ )
643
+ }
644
+
645
+ // Serve the stored bytes back (GET /api/artifacts?id=…):
646
+ export async function GET(req: Request) {
647
+ const id = new URL(req.url).searchParams.get('id') ?? ''
648
+ const record = await retrieveArtifact(persistence, id)
649
+ if (!record) return new Response('Not found', { status: 404 })
650
+ const blob = await retrieveBlob(persistence, record)
651
+ if (!blob?.body) return new Response('Not found', { status: 404 })
652
+ return new Response(blob.body, {
653
+ headers: { 'content-type': record.mimeType },
654
+ })
655
+ }
656
+ ```
657
+
658
+ That route is enough for images. **Video needs `Range`**: seeking a `<video>`
659
+ is built on `206` / `Content-Range`, and Safari refuses to play a source that
660
+ ignores `Range` at all. Resolve the header against `record.size` (`416` when it
661
+ does not fit), pass `retrieveBlob(persistence, record, { range })`, and answer
662
+ `206` from the returned `blob.range` plus `accept-ranges: bytes`. The full
663
+ route is in the persistence docs under **Serve video: honour `Range`**.
664
+
665
+ On the client, `persistence` is **boolean only**: `persistence: true` hydrates
666
+ the last generation for the thread on mount, via the connection's
667
+ `hydrateGeneration` handler backed by a `reconstructGeneration` GET route. There
668
+ is no storage-adapter mode, so nothing about a generation is cached in the
669
+ browser.
670
+
671
+ **`persistence: true` requires a stable `threadId`**, and it is a type error to
672
+ set one without the other. That is the generation's scope, the slot successive
673
+ runs fill (e.g. `video-9-start-frame`), not a link to a chat. `id` is the
674
+ devtools label only and never a persistence key.
675
+
676
+ The hooks are transparent (like `useChat`): a reload repaints `status` /
677
+ `result` / `error`, not a separate `resumeSnapshot`. Because the `artifactUrl`
678
+ above stamps a durable URL onto each ref (carried on `result.artifacts`), the
679
+ restored `result` rebuilds its media from those refs and serves from your own
680
+ origin. Without byte storage, a reload restores `status` / `error` and `result`
681
+ stays `null`.
682
+
683
+ - Building the R2/D1-backed byte stores for a Cloudflare Worker:
684
+ **ai-persistence/build-cloudflare-artifact-store**.
685
+ - Store contracts, `composePersistence`, and the wiring end-to-end:
686
+ `docs/persistence/generation-persistence.md` and the
687
+ `ai-core/client-persistence` sub-skill.
688
+
566
689
  ---
567
690
 
568
691
  ## Common Hook API
@@ -577,7 +700,16 @@ All generation hooks return the same shape:
577
700
  | `error` | `Error \| undefined` | Current error |
578
701
  | `status` | `GenerationClientState` | `'idle' \| 'generating' \| 'success' \| 'error'` |
579
702
  | `stop` | `() => void` | Abort current generation |
580
- | `reset` | `() => void` | Clear state, return to idle |
703
+ | `reset` | `() => void` | Clear state and the in-memory snapshot |
704
+ | `runId` | `string \| null` | Id of the job WHILE it runs; null when idle |
705
+
706
+ The hook is **transparent**, mirroring `useChat`: there is no `resumeSnapshot`,
707
+ `resumeState`, `pendingArtifacts`, or `resultArtifacts` field. Hooks also accept
708
+ `persistence: true` plus a stable `threadId`: on mount the client hydrates the
709
+ last run for that scope from the server and repaints the **normal** `status` /
710
+ `result` / `error` fields, so the last run survives a reload (metadata only,
711
+ never media bytes; `result`'s media returns only with server byte storage +
712
+ `artifactUrl`). See `ai-core/client-persistence` for details.
581
713
 
582
714
  Provide either `connection` (streaming SSE transport) or `fetcher`
583
715
  (direct async call / server function returning `Response`). Use `onResult`
@@ -8,10 +8,11 @@ description: >
8
8
  execution order. NOT onEnd/onFinish callbacks on chat() — use middleware.
9
9
  type: sub-skill
10
10
  library: tanstack-ai
11
- library_version: '0.10.0'
11
+ library_version: '0.42.0'
12
12
  sources:
13
13
  - 'TanStack/ai:docs/advanced/middleware.md'
14
14
  - 'TanStack/ai:docs/sandbox/observability.md'
15
+ - 'TanStack/ai:docs/persistence/overview.md'
15
16
  ---
16
17
 
17
18
  # Middleware
@@ -57,6 +58,7 @@ Every hook receives a `ChatMiddlewareContext` as its first argument, which provi
57
58
  | `onStructuredOutputConfig` | Once at the structured-output boundary (only when `chat({ outputSchema })`) | `StructuredOutputMiddlewareConfig` (return partial) |
58
59
  | `onStart` | Once after initial `onConfig` | none |
59
60
  | `onIteration` | Start of each agent loop iteration | `IterationInfo` |
61
+ | `onShouldContinue` | Whether to start another agent-loop iteration (AND with strategy; `false` stops) | `AgentLoopState` |
60
62
  | `onChunk` | Every streamed chunk | `StreamChunk` (return void/chunk/chunk[]/null) |
61
63
  | `onBeforeToolCall` | Before each tool executes | `ToolCallHookContext` (return decision or void) |
62
64
  | `onAfterToolCall` | After each tool executes | `AfterToolCallInfo` |
@@ -116,17 +118,25 @@ onStructuredOutputConfig?: (
116
118
  | void
117
119
  | null
118
120
  | Partial<StructuredOutputMiddlewareConfig>
119
- | Promise<void | Partial<StructuredOutputMiddlewareConfig>>
121
+ | Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>
120
122
  ```
121
123
 
122
124
  **`StructuredOutputMiddlewareConfig` shape:**
123
125
 
124
126
  ```ts
125
- interface StructuredOutputMiddlewareConfig extends ChatMiddlewareConfig {
127
+ interface StructuredOutputMiddlewareConfig extends Omit<
128
+ ChatMiddlewareConfig,
129
+ 'tools'
130
+ > {
126
131
  outputSchema: JSONSchema // The JSON Schema being sent to the provider
127
132
  }
128
133
  ```
129
134
 
135
+ Note the `Omit<…, 'tools'>`: there is **no `config.tools`** on this hook. The
136
+ structured-output call is the final, tool-free call, so reading or returning
137
+ `tools` here is a compile error, not a no-op. Transform tools in `onConfig`
138
+ instead.
139
+
130
140
  **Ordering rule:**
131
141
 
132
142
  - `onStructuredOutputConfig` fires **before** `onConfig` at the structured-output boundary.
@@ -211,11 +221,18 @@ const toolGuard: ChatMiddleware = {
211
221
  return { type: 'abort', reason: 'Dangerous operation blocked' }
212
222
  }
213
223
 
214
- // Enforce default arguments
215
- if (hookCtx.toolName === 'search' && !hookCtx.args.limit) {
216
- return {
217
- type: 'transformArgs',
218
- args: { ...hookCtx.args, limit: 10 },
224
+ // Enforce default arguments. `hookCtx.args` is `unknown` — the provider
225
+ // sent it — so narrow before reading it. No `as` casts.
226
+ if (hookCtx.toolName === 'search') {
227
+ const args =
228
+ typeof hookCtx.args === 'object' && hookCtx.args !== null
229
+ ? hookCtx.args
230
+ : {}
231
+ if (!('limit' in args)) {
232
+ return {
233
+ type: 'transformArgs',
234
+ args: { ...args, limit: 10 },
235
+ }
219
236
  }
220
237
  }
221
238
 
@@ -346,6 +363,52 @@ const stream = chat({
346
363
  | `onUsage` | Sequential | All run in order |
347
364
  | `onFinish/onAbort/onError` | Sequential | All run in order |
348
365
 
366
+ ## Pattern: tool-call budget (app-owned)
367
+
368
+ Not a built-in. Cap fan-out with `onBeforeToolCall` skip + `onShouldContinue`.
369
+ See `docs/chat/agentic-cycle.md` ("Tool-call budgets").
370
+
371
+ ```typescript
372
+ import { chat, maxIterations, type ChatMiddleware } from '@tanstack/ai'
373
+
374
+ function toolCallBudget(opts: {
375
+ max?: number
376
+ maxPerTurn?: number
377
+ }): ChatMiddleware {
378
+ let perTurn = 0
379
+ return {
380
+ onIteration: () => {
381
+ perTurn = 0
382
+ },
383
+ onToolPhaseComplete: () => {
384
+ perTurn = 0
385
+ },
386
+ onBeforeToolCall: () => {
387
+ if (opts.maxPerTurn == null) return undefined
388
+ if (++perTurn > opts.maxPerTurn) {
389
+ return {
390
+ type: 'skip',
391
+ result: {
392
+ error: `Skipped: exceeded maxToolCallsPerTurn (${opts.maxPerTurn})`,
393
+ },
394
+ }
395
+ }
396
+ return undefined
397
+ },
398
+ onShouldContinue: (_ctx, state) =>
399
+ opts.max != null && state.toolCallCount >= opts.max ? false : undefined,
400
+ }
401
+ }
402
+
403
+ chat({
404
+ adapter,
405
+ messages,
406
+ tools: [weatherTool],
407
+ agentLoopStrategy: maxIterations(20),
408
+ middleware: [toolCallBudget({ maxPerTurn: 10, max: 20 })],
409
+ })
410
+ ```
411
+
349
412
  ## Built-in: toolCacheMiddleware
350
413
 
351
414
  Caches tool call results by name + arguments. Import from `@tanstack/ai/middlewares`:
@@ -372,6 +435,148 @@ Options: `maxSize` (default 100), `ttl` (default Infinity), `toolNames` (default
372
435
  `keyFn` (custom cache key), `storage` (custom backend like Redis). See
373
436
  `docs/advanced/middleware.md` for custom storage examples.
374
437
 
438
+ ## Server State Persistence: withPersistence
439
+
440
+ `withPersistence(persistence)` (from `@tanstack/ai-persistence`) is a
441
+ `ChatMiddleware` that persists **state** for `chat()` — thread messages, run
442
+ records (status/timing/usage/errors), and interrupt state — to a backend store.
443
+ Add it to the `middleware` array like any other middleware. It never mutates the
444
+ chunk stream; replaying a dropped/reloaded _stream_ is a separate transport-layer
445
+ concern (see ai-core/chat-experience/SKILL.md resumability, not this middleware).
446
+
447
+ ```typescript
448
+ import {
449
+ chat,
450
+ chatParamsFromRequest,
451
+ toServerSentEventsResponse,
452
+ } from '@tanstack/ai'
453
+ import { openaiText } from '@tanstack/ai-openai'
454
+ import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'
455
+
456
+ // memoryPersistence() is the in-process reference backend (dev/tests). For a
457
+ // durable one, implement the store contracts against your database — see the
458
+ // @tanstack/ai-persistence skills.
459
+ const persistence = memoryPersistence()
460
+
461
+ export async function POST(request: Request) {
462
+ const params = await chatParamsFromRequest(request)
463
+
464
+ const stream = chat({
465
+ adapter: openaiText('gpt-5.5'),
466
+ messages: params.messages,
467
+ threadId: params.threadId,
468
+ runId: params.runId,
469
+ ...(params.resume ? { resume: params.resume } : {}),
470
+ middleware: [withPersistence(persistence)],
471
+ })
472
+
473
+ return toServerSentEventsResponse(stream)
474
+ }
475
+ ```
476
+
477
+ ### Authoritative-history contract
478
+
479
+ The middleware treats each request's `messages` as the source of truth for the
480
+ thread:
481
+
482
+ - **Non-empty `messages`** → on a successful finish (and at an interrupt
483
+ boundary) the middleware **overwrites** the entire stored thread with that
484
+ array. Post the **complete** transcript, never just the newest message(s) — a
485
+ delta would replace and destroy the stored history.
486
+ - **Empty `messages`** → the middleware **loads** the stored thread and runs the
487
+ turn from the server's copy. This is how you continue a conversation without
488
+ resending history from the client.
489
+
490
+ ### Backends
491
+
492
+ `@tanstack/ai-persistence` ships **contracts, not a database backend**. It
493
+ provides the four store interfaces (`messages`, `runs`, `interrupts`,
494
+ `metadata`), the middleware that drives them, `memoryPersistence()` for
495
+ dev/tests, and a conformance testkit. For anything durable you implement the
496
+ stores against your own database and pass the result to `withPersistence`.
497
+
498
+ Annotate your factory with a named shape (`ChatPersistence` /
499
+ `ChatTranscriptPersistence`) — bare `AIPersistence` is the all-optional bag and
500
+ `withPersistence` rejects it.
501
+
502
+ Locks are separate from state and are **not** a `stores` key: wire a
503
+ `LockStore` with `withLocks(lockStore)`.
504
+
505
+ The `runs` store in that list is typed against `RunStore`, which ships in
506
+ `@tanstack/ai` alongside `RunRecord`, `RunStatus`, `TerminalRunStatus`,
507
+ `RunError`, `isTerminalRunStatus`, `defineRunStore`, and `InMemoryRunStore`. A
508
+ `RunRecord` tracks one run: `runId`, `threadId`, `status`, `startedAt`, plus
509
+ optional `finishedAt`, `error`, `usage`, `sandboxKey`, `detachedSince`,
510
+ `cancelRequested`, and `driverEpoch`. A backend must round-trip **all** of
511
+ them: `cancelRequested` is the durable out-of-band cancel channel
512
+ (`requestRunCancel` writes it, `wasCancelRequested` reads it), and
513
+ `driverEpoch` is the monotonic fencing token each host bumps when it claims a
514
+ run, so a superseded host can discover it lost. Omit either and a durable
515
+ sandboxed run loses a mechanism silently — Stop stops reaching a remote
516
+ driver, or nothing fences a dead host's writes.
517
+ `error` is a structured `RunError` (`{ message: string, code?: string }`), not
518
+ a bare string: `message` is the provider's prose, `code` is the stable,
519
+ machine-branchable classification a consumer switches on. Only
520
+ `createOrResume`, `update`, `get`, and `findActiveRun` are required on a
521
+ `RunStore`; `listByThread` and `listReclaimable` are optional, so a backend can
522
+ leave either out and callers feature-detect
523
+ (`store.listReclaimable?.(opts)`). Shape your own store with
524
+ `defineRunStore` for autocomplete without a separate `: RunStore` annotation,
525
+ matching `defineLock`; `defineRunStore<const T extends RunStore>(store: T): T`
526
+ returns the argument's own type, so an optional method your store implements
527
+ stays known-present on the result instead of collapsing to `| undefined`.
528
+ `isTerminalRunStatus(status)` is a type predicate narrowing `RunStatus` to
529
+ `TerminalRunStatus`, so code inside the guard can pass `status` where a
530
+ `TerminalRunStatus` is required without a cast. When a backend omits an
531
+ optional `RunStore` method, declare the omission when running the conformance
532
+ testkit (`ai-persistence/stores`'s `skipMethods` option) rather than leaving it
533
+ undeclared.
534
+
535
+ ### `StreamDurability.snapshot()`
536
+
537
+ A `StreamDurability` (the event-log backend `memoryStream` / `durableStream`
538
+ implement, and what `@tanstack/ai-sandbox`'s run driver resolves per run — its
539
+ `RunDeps.durability` / `sandboxRunDriver({ durability })` is a factory
540
+ `(runId) => StreamDurability`, because one log is bound to one run) requires a
541
+ `snapshot()` method alongside `append`, `read`, and `close`:
542
+
543
+ ```ts
544
+ snapshot: () => Promise<Array<{ offset: TOffset; chunk: StreamChunk }>>
545
+ ```
546
+
547
+ It returns everything stored for a run right now, in append order, then
548
+ resolves. Use it, not `read()`, when a caller needs to inspect a run's stored
549
+ prefix and get an answer back: `read()` tails and only resolves once the log
550
+ is terminalized with `close()` or the caller aborts, so it never resolves
551
+ against a producer that crashed without calling `close()`, and its log stays
552
+ open indefinitely. `snapshot()` resolves immediately with what is stored,
553
+ including while the log is still open, and resolves to an empty array for a
554
+ run with nothing stored yet.
555
+
556
+ **Full guidance lives in the package's own skills** — start at
557
+ `node_modules/@tanstack/ai-persistence/skills/ai-persistence/SKILL.md`,
558
+ which routes to the server, client, stores, locks, and adapter-recipe
559
+ (Drizzle / Prisma / Cloudflare) sub-skills.
560
+
561
+ ### Resume reconstruction is the middleware's job (server-authoritative path)
562
+
563
+ When a thread has pending interrupts, the middleware **records** them and
564
+ **gates** new input: a request that carries pending interrupts must include a
565
+ `resume` batch that references them, or `onConfig` throws. On a valid resume
566
+ batch the middleware also **builds `ChatResumeToolState`** (approvals /
567
+ client-tool results) and **clears `config.resume`** so the chat engine skips
568
+ its ephemeral reconstruction — that path needs client message history the
569
+ persistence flow deliberately omits when the server owns the transcript.
570
+ Resumes accepted in `onConfig` are committed (marked resolved/cancelled) only
571
+ once the run reaches a successful boundary, so a provider failure between
572
+ accepting a resume and finishing leaves the interrupt pending and a retry with
573
+ the same resume succeeds.
574
+
575
+ > A companion `withGenerationPersistence(persistence)` tracks run records for
576
+ > non-chat generation activities (image, audio, TTS, video, transcription).
577
+
578
+ Source: docs/persistence/overview.md
579
+
375
580
  ## Sandbox File-Event Hooks (`sandbox` group)
376
581
 
377
582
  Declare a `sandbox: ChatSandboxHooks` group on `defineChatMiddleware` to react
@@ -507,35 +712,38 @@ has no effect on the stream output.
507
712
 
508
713
  Source: docs/advanced/middleware.md
509
714
 
510
- ### b. MEDIUM: Middleware exceptions breaking the stream
715
+ ### b. MEDIUM: Middleware exceptions breaking the stream — in `onChunk` / `onConfig`
716
+
717
+ Know which hooks the framework already guards. **The terminal hooks
718
+ (`onFinish`, `onAbort`, `onError`) are individually wrapped** by core's
719
+ `runTerminalHook`: a throw there is logged on the `errors` channel and the next
720
+ middleware's terminal hook still runs, so a failed analytics `POST` in `onFinish`
721
+ cannot break the stream or replace the abort reason. Guarding those is about
722
+ keeping your own bookkeeping intact, not about protecting the run.
723
+
724
+ **`onChunk` and `onConfig` are NOT guarded, deliberately** — they are transforms
725
+ on the data path, where swallowing a throw would forward a chunk or a config the
726
+ middleware had decided to reject. A throw from either fails the whole stream. That
727
+ is where an unhandled error actually costs you a response:
511
728
 
512
729
  ```typescript
513
- // WRONG -- unhandled error kills the entire streaming response
730
+ // WRONG -- an unhandled error in onChunk kills the entire streaming response
514
731
  const fragile: ChatMiddleware = {
515
- name: 'fragile-analytics',
516
- onFinish: async (ctx, info) => {
517
- // If this fetch fails, the stream breaks
518
- await fetch('/api/analytics', {
519
- method: 'POST',
520
- body: JSON.stringify({ duration: info.duration }),
521
- })
732
+ name: 'fragile-chunk-logger',
733
+ onChunk: (ctx, chunk) => {
734
+ // A logger that throws on an unexpected chunk shape takes the stream with it
735
+ logChunk(chunk)
736
+ },
737
+ onConfig: (ctx, config) => {
738
+ // Same for a config transform that reads an env var that is not set
739
+ return { model: requireEnv('MODEL_OVERRIDE') }
522
740
  },
523
741
  }
524
742
 
525
- // CORRECT -- wrap in try-catch and/or use ctx.defer()
743
+ // CORRECT -- own the failure inside the unguarded hooks
526
744
  const resilient: ChatMiddleware = {
527
- name: 'resilient-analytics',
528
- onFinish: (ctx, info) => {
529
- // Option 1: defer (non-blocking, errors are isolated)
530
- ctx.defer(
531
- fetch('/api/analytics', {
532
- method: 'POST',
533
- body: JSON.stringify({ duration: info.duration }),
534
- }),
535
- )
536
- },
745
+ name: 'resilient-chunk-logger',
537
746
  onChunk: (ctx, chunk) => {
538
- // Option 2: try-catch for synchronous/critical hooks
539
747
  try {
540
748
  logChunk(chunk)
541
749
  } catch (err) {
@@ -543,17 +751,34 @@ const resilient: ChatMiddleware = {
543
751
  }
544
752
  // Return void to pass through
545
753
  },
754
+ onConfig: (ctx, config) => {
755
+ const override = process.env.MODEL_OVERRIDE
756
+ // Decide, do not throw: no override means no transform.
757
+ return override === undefined ? undefined : { model: override }
758
+ },
759
+ onFinish: (ctx, info) => {
760
+ // Already guarded by core — but prefer ctx.defer() anyway, so a slow
761
+ // analytics call does not delay the terminal fan-out at all.
762
+ ctx.defer(
763
+ fetch('/api/analytics', {
764
+ method: 'POST',
765
+ body: JSON.stringify({ duration: info.duration }),
766
+ }),
767
+ )
768
+ },
546
769
  }
547
770
  ```
548
771
 
549
- Wrap all middleware hooks in try-catch to prevent analytics or logging failures
550
- from killing the chat stream. For async side effects, prefer `ctx.defer()` which
551
- runs after the terminal hook and isolates failures.
772
+ Rule: put the try-catch where the framework has none — `onChunk` and `onConfig`
773
+ (and the other transform hooks: `onStructuredOutputConfig`, `onBeforeToolCall`,
774
+ `onAfterToolCall`). For async side effects in the terminal hooks, prefer
775
+ `ctx.defer()`, which runs after the terminal hook and isolates failures.
552
776
 
553
- Source: docs/advanced/middleware.md
777
+ Source: docs/advanced/middleware.md, `packages/ai/src/activities/chat/middleware/compose.ts`
554
778
 
555
779
  ## Cross-References
556
780
 
557
781
  - See also: **ai-core/chat-experience/SKILL.md** -- Middleware hooks into the chat lifecycle
558
782
  - See also: **ai-core/structured-outputs/SKILL.md** -- Middleware now wraps the final structured-output call; use `onStructuredOutputConfig` for JSON-Schema transforms
559
783
  - See also: **ai-core/ag-ui-protocol/SKILL.md** -- Reading the `sandbox.file` / `sandbox.file.diff` `CUSTOM` chunks the sandbox runtime emits alongside these `sandbox` hooks, via `ChatStream`'s typed `KnownCustomEvent` narrowing
784
+ - See also: **`@tanstack/ai-persistence` skills** (`skills/ai-persistence/SKILL.md` in that package) -- Full persistence suite (`withPersistence`, client storage, store contracts, adapter recipes, locks). This file only sketches server `withPersistence`.
@@ -13,7 +13,7 @@ description: >
13
13
  part. convertSchemaToJsonSchema() for manual schema conversion.
14
14
  type: sub-skill
15
15
  library: tanstack-ai
16
- library_version: '0.10.0'
16
+ library_version: '0.42.0'
17
17
  sources:
18
18
  - 'TanStack/ai:docs/structured-outputs/overview.md'
19
19
  - 'TanStack/ai:docs/structured-outputs/one-shot.md'