@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
@@ -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'
@@ -4,12 +4,12 @@ description: >
4
4
  Isomorphic tool system: toolDefinition() with Zod schemas,
5
5
  .server() and .client() implementations, passing tools to both
6
6
  chat() on server and useChat/clientTools on client, tool approval
7
- flows with needsApproval and addToolApprovalResponse(), lazy tool
7
+ flows with needsApproval and bound interrupts (resolveInterrupt), lazy tool
8
8
  discovery with lazy:true, rendering ToolCallPart and ToolResultPart
9
9
  in UI.
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/tools/tools.md'
15
15
  - 'TanStack/ai:docs/tools/server-tools.md'
@@ -108,14 +108,14 @@ function ChatPage() {
108
108
  connection: fetchServerSentEvents("/api/chat"),
109
109
  tools,
110
110
  });
111
- type Messages = InferChatMessages<typeof chatOptions>;
112
-
113
111
  const { messages, sendMessage } = useChat(chatOptions);
112
+ // InferChatMessages ties part types to the configured tools when needed:
113
+ // type Messages = InferChatMessages<typeof chatOptions>
114
114
 
115
115
  return (
116
116
  <div>
117
117
  <span>Cart: {cartCount}</span>
118
- {(messages as Messages).map((msg) => (
118
+ {messages.map((msg) => (
119
119
  <div key={msg.id}>
120
120
  {msg.parts.map((part) => {
121
121
  if (part.type === "text") return <p>{part.content}</p>;
@@ -239,9 +239,11 @@ function ChatPage() {
239
239
 
240
240
  ### Pattern 3: Tool with Approval Flow
241
241
 
242
- Set `needsApproval: true` in the definition. Execution pauses until the client
243
- calls `addToolApprovalResponse()`. The part has `state: "approval-requested"`
244
- and an `approval` object with an `id`.
242
+ Set `needsApproval: true` in the definition. Execution pauses with
243
+ `RUN_FINISHED.outcome.type === 'interrupt'`. The primary client API is bound
244
+ `interrupts` + `resolveInterrupt` / `resolveInterrupts` / `cancel`.
245
+ `addToolApprovalResponse` and `pendingInterrupts` remain as deprecated
246
+ compatibility shims during migration.
245
247
 
246
248
  ```typescript
247
249
  import { toolDefinition } from '@tanstack/ai'
@@ -265,59 +267,40 @@ export const sendEmail = sendEmailDef.server(async ({ to, subject, body }) => {
265
267
  })
266
268
  ```
267
269
 
268
- Client -- render approval UI and respond:
270
+ Server route must forward `resume` / `parentRunId` (via `chatParamsFromRequest`
271
+ or equivalent). Client -- render bound interrupts:
269
272
 
270
273
  ```typescript
271
274
  import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
272
275
 
273
276
  function ChatPage() {
274
- const { messages, addToolApprovalResponse } = useChat({
277
+ const { messages, interrupts, sendMessage } = useChat({
275
278
  connection: fetchServerSentEvents("/api/chat"),
276
279
  });
277
280
 
278
281
  return (
279
282
  <div>
283
+ {interrupts.map((interrupt) => {
284
+ if (interrupt.kind !== "tool-approval") return null;
285
+ return (
286
+ <div key={interrupt.id}>
287
+ <p>Approve "{interrupt.toolName}"?</p>
288
+ <pre>{JSON.stringify(interrupt.originalArgs, null, 2)}</pre>
289
+ <button onClick={() => interrupt.resolveInterrupt(true)}>
290
+ Approve
291
+ </button>
292
+ <button onClick={() => interrupt.resolveInterrupt(false)}>
293
+ Deny
294
+ </button>
295
+ <button onClick={() => interrupt.cancel()}>Cancel</button>
296
+ </div>
297
+ );
298
+ })}
280
299
  {messages.map((msg) => (
281
300
  <div key={msg.id}>
282
- {msg.parts.map((part) => {
283
- if (part.type === "text") return <p>{part.content}</p>;
284
- if (
285
- part.type === "tool-call" &&
286
- part.state === "approval-requested" &&
287
- part.approval
288
- ) {
289
- return (
290
- <div key={part.id}>
291
- <p>Approve "{part.name}"?</p>
292
- {/* `part.input` is the parsed, typed object (populated once
293
- the arguments are complete, as they are at approval
294
- time); `part.arguments` remains the raw JSON string. */}
295
- <pre>{JSON.stringify(part.input, null, 2)}</pre>
296
- <button
297
- onClick={() =>
298
- addToolApprovalResponse({
299
- id: part.approval!.id,
300
- approved: true,
301
- })
302
- }
303
- >
304
- Approve
305
- </button>
306
- <button
307
- onClick={() =>
308
- addToolApprovalResponse({
309
- id: part.approval!.id,
310
- approved: false,
311
- })
312
- }
313
- >
314
- Deny
315
- </button>
316
- </div>
317
- );
318
- }
319
- return null;
320
- })}
301
+ {msg.parts.map((part) =>
302
+ part.type === "text" ? <p key={part.content}>{part.content}</p> : null
303
+ )}
321
304
  </div>
322
305
  ))}
323
306
  </div>
@@ -325,14 +308,24 @@ function ChatPage() {
325
308
  }
326
309
  ```
327
310
 
328
- > **Type-safe approval:** With typed `tools`, `part.approval` exists **only**
329
- > on parts for tools defined with `needsApproval: true`. Tools without approval
330
- > have no `approval` field (reading it is a compile error). For a
331
- > tool-agnostic handler over a typed union, narrow with `'approval' in part`
332
- > (`if (part.type === 'tool-call' && 'approval' in part && part.approval)`),
333
- > or type a shared component against the base `ToolCallPart`. An untyped
334
- > `useChat()` keeps `approval` on every tool-call part, which is why the
335
- > snippet above (no `tools` generic) reads it directly.
311
+ Batch all pending approvals with `resolveInterrupts` (void — submission is
312
+ async; watch `resuming` / `interruptErrors`):
313
+
314
+ ```typescript
315
+ // Payloadless tool-approvals only
316
+ resolveInterrupts(true)
317
+
318
+ // Or per-item:
319
+ resolveInterrupts((interrupt) => {
320
+ if (interrupt.kind === 'tool-approval') {
321
+ interrupt.resolveInterrupt(true)
322
+ }
323
+ })
324
+ ```
325
+
326
+ Migration: `pendingInterrupts` aliases `interrupts`; `addToolApprovalResponse`
327
+ forwards to the matching bound approval when present. Prefer the bound methods
328
+ above for new code. See `docs/interrupts/`.
336
329
 
337
330
  ### Pattern 4: Lazy Tool Discovery
338
331
 
@@ -375,6 +368,8 @@ export async function POST(request: Request) {
375
368
  adapter: openaiText('gpt-5.5'),
376
369
  messages,
377
370
  tools: [getProducts, compareProducts],
371
+ // maxIterations bounds model turns, not tool calls. For tool budgets,
372
+ // use middleware onBeforeToolCall + onShouldContinue (see agentic-cycle docs).
378
373
  agentLoopStrategy: maxIterations(20),
379
374
  })
380
375
  return toServerSentEventsResponse(stream)
@@ -647,7 +642,7 @@ import { anthropicText } from '@tanstack/ai-anthropic'
647
642
  export async function POST(request: Request) {
648
643
  const { messages } = await request.json()
649
644
  const stream = chat({
650
- adapter: anthropicText('claude-sonnet-4-5'),
645
+ adapter: anthropicText('claude-sonnet-4-6'),
651
646
  messages,
652
647
  tools: [
653
648
  codeExecutionTool(
@@ -683,7 +678,7 @@ import { openaiText } from '@tanstack/ai-openai'
683
678
  export async function POST(request: Request) {
684
679
  const { messages } = await request.json()
685
680
  const stream = chat({
686
- adapter: openaiText('gpt-5.2'),
681
+ adapter: openaiText('gpt-5.5'),
687
682
  messages,
688
683
  tools: [
689
684
  shellTool({
@@ -1,9 +1,15 @@
1
1
  import type { AgentLoopStrategy } from '../../types'
2
2
 
3
3
  /**
4
- * Creates a strategy that continues for a maximum number of iterations
4
+ * Creates a strategy that continues for a maximum number of **model turns**
5
+ * (iterations), not tool calls.
5
6
  *
6
- * @param max - Maximum number of iterations to allow
7
+ * One iteration can still emit many parallel tool calls. For a tool-call
8
+ * budget, use middleware with `onBeforeToolCall` (per-turn cap) and
9
+ * `onShouldContinue` (cumulative run budget) — see the docs recipe under
10
+ * Agentic Cycle.
11
+ *
12
+ * @param max - Maximum number of model turns to allow
7
13
  * @returns AgentLoopStrategy that stops after max iterations
8
14
  *
9
15
  * @example
@@ -13,7 +19,7 @@ import type { AgentLoopStrategy } from '../../types'
13
19
  * model: "gpt-4o",
14
20
  * messages: [...],
15
21
  * tools: [weatherTool],
16
- * agentLoopStrategy: maxIterations(3), // Max 3 iterations
22
+ * agentLoopStrategy: maxIterations(3), // Max 3 model turns
17
23
  * });
18
24
  * ```
19
25
  */
@@ -60,7 +66,7 @@ export function untilFinishReason(
60
66
  * All strategies must return true to continue
61
67
  *
62
68
  * @param strategies - Array of strategies to combine
63
- * @returns AgentLoopStrategy that continues only if all strategies return true
69
+ * @returns AgentLoopStrategy that continues only if all strategies agree
64
70
  *
65
71
  * @example
66
72
  * ```typescript