@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
@@ -11,6 +11,7 @@ import { aiEventClient } from '@tanstack/ai-event-client'
11
11
  import { toRunErrorPayload } from '../error-payload'
12
12
  import { resolveDebugOption } from '../../logger/resolve'
13
13
  import {
14
+ applyGenerationResultTransforms,
14
15
  createGenerationContext,
15
16
  runGenerationAbort,
16
17
  runGenerationError,
@@ -20,11 +21,15 @@ import {
20
21
  } from '../middleware/run'
21
22
  import type { InternalLogger } from '../../logger/internal-logger'
22
23
  import type { DebugOption } from '../../logger/types'
23
- import type { GenerationMiddleware } from '../middleware/types'
24
+ import type {
25
+ GenerationMiddleware,
26
+ GenerationMiddlewareContext,
27
+ } from '../middleware/types'
24
28
  import type { VideoAdapter } from './adapter'
25
29
  import type {
26
30
  MediaPrompt,
27
31
  MediaPromptFor,
32
+ PersistedArtifactRef,
28
33
  StreamChunk,
29
34
  TokenUsage,
30
35
  VideoJobResult,
@@ -175,8 +180,28 @@ export type VideoCreateOptions<
175
180
  pollingInterval?: number
176
181
  /** Maximum time to wait before timing out in milliseconds (stream mode only). @default 600000 */
177
182
  maxDuration?: number
178
- /** Custom run ID (stream mode only) */
183
+ /**
184
+ * Custom run id (stream mode only) — the id stamped on the emitted
185
+ * `RUN_STARTED` / `RUN_FINISHED` chunks.
186
+ *
187
+ * IGNORED by a non-streaming submit. That run spans two calls, and its id is
188
+ * derived from the provider's job instead, so {@link getVideoJobStatus} can
189
+ * recompute it from the `jobId` you already have to poll with. Honoring a
190
+ * custom id here would reintroduce the failure this avoids: a caller who set
191
+ * it on the submit and forgot it on the poll would silently open a second
192
+ * record while the first sat unfinished forever.
193
+ */
179
194
  runId?: string
195
+ /**
196
+ * Stable conversation/thread id for correlating this run when persisted.
197
+ *
198
+ * Also the `threadId` stamped on the emitted `RUN_STARTED` / `RUN_FINISHED`
199
+ * chunks; when omitted a throwaway id is minted for those chunks only, and
200
+ * the persisted run record carries NO thread link rather than a fabricated
201
+ * one. Pass it whenever persistence is on — it is the slot a reloading client
202
+ * hydrates by, so a run stored without it can only be fetched by run id.
203
+ */
204
+ threadId?: string
180
205
  /**
181
206
  * Enable debug logging. Pass `true` to enable all categories, `false` to
182
207
  * silence everything including errors, or a `DebugConfig` object for granular
@@ -185,10 +210,29 @@ export type VideoCreateOptions<
185
210
  debug?: DebugOption
186
211
  /**
187
212
  * Observe-only middleware notified on start, usage, success, and error. Pass
188
- * `otelMiddleware()` to emit OpenTelemetry spans, or implement the
189
- * `GenerationMiddleware` contract for a custom backend. In streaming mode the
190
- * span covers the full create→poll→complete lifecycle; in non-streaming mode
191
- * it covers job submission. An abandoned stream fires `onAbort`.
213
+ * `otelMiddleware()` to emit OpenTelemetry spans, `withGenerationPersistence()`
214
+ * to persist the run, or implement the `GenerationMiddleware` contract for a
215
+ * custom backend.
216
+ *
217
+ * In streaming mode one run covers the full create→poll→complete lifecycle:
218
+ * `onStart` at submission, a terminal `onFinish`/`onError` when the job
219
+ * settles, and `onAbort` if the consumer abandons the stream.
220
+ *
221
+ * In NON-streaming mode the call only SUBMITS the job, so it only opens the
222
+ * run: no terminal hook fires here, because the video does not exist yet.
223
+ * Pass the same `middleware` and `threadId` to {@link getVideoJobStatus}; the
224
+ * poll that observes a terminal job state finishes the run and is where the
225
+ * result and its artifacts are recorded. Nothing else has to be threaded
226
+ * through — both calls derive the run id from the provider's `jobId`, the one
227
+ * id a poller cannot be missing.
228
+ *
229
+ * Because the job id only exists once the provider accepts the job, `onStart`
230
+ * fires AFTER the submit request rather than before it — an observer's span
231
+ * therefore covers the run from acceptance onward, not the submit round-trip.
232
+ * A submission that FAILS has no job to key on, so it opens and immediately
233
+ * fails a run under this call's `requestId`: the thread's latest run reports
234
+ * the failure (a client hydrating the slot sees it) even though there is no
235
+ * job to resume.
192
236
  */
193
237
  middleware?: Array<GenerationMiddleware>
194
238
  } & ({} extends VideoProviderOptions<TAdapter>
@@ -282,7 +326,7 @@ export type VideoActivityResult<
282
326
  *
283
327
  * @example Create a video generation job
284
328
  * ```ts
285
- * import { generateVideo } from '@tanstack/ai'
329
+ * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'
286
330
  * import { openaiVideo } from '@tanstack/ai-openai'
287
331
  *
288
332
  * // Start a video generation job
@@ -292,6 +336,14 @@ export type VideoActivityResult<
292
336
  * })
293
337
  *
294
338
  * console.log('Job started:', jobId)
339
+ *
340
+ * // The submission only OPENS the run; the poll that sees a terminal state is
341
+ * // what completes it. The `jobId` is the whole correlation — pass the same
342
+ * // `middleware` and `threadId` when you use them.
343
+ * const status = await getVideoJobStatus({
344
+ * adapter: openaiVideo('sora-2'),
345
+ * jobId,
346
+ * })
295
347
  * ```
296
348
  *
297
349
  * @example Stream the full video generation lifecycle
@@ -324,8 +376,39 @@ export function generateVideo<
324
376
  return runCreateVideoJob(options) as VideoActivityResult<'create', TStream>
325
377
  }
326
378
 
379
+ /**
380
+ * The run id a non-streaming video job is filed under, derived from the
381
+ * provider job itself.
382
+ *
383
+ * A submit-and-poll run spans two calls in two different requests, so the two
384
+ * halves need to agree on an id. Deriving it from the `jobId` — the one id a
385
+ * poller structurally cannot be missing, because it cannot poll without it —
386
+ * means no correlation state has to survive the boundary and there is no
387
+ * "forgot to pass the run id" failure to document. The provider is part of the
388
+ * key so two providers' job-id spaces cannot collide, and both halves are
389
+ * percent-encoded so the joined string stays unambiguous (and url-safe, since
390
+ * run ids end up in storage keys and query strings).
391
+ */
392
+ function videoRunIdForJob(provider: string, jobId: string): string {
393
+ return `video:${encodeURIComponent(provider)}:${encodeURIComponent(jobId)}`
394
+ }
395
+
327
396
  /**
328
397
  * Internal implementation of non-streaming video job creation.
398
+ *
399
+ * Submitting a job OPENS a run, it does not complete one: the video does not
400
+ * exist yet, and the bytes only appear on a later poll. So this fires `onStart`
401
+ * and runs the result transforms over the submission result — the jobId lands
402
+ * on the run record, which is what lets a later request resume polling — but
403
+ * fires NO terminal hook. {@link getVideoJobStatus} finishes the run when the
404
+ * job settles, keyed on the same derived id.
405
+ *
406
+ * `onStart` therefore runs AFTER the submit request: the run's id comes from
407
+ * the job, which does not exist until the provider accepts it. A submission
408
+ * that fails has no job, so it opens and immediately fails a run under this
409
+ * call's `requestId` — terminal and unresumable by construction, but it puts
410
+ * the failure where a client hydrating the thread will see it instead of
411
+ * showing nothing.
329
412
  */
330
413
  async function runCreateVideoJob<
331
414
  TAdapter extends VideoAdapter<string, any, any, any, any, any>,
@@ -340,24 +423,35 @@ async function runCreateVideoJob<
340
423
  (adapter as { name?: string }).name ??
341
424
  'unknown'
342
425
 
343
- const mwCtx = createGenerationContext({
344
- requestId,
345
- activity: 'video',
346
- provider: adapter.name,
347
- model,
348
- modelOptions,
349
- createId,
350
- })
351
-
352
- await runGenerationStart(middleware, mwCtx)
426
+ // `runId` is resolved per outcome (from the job, or absent on failure), so the
427
+ // context is built once the outcome is known. `options.runId` is deliberately
428
+ // not consulted: in non-streaming mode the run id is always the derived one,
429
+ // the single rule that keeps the two calls in agreement.
430
+ const contextFor = (runId?: string): GenerationMiddlewareContext =>
431
+ createGenerationContext({
432
+ requestId,
433
+ activity: 'video',
434
+ provider: adapter.name,
435
+ model,
436
+ modelOptions,
437
+ // Deliberately the CALLER's `threadId` — no minted fallback. A thread id
438
+ // nobody else knows would file the run in a slot no client could hydrate,
439
+ // which is worse than no link because it looks like one. Mirrors the
440
+ // streaming path.
441
+ threadId: options.threadId,
442
+ runId,
443
+ artifactInputs: { prompt },
444
+ createId,
445
+ })
353
446
 
354
447
  logger.request(`activity=generateVideo provider=${providerName}`, {
355
448
  provider: providerName,
356
449
  model,
357
450
  })
358
451
 
452
+ let jobResult: VideoJobResult
359
453
  try {
360
- const result = await adapter.createVideoJob({
454
+ jobResult = await adapter.createVideoJob({
361
455
  model,
362
456
  prompt,
363
457
  size,
@@ -365,18 +459,13 @@ async function runCreateVideoJob<
365
459
  modelOptions,
366
460
  logger,
367
461
  })
368
- logger.output(`activity=generateVideo jobId=${result.jobId}`, {
369
- jobId: result.jobId,
370
- model: result.model,
371
- })
372
- // Non-streaming create only submits the job; usage isn't known until the
373
- // job completes via polling, so the span covers submission only.
374
- await runGenerationFinish(middleware, mwCtx, {
375
- duration: Date.now() - startTime,
376
- })
377
- return result
378
462
  } catch (error) {
379
- await runGenerationError(middleware, mwCtx, {
463
+ // No jobId exists, so this run can only be keyed on the request. Start it
464
+ // just to fail it: `generationRuns.update` on an unknown run id is a no-op
465
+ // by contract, so without the `onStart` the failure would persist nowhere.
466
+ const failedCtx = contextFor()
467
+ await runGenerationStart(middleware, failedCtx)
468
+ await runGenerationError(middleware, failedCtx, {
380
469
  error,
381
470
  duration: Date.now() - startTime,
382
471
  })
@@ -386,6 +475,18 @@ async function runCreateVideoJob<
386
475
  })
387
476
  throw error
388
477
  }
478
+
479
+ logger.output(`activity=generateVideo jobId=${jobResult.jobId}`, {
480
+ jobId: jobResult.jobId,
481
+ model: jobResult.model,
482
+ })
483
+
484
+ const mwCtx = contextFor(videoRunIdForJob(adapter.name, jobResult.jobId))
485
+ await runGenerationStart(middleware, mwCtx)
486
+ // Transforms see the submission result (no url yet, so nothing to copy into a
487
+ // blob store) purely so the run record captures the jobId and any prompt
488
+ // inputs. No finish hook: the run is still running.
489
+ return await applyGenerationResultTransforms(mwCtx, jobResult)
389
490
  }
390
491
 
391
492
  function sleep(ms: number): Promise<void> {
@@ -412,12 +513,15 @@ async function* runStreamingVideoGeneration<
412
513
  (adapter as { name?: string }).name ??
413
514
  'unknown'
414
515
 
415
- const threadId = createId('thread')
516
+ // The wire needs a thread id on every RUN_* chunk, so one is minted when the
517
+ // caller passes none — matching `streamGenerationResult`, which the other
518
+ // activities stream through.
519
+ const wireThreadId = options.threadId ?? createId('thread')
416
520
 
417
521
  yield {
418
522
  type: 'RUN_STARTED',
419
523
  runId,
420
- threadId,
524
+ threadId: wireThreadId,
421
525
  timestamp: Date.now(),
422
526
  } as StreamChunk
423
527
 
@@ -427,6 +531,17 @@ async function* runStreamingVideoGeneration<
427
531
  provider: adapter.name,
428
532
  model,
429
533
  modelOptions,
534
+ // Identity has to reach the middleware, not just the chunks: persistence
535
+ // keys the run record on these, and without them it falls back to the
536
+ // internal `requestId` and records no thread link at all.
537
+ //
538
+ // Deliberately the CALLER's `threadId`, never `wireThreadId`: a minted id is
539
+ // known to nobody, so persisting it would file the run in a slot no client
540
+ // could ever hydrate — worse than recording no link, because it looks like
541
+ // one. This mirrors `generateImage`.
542
+ threadId: options.threadId,
543
+ runId,
544
+ artifactInputs: { prompt },
430
545
  createId,
431
546
  })
432
547
 
@@ -459,7 +574,7 @@ async function* runStreamingVideoGeneration<
459
574
  name: 'video:job:created',
460
575
  value: { jobId: jobResult.jobId },
461
576
  timestamp: Date.now(),
462
- } as StreamChunk
577
+ }
463
578
 
464
579
  // Poll for completion
465
580
  const startTime = Date.now()
@@ -478,7 +593,7 @@ async function* runStreamingVideoGeneration<
478
593
  error: statusResult.error,
479
594
  },
480
595
  timestamp: Date.now(),
481
- } as StreamChunk
596
+ }
482
597
 
483
598
  if (statusResult.status === 'completed') {
484
599
  const urlResult = await adapter.getVideoUrl(jobResult.jobId)
@@ -491,6 +606,21 @@ async function* runStreamingVideoGeneration<
491
606
  },
492
607
  )
493
608
 
609
+ // Run the result transforms before anything observes the result, the
610
+ // same as every other media activity. This is what lets persistence
611
+ // copy the video into a blob store, attach its artifact refs, and
612
+ // rewrite `url` to a durable app-origin one — so the chunk below and
613
+ // the stored run record carry the SAME urls. Skipping it leaves a
614
+ // result whose only url is the provider's expiring link.
615
+ const rawResult = {
616
+ jobId: jobResult.jobId,
617
+ status: 'completed' as const,
618
+ url: urlResult.url,
619
+ expiresAt: urlResult.expiresAt,
620
+ ...(urlResult.usage ? { usage: urlResult.usage } : {}),
621
+ }
622
+ const result = await applyGenerationResultTransforms(mwCtx, rawResult)
623
+
494
624
  // Fire finish before yielding the terminal chunks: the generation has
495
625
  // succeeded, so a consumer that stops reading after `generation:result`
496
626
  // (without pulling `RUN_FINISHED`) must not trip the abandonment path in
@@ -506,20 +636,14 @@ async function* runStreamingVideoGeneration<
506
636
  yield {
507
637
  type: 'CUSTOM',
508
638
  name: 'generation:result',
509
- value: {
510
- jobId: jobResult.jobId,
511
- status: 'completed',
512
- url: urlResult.url,
513
- expiresAt: urlResult.expiresAt,
514
- ...(urlResult.usage ? { usage: urlResult.usage } : {}),
515
- },
639
+ value: result,
516
640
  timestamp: Date.now(),
517
- } as StreamChunk
641
+ }
518
642
 
519
643
  yield {
520
644
  type: 'RUN_FINISHED',
521
645
  runId,
522
- threadId,
646
+ threadId: wireThreadId,
523
647
  finishReason: 'stop',
524
648
  timestamp: Date.now(),
525
649
  } as StreamChunk
@@ -550,7 +674,7 @@ async function* runStreamingVideoGeneration<
550
674
  yield {
551
675
  type: 'RUN_ERROR',
552
676
  runId,
553
- threadId,
677
+ threadId: wireThreadId,
554
678
  message: payload.message,
555
679
  code: payload.code,
556
680
  error: payload,
@@ -570,12 +694,73 @@ async function* runStreamingVideoGeneration<
570
694
  }
571
695
  }
572
696
 
697
+ /**
698
+ * Options for {@link getVideoJobStatus}.
699
+ *
700
+ * The run this poll finishes is identified by `adapter` + `jobId` alone — the
701
+ * same pair the submitting `generateVideo()` call derived it from — so there is
702
+ * no run id to thread through. Pass the submission's `threadId` and the same
703
+ * `middleware`.
704
+ *
705
+ * @experimental Video generation is an experimental feature and may change.
706
+ */
707
+ export interface VideoJobStatusOptions<
708
+ TAdapter extends VideoAdapter<string, any, any, any, any, any>,
709
+ > {
710
+ /** The video adapter to use (must be created with a model) */
711
+ adapter: TAdapter & { kind: typeof kind }
712
+ /** The job ID to check status for */
713
+ jobId: string
714
+ /**
715
+ * The scope the run is filed under. Must match the submission's `threadId` —
716
+ * generation persistence REFUSES a run without a scope (a run filed under
717
+ * none can never be hydrated by one), so omitting it throws rather than
718
+ * quietly filing the finished video somewhere unreachable.
719
+ */
720
+ threadId?: string
721
+ /**
722
+ * Observe-only middleware. Hooks fire ONLY on the poll that observes a
723
+ * terminal job state: `onStart` (resuming the submission's run), then the
724
+ * result transforms — which is where persistence copies the video into a blob
725
+ * store and rewrites `url` to a durable one, so the returned result carries
726
+ * the same urls as the stored record — then `onFinish`, or `onError` when the
727
+ * job failed. Intermediate polls invoke nothing, so a middleware is not
728
+ * charged for the wait.
729
+ */
730
+ middleware?: Array<GenerationMiddleware>
731
+ }
732
+
733
+ /**
734
+ * The status of a video job, plus the video itself once the job completed.
735
+ *
736
+ * @experimental Video generation is an experimental feature and may change.
737
+ */
738
+ export interface VideoJobStatusResult {
739
+ /** Job identifier */
740
+ jobId: string
741
+ status: 'pending' | 'processing' | 'completed' | 'failed'
742
+ progress?: number
743
+ url?: string
744
+ /** When the provider url expires, if it reported one. */
745
+ expiresAt?: Date
746
+ error?: string
747
+ usage?: TokenUsage
748
+ /** Durable artifact references, when generation persistence is wired. */
749
+ artifacts?: Array<PersistedArtifactRef>
750
+ }
751
+
573
752
  /**
574
753
  * Get video job status - returns the current status, progress, and URL if available.
575
754
  *
576
755
  * This function combines status checking and URL retrieval. If the job is completed,
577
756
  * it will automatically fetch and include the video URL.
578
757
  *
758
+ * It is also where a non-streaming `generateVideo()` run ENDS: pass the same
759
+ * `middleware` and `threadId`, and the poll that first sees a terminal job state
760
+ * finishes the run (recording the result and its artifacts) or fails it. The run
761
+ * is identified by `adapter` + `jobId`, exactly what the submission derived it
762
+ * from, so there is nothing else to carry between the two calls.
763
+ *
579
764
  * @experimental Video generation is an experimental feature and may change.
580
765
  *
581
766
  * @example Check job status
@@ -594,23 +779,63 @@ async function* runStreamingVideoGeneration<
594
779
  * console.log('Video URL:', result.url)
595
780
  * }
596
781
  * ```
782
+ *
783
+ * @example Submit and poll one persisted run
784
+ * ```ts
785
+ * import { generateVideo, getVideoJobStatus } from '@tanstack/ai'
786
+ * import { withGenerationPersistence } from '@tanstack/ai-persistence'
787
+ * import { openaiVideo } from '@tanstack/ai-openai'
788
+ *
789
+ * const adapter = openaiVideo('sora-2')
790
+ * const middleware = [withGenerationPersistence(persistence)]
791
+ *
792
+ * // Opens the run (status `running`, jobId recorded). Its run id is derived
793
+ * // from the provider job, so nothing has to be stored to resume it.
794
+ * const { jobId } = await generateVideo({
795
+ * adapter,
796
+ * prompt: 'A cat chasing a dog in a sunny park',
797
+ * threadId,
798
+ * middleware,
799
+ * })
800
+ *
801
+ * // Completes the SAME run once the job settles — this is what writes the
802
+ * // video, its artifacts, and the terminal status. Works from a different
803
+ * // request or process: the jobId is the only correlation.
804
+ * const status = await getVideoJobStatus({
805
+ * adapter,
806
+ * jobId,
807
+ * threadId,
808
+ * middleware,
809
+ * })
810
+ * ```
597
811
  */
598
812
  export async function getVideoJobStatus<
599
813
  TAdapter extends VideoAdapter<string, any, any, any, any, any>,
600
- >(options: {
601
- adapter: TAdapter & { kind: typeof kind }
602
- jobId: string
603
- }): Promise<{
604
- status: 'pending' | 'processing' | 'completed' | 'failed'
605
- progress?: number
606
- url?: string
607
- error?: string
608
- usage?: TokenUsage
609
- }> {
610
- const { adapter, jobId } = options
814
+ >(options: VideoJobStatusOptions<TAdapter>): Promise<VideoJobStatusResult> {
815
+ const { adapter, jobId, middleware } = options
611
816
  const requestId = createId('video-status')
612
817
  const startTime = Date.now()
613
818
 
819
+ // Built per call but only USED on a terminal poll — `onStart` is what
820
+ // registers the result transforms, so it has to run in the same call that
821
+ // applies them.
822
+ const terminalContext = (): GenerationMiddlewareContext =>
823
+ createGenerationContext({
824
+ requestId,
825
+ activity: 'video',
826
+ provider: adapter.name,
827
+ model: adapter.model,
828
+ threadId: options.threadId,
829
+ // Recomputed, never passed in: the submitting call derived the same id
830
+ // from the same provider + job, so the two halves agree without the
831
+ // caller carrying anything but the jobId they must already have.
832
+ runId: videoRunIdForJob(adapter.name, jobId),
833
+ // Deliberately no `artifactInputs`: the submission already persisted any
834
+ // prompt inputs under this run, and passing them again would store a
835
+ // second copy of every input image.
836
+ createId,
837
+ })
838
+
614
839
  aiEventClient.emit('video:request:started', {
615
840
  requestId,
616
841
  provider: adapter.name,
@@ -625,34 +850,12 @@ export async function getVideoJobStatus<
625
850
 
626
851
  // If completed, also get the URL
627
852
  if (statusResult.status === 'completed') {
853
+ let urlResult: VideoUrlResult
854
+ // Scoped tightly to the provider call: a middleware hook that throws must
855
+ // surface as itself, not be relabelled "failed to get video URL" and then
856
+ // re-reported to the very middleware that threw.
628
857
  try {
629
- const urlResult = await adapter.getVideoUrl(jobId)
630
- aiEventClient.emit('video:request:completed', {
631
- requestId,
632
- provider: adapter.name,
633
- model: adapter.model,
634
- requestType: 'status',
635
- jobId,
636
- status: statusResult.status,
637
- progress: statusResult.progress,
638
- url: urlResult.url,
639
- duration: Date.now() - startTime,
640
- timestamp: Date.now(),
641
- })
642
- if (urlResult.usage) {
643
- aiEventClient.emit('video:usage', {
644
- requestId,
645
- model: adapter.model,
646
- usage: urlResult.usage,
647
- timestamp: Date.now(),
648
- })
649
- }
650
- return {
651
- status: statusResult.status,
652
- progress: statusResult.progress,
653
- url: urlResult.url,
654
- ...(urlResult.usage ? { usage: urlResult.usage } : {}),
655
- }
858
+ urlResult = await adapter.getVideoUrl(jobId)
656
859
  } catch (error) {
657
860
  const errorMessage =
658
861
  error instanceof Error ? error.message : 'Failed to get video URL'
@@ -668,13 +871,63 @@ export async function getVideoJobStatus<
668
871
  duration: Date.now() - startTime,
669
872
  timestamp: Date.now(),
670
873
  })
671
- // Provider reported completed but result fetch failed — treat as failed
874
+ // Provider reported completed but result fetch failed — treat as failed,
875
+ // and fail the run with it: the job is terminal, so nothing later will.
876
+ await runGenerationError(middleware, terminalContext(), {
877
+ error,
878
+ duration: Date.now() - startTime,
879
+ })
672
880
  return {
881
+ jobId,
673
882
  status: 'failed' as const,
674
883
  progress: statusResult.progress,
675
884
  error: errorMessage,
676
885
  }
677
886
  }
887
+
888
+ aiEventClient.emit('video:request:completed', {
889
+ requestId,
890
+ provider: adapter.name,
891
+ model: adapter.model,
892
+ requestType: 'status',
893
+ jobId,
894
+ status: statusResult.status,
895
+ progress: statusResult.progress,
896
+ url: urlResult.url,
897
+ duration: Date.now() - startTime,
898
+ timestamp: Date.now(),
899
+ })
900
+ if (urlResult.usage) {
901
+ aiEventClient.emit('video:usage', {
902
+ requestId,
903
+ model: adapter.model,
904
+ usage: urlResult.usage,
905
+ timestamp: Date.now(),
906
+ })
907
+ }
908
+
909
+ const mwCtx = terminalContext()
910
+ await runGenerationStart(middleware, mwCtx)
911
+ const result = await applyGenerationResultTransforms<VideoJobStatusResult>(
912
+ mwCtx,
913
+ {
914
+ jobId,
915
+ status: 'completed',
916
+ ...(statusResult.progress !== undefined
917
+ ? { progress: statusResult.progress }
918
+ : {}),
919
+ url: urlResult.url,
920
+ ...(urlResult.expiresAt ? { expiresAt: urlResult.expiresAt } : {}),
921
+ ...(urlResult.usage ? { usage: urlResult.usage } : {}),
922
+ },
923
+ )
924
+ if (urlResult.usage)
925
+ await runGenerationUsage(middleware, mwCtx, urlResult.usage)
926
+ await runGenerationFinish(middleware, mwCtx, {
927
+ duration: Date.now() - startTime,
928
+ usage: urlResult.usage,
929
+ })
930
+ return result
678
931
  }
679
932
 
680
933
  aiEventClient.emit('video:request:completed', {
@@ -690,8 +943,18 @@ export async function getVideoJobStatus<
690
943
  timestamp: Date.now(),
691
944
  })
692
945
 
946
+ // A failed job is terminal for the run too: without this the record would sit
947
+ // at `running` forever, indistinguishable from a job still being worked on.
948
+ if (statusResult.status === 'failed') {
949
+ await runGenerationError(middleware, terminalContext(), {
950
+ error: new Error(statusResult.error || 'Video generation failed'),
951
+ duration: Date.now() - startTime,
952
+ })
953
+ }
954
+
693
955
  // Return status for non-completed jobs
694
956
  return {
957
+ jobId,
695
958
  status: statusResult.status,
696
959
  progress: statusResult.progress,
697
960
  error: statusResult.error,
@@ -9,6 +9,8 @@ export type {
9
9
  GenerationAbortInfo,
10
10
  GenerationErrorInfo,
11
11
  AnyGenerationMiddleware,
12
+ GenerationResultTransform,
13
+ GenerationResultTransformContext,
12
14
  } from './types'
13
15
  export {
14
16
  createGenerationContext,
@@ -4,6 +4,7 @@ import type {
4
4
  GenerationFinishInfo,
5
5
  GenerationMiddleware,
6
6
  GenerationMiddlewareContext,
7
+ GenerationResultTransformContext,
7
8
  GenerationUsageInfo,
8
9
  } from './types'
9
10
 
@@ -19,6 +20,9 @@ export function createGenerationContext(args: {
19
20
  provider: string
20
21
  model: string
21
22
  modelOptions?: unknown
23
+ threadId?: string
24
+ runId?: string
25
+ artifactInputs?: unknown
22
26
  createId: (prefix: string) => string
23
27
  }): GenerationMiddlewareContext {
24
28
  return {
@@ -27,9 +31,13 @@ export function createGenerationContext(args: {
27
31
  provider: args.provider,
28
32
  model: args.model,
29
33
  modelOptions: args.modelOptions,
34
+ threadId: args.threadId,
35
+ runId: args.runId,
30
36
  source: 'server',
31
37
  createId: args.createId,
32
38
  context: undefined,
39
+ resultTransforms: [],
40
+ artifactInputs: args.artifactInputs,
33
41
  }
34
42
  }
35
43
 
@@ -86,3 +94,26 @@ export function runGenerationError(
86
94
  ): Promise<void> {
87
95
  return run(middleware, (mw) => mw.onError?.(ctx, info))
88
96
  }
97
+
98
+ /**
99
+ * Apply the result transforms middleware registered on the context, in order,
100
+ * to the raw adapter result. Each transform may return a replacement result or
101
+ * `undefined` to leave it unchanged. Runs after the adapter result exists and
102
+ * before the final result is returned or streamed.
103
+ */
104
+ export async function applyGenerationResultTransforms<TResult>(
105
+ ctx: GenerationMiddlewareContext,
106
+ result: TResult,
107
+ ): Promise<TResult> {
108
+ let current = result
109
+ const transformCtx: GenerationResultTransformContext = { middleware: ctx }
110
+
111
+ for (const transform of ctx.resultTransforms ?? []) {
112
+ const transformed = await transform(current, transformCtx)
113
+ if (transformed !== undefined) {
114
+ current = transformed as TResult
115
+ }
116
+ }
117
+
118
+ return current
119
+ }