@tanstack/ai 0.53.0 → 0.55.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 (68) hide show
  1. package/README.md +14 -13
  2. package/dist/esm/activities/chat/index.js +22 -4
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.d.ts +21 -1
  5. package/dist/esm/activities/chat/messages.js +50 -1
  6. package/dist/esm/activities/chat/messages.js.map +1 -1
  7. package/dist/esm/activities/chat/stream/processor.d.ts +17 -0
  8. package/dist/esm/activities/chat/stream/processor.js +27 -0
  9. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  10. package/dist/esm/activities/generateLiveVideo/adapter.d.ts +69 -0
  11. package/dist/esm/activities/generateLiveVideo/adapter.js +23 -0
  12. package/dist/esm/activities/generateLiveVideo/adapter.js.map +1 -0
  13. package/dist/esm/activities/generateLiveVideo/index.d.ts +99 -0
  14. package/dist/esm/activities/generateLiveVideo/index.js +162 -0
  15. package/dist/esm/activities/generateLiveVideo/index.js.map +1 -0
  16. package/dist/esm/activities/generateVideo/index.js +3 -1
  17. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  18. package/dist/esm/activities/generateWorld/adapter.d.ts +69 -0
  19. package/dist/esm/activities/generateWorld/adapter.js +23 -0
  20. package/dist/esm/activities/generateWorld/adapter.js.map +1 -0
  21. package/dist/esm/activities/generateWorld/index.d.ts +99 -0
  22. package/dist/esm/activities/generateWorld/index.js +162 -0
  23. package/dist/esm/activities/generateWorld/index.js.map +1 -0
  24. package/dist/esm/activities/index.d.ts +8 -2
  25. package/dist/esm/activities/index.js +11 -7
  26. package/dist/esm/activities/middleware/types.d.ts +1 -1
  27. package/dist/esm/client.d.ts +4 -2
  28. package/dist/esm/client.js +3 -1
  29. package/dist/esm/client.js.map +1 -1
  30. package/dist/esm/index.d.ts +4 -2
  31. package/dist/esm/index.js +3 -1
  32. package/dist/esm/middlewares/otel.js +3 -1
  33. package/dist/esm/middlewares/otel.js.map +1 -1
  34. package/dist/esm/types.d.ts +112 -0
  35. package/package.json +3 -3
  36. package/skills/ai-core/adapter-configuration/SKILL.md +91 -43
  37. package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +39 -21
  38. package/skills/ai-core/adapter-configuration/references/byteplus-adapter.md +5 -0
  39. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +14 -6
  40. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +33 -25
  41. package/skills/ai-core/adapter-configuration/references/groq-adapter.md +7 -2
  42. package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +25 -12
  43. package/skills/ai-core/adapter-configuration/references/openai-adapter.md +19 -9
  44. package/skills/ai-core/adapter-configuration/references/openrouter-adapter.md +34 -21
  45. package/skills/ai-core/ag-ui-protocol/SKILL.md +16 -10
  46. package/skills/ai-core/chat-experience/SKILL.md +228 -108
  47. package/skills/ai-core/client-persistence/SKILL.md +21 -9
  48. package/skills/ai-core/custom-backend-integration/SKILL.md +86 -52
  49. package/skills/ai-core/debug-logging/SKILL.md +100 -18
  50. package/skills/ai-core/locks/SKILL.md +35 -7
  51. package/skills/ai-core/media-generation/SKILL.md +136 -61
  52. package/skills/ai-core/middleware/SKILL.md +174 -69
  53. package/skills/ai-core/structured-outputs/SKILL.md +98 -49
  54. package/skills/ai-core/tool-calling/SKILL.md +245 -158
  55. package/src/activities/chat/index.ts +29 -7
  56. package/src/activities/chat/messages.ts +60 -0
  57. package/src/activities/chat/stream/processor.ts +31 -0
  58. package/src/activities/generateLiveVideo/adapter.ts +99 -0
  59. package/src/activities/generateLiveVideo/index.ts +339 -0
  60. package/src/activities/generateVideo/index.ts +3 -4
  61. package/src/activities/generateWorld/adapter.ts +96 -0
  62. package/src/activities/generateWorld/index.ts +339 -0
  63. package/src/activities/index.ts +44 -0
  64. package/src/activities/middleware/types.ts +2 -0
  65. package/src/client.ts +8 -0
  66. package/src/index.ts +8 -0
  67. package/src/middlewares/otel.ts +2 -0
  68. package/src/types.ts +128 -0
@@ -63,10 +63,12 @@ import {
63
63
  import { maxIterations as maxIterationsStrategy } from './agent-loop-strategies'
64
64
  import { isCancelRequestedReason } from './cancel'
65
65
  import {
66
+ appendUiResourceToModelMessages,
66
67
  convertMessagesToModelMessages,
67
68
  generateMessageId,
68
69
  modelMessagesToUIMessages,
69
70
  safeJsonStringify,
71
+ uiResourcePartFromCustomValue,
70
72
  } from './messages'
71
73
  import { MiddlewareRunner } from './middleware/compose'
72
74
  import { getRunDetached } from './middleware/run-store'
@@ -1245,7 +1247,7 @@ class TextEngine<
1245
1247
  ...this.deferredModelRunFinishedChunks,
1246
1248
  )
1247
1249
  this.deferredModelRunFinishedChunks = []
1248
- } else {
1250
+ } else if (!this.finalStructuredOutput?.nativeCombined) {
1249
1251
  yield* this.flushDeferredModelRunFinishedChunks()
1250
1252
  }
1251
1253
  } else {
@@ -1279,6 +1281,9 @@ class TextEngine<
1279
1281
  ) {
1280
1282
  if (this.finalStructuredOutput.nativeCombined === true) {
1281
1283
  yield* this.harvestCombinedStructuredOutput()
1284
+ if (!this.finalizationError && !this.isCancelled()) {
1285
+ yield* this.flushDeferredModelRunFinishedChunks()
1286
+ }
1282
1287
  } else {
1283
1288
  yield* this.runStructuredFinalization()
1284
1289
  }
@@ -2767,6 +2772,22 @@ class TextEngine<
2767
2772
  }
2768
2773
  }
2769
2774
 
2775
+ /**
2776
+ * Record a `ui-resource` CUSTOM chunk on the assistant ModelMessage owning
2777
+ * its `toolCallId` so the resource survives later MESSAGES_SNAPSHOT chunks
2778
+ * (e.g. the interrupt snapshot emitted when the run pauses on a client
2779
+ * tool). Mirrors the anchor-preserving approach used for
2780
+ * `toolCallMetadata` (#867). See #1397.
2781
+ */
2782
+ private recordEmittedUiResource(value: unknown): void {
2783
+ const part = uiResourcePartFromCustomValue(value)
2784
+ if (!part) return
2785
+ const next = appendUiResourceToModelMessages(this.messages, part)
2786
+ if (next === this.messages) return
2787
+ this.messages = next
2788
+ this.middlewareCtx.messages = this.messages
2789
+ }
2790
+
2770
2791
  private buildMessagesSnapshotChunk(): StreamChunk {
2771
2792
  const withIds = this.messages.map((message, index) => ({
2772
2793
  ...message,
@@ -3926,12 +3947,8 @@ class TextEngine<
3926
3947
  }
3927
3948
 
3928
3949
  // On success, emit the synthetic `structured-output.complete` carrying
3929
- // the parsed object + raw text. Pin the messageId so the client-side
3930
- // handler can target the right UIMessage even when the agent loop's
3931
- // terminal RUN_FINISHED has already cleared `activeMessageIds` (the
3932
- // complete event yields AFTER the loop ends, by which point
3933
- // `getActiveAssistantMessageId()` returns null and would otherwise drop
3934
- // the event silently).
3950
+ // the parsed object + raw text before the deferred RUN_FINISHED. Pin
3951
+ // the messageId so the client targets the schema-constrained turn.
3935
3952
  if (
3936
3953
  this.structuredOutputResult &&
3937
3954
  !this.finalizationError &&
@@ -4432,6 +4449,11 @@ class TextEngine<
4432
4449
  if (this.hasPublicRunStarted) continue
4433
4450
  this.hasPublicRunStarted = true
4434
4451
  }
4452
+ // Persist MCP Apps ui-resource emissions onto the tool-call anchor
4453
+ // message so interrupt MESSAGES_SNAPSHOT chunks keep them (#1397).
4454
+ if (spec.type === EventType.CUSTOM && spec.name === 'ui-resource') {
4455
+ this.recordEmittedUiResource((spec as CustomEvent).value)
4456
+ }
4435
4457
  yield spec
4436
4458
  this.middlewareCtx.chunkIndex++
4437
4459
  }
@@ -387,6 +387,66 @@ function appendUiResources(
387
387
  return { ...ui, parts: [...ui.parts, ...extra] }
388
388
  }
389
389
 
390
+ /**
391
+ * Build a UIResourcePart from the value of a CUSTOM `ui-resource` chunk
392
+ * emitted via `ctx.emitCustomEvent('ui-resource', ...)` (MCP Apps). The
393
+ * emission-side value carries `resource`/`serverId`/`toolName` plus the
394
+ * `toolCallId` stamped by the tool-call context wrapper — the `type`
395
+ * discriminator is added here. Returns undefined when the value does not
396
+ * match the ui-resource shape.
397
+ */
398
+ export function uiResourcePartFromCustomValue(
399
+ value: unknown,
400
+ ): UIResourcePart | undefined {
401
+ if (!isRecord(value)) return undefined
402
+ const part: unknown = { type: 'ui-resource', ...value }
403
+ return isUiResourcePart(part) ? part : undefined
404
+ }
405
+
406
+ /**
407
+ * Store an emitted ui-resource part on the assistant ModelMessage that owns
408
+ * its `toolCallId` (the tool-call anchor), so it survives later
409
+ * MESSAGES_SNAPSHOT chunks — e.g. the interrupt snapshot emitted when the
410
+ * run pauses on a client tool (#1397). Mirrors how `toolCallMetadata` is
411
+ * preserved on the anchor (#867).
412
+ *
413
+ * Returns the SAME array reference when no anchor owns the tool call or the
414
+ * resource is already stored (idempotent).
415
+ */
416
+ export function appendUiResourceToModelMessages(
417
+ messages: Array<ModelMessage>,
418
+ part: UIResourcePart,
419
+ ): Array<ModelMessage> {
420
+ for (let index = messages.length - 1; index >= 0; index--) {
421
+ const message = messages[index]
422
+ if (!message || message.role !== 'assistant') continue
423
+ const ownsToolCall = message.toolCalls?.some(
424
+ (toolCall) => toolCall.id === part.toolCallId,
425
+ )
426
+ if (!ownsToolCall) continue
427
+ const previous = tanstackMetadata(message)?.uiResources ?? []
428
+ if (
429
+ previous.some((stored) => uiResourceKey(stored) === uiResourceKey(part))
430
+ ) {
431
+ return messages
432
+ }
433
+ const nextMessage = {
434
+ ...message,
435
+ metadata: {
436
+ ...message.metadata,
437
+ tanstack: {
438
+ ...tanstackMetadata(message),
439
+ uiResources: [...previous, part],
440
+ },
441
+ },
442
+ }
443
+ const next = messages.slice()
444
+ next[index] = nextMessage
445
+ return next
446
+ }
447
+ return messages
448
+ }
449
+
390
450
  function assistantMetadata(
391
451
  uiMessage: UIMessage,
392
452
  ): UIMessage['metadata'] | undefined {
@@ -242,6 +242,37 @@ export class StreamProcessor {
242
242
  this.emitMessagesChange()
243
243
  }
244
244
 
245
+ /**
246
+ * Put older UI messages at the front of the conversation.
247
+ *
248
+ * Skip a message if its id is already in the list. Keep the existing message.
249
+ * Then emit the same messages-change event as `setMessages`.
250
+ *
251
+ * Use this for older history pages. The first hydrate window uses `setMessages`.
252
+ *
253
+ * @param messages Older UI messages in insertion order. The first item is the oldest.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * processor.setMessages([newest])
258
+ * processor.prependMessages([oldest])
259
+ * ```
260
+ */
261
+ prependMessages(messages: Array<UIMessage>) {
262
+ const existingIds = new Set(this.messages.map((message) => message.id))
263
+ const olderMessages: Array<UIMessage> = []
264
+ for (const message of messages) {
265
+ const isDuplicate = existingIds.has(message.id)
266
+ if (isDuplicate) {
267
+ continue
268
+ }
269
+ existingIds.add(message.id)
270
+ olderMessages.push(message)
271
+ }
272
+ this.messages = [...olderMessages, ...this.messages]
273
+ this.emitMessagesChange()
274
+ }
275
+
245
276
  /**
246
277
  * Add a user message to the conversation.
247
278
  * Supports both simple string content and multimodal content arrays.
@@ -0,0 +1,99 @@
1
+ import type {
2
+ LiveVideoGenerationOptions,
3
+ LiveVideoGenerationResult,
4
+ } from '../../types'
5
+
6
+ /**
7
+ * Configuration for live generation adapter instances.
8
+ *
9
+ * @experimental Live generation is an experimental feature and may change.
10
+ */
11
+ export interface LiveVideoAdapterConfig {
12
+ apiKey?: string
13
+ baseUrl?: string
14
+ timeout?: number
15
+ maxRetries?: number
16
+ headers?: Record<string, string>
17
+ }
18
+
19
+ /**
20
+ * Live adapter interface with pre-resolved generics.
21
+ *
22
+ * An adapter is created by a provider function: `provider('model')` → `adapter`.
23
+ * All type resolution happens at the provider call site, not in this interface.
24
+ *
25
+ * Generic parameters:
26
+ * - TModel: The specific model name (e.g. 'helios')
27
+ * - TProviderOptions: Provider-specific options (already resolved)
28
+ *
29
+ * @experimental Live generation is an experimental feature and may change.
30
+ */
31
+ export interface LiveVideoAdapter<
32
+ TModel extends string = string,
33
+ TProviderOptions extends object = Record<string, unknown>,
34
+ > {
35
+ /** Discriminator for adapter kind - used to determine API shape */
36
+ readonly kind: 'liveVideo'
37
+ /** Adapter name identifier */
38
+ readonly name: string
39
+ /** The model this adapter is configured for */
40
+ readonly model: TModel
41
+
42
+ /**
43
+ * @internal Type-only properties for inference. Not assigned at runtime.
44
+ */
45
+ '~types': {
46
+ providerOptions: TProviderOptions
47
+ }
48
+
49
+ /**
50
+ * Open a live video session from a prompt.
51
+ *
52
+ * Server adapters typically mint a short-lived token and return it with the
53
+ * prompt so a browser can connect, set the prompt, and start streaming.
54
+ */
55
+ createLiveVideo: (
56
+ options: LiveVideoGenerationOptions<TProviderOptions>,
57
+ ) => Promise<LiveVideoGenerationResult>
58
+ }
59
+
60
+ /**
61
+ * A LiveVideoAdapter with any/unknown type parameters.
62
+ * Useful as a constraint in generic functions and interfaces.
63
+ */
64
+ export type AnyLiveVideoAdapter = LiveVideoAdapter<any, any>
65
+
66
+ /**
67
+ * Abstract base class for live generation adapters.
68
+ * Extend this class to implement a live adapter for a specific provider.
69
+ *
70
+ * @experimental Live generation is an experimental feature and may change.
71
+ */
72
+ export abstract class BaseLiveVideoAdapter<
73
+ TModel extends string = string,
74
+ TProviderOptions extends object = Record<string, unknown>,
75
+ > implements LiveVideoAdapter<TModel, TProviderOptions> {
76
+ readonly kind = 'liveVideo' as const
77
+ abstract readonly name: string
78
+ readonly model: TModel
79
+
80
+ // Type-only property - never assigned at runtime
81
+ declare '~types': {
82
+ providerOptions: TProviderOptions
83
+ }
84
+
85
+ protected config: LiveVideoAdapterConfig
86
+
87
+ constructor(model: TModel, config: LiveVideoAdapterConfig = {}) {
88
+ this.config = config
89
+ this.model = model
90
+ }
91
+
92
+ abstract createLiveVideo(
93
+ options: LiveVideoGenerationOptions<TProviderOptions>,
94
+ ): Promise<LiveVideoGenerationResult>
95
+
96
+ protected generateId(): string {
97
+ return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`
98
+ }
99
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Live Activity (Experimental)
3
+ *
4
+ * Mints a session token for a live, prompt-steerable video session. Unlike
5
+ * generateVideo (a job that finishes with a URL), the browser then connects
6
+ * with the token, sets the prompt, and streams until stop/close.
7
+ *
8
+ * @experimental Live generation is an experimental feature and may change.
9
+ */
10
+
11
+ import { aiEventClient } from '@tanstack/ai-event-client'
12
+ import { streamGenerationResult } from '../stream-generation-result.js'
13
+ import { resolveDebugOption } from '../../logger/resolve'
14
+ import {
15
+ applyGenerationResultTransforms,
16
+ createGenerationContext,
17
+ runGenerationAbort,
18
+ runGenerationError,
19
+ runGenerationFinish,
20
+ runGenerationStart,
21
+ runGenerationUsage,
22
+ } from '../middleware/run'
23
+ import {
24
+ abortReasonMessage,
25
+ createActivityAbortControls,
26
+ isActivityAbortError,
27
+ raceWithAbort,
28
+ } from '../../utilities/activity-abort'
29
+ import type { InternalLogger } from '../../logger/internal-logger'
30
+ import type { DebugOption } from '../../logger/types'
31
+ import type { GenerationMiddleware } from '../middleware/types'
32
+ import type { LiveVideoAdapter } from './adapter'
33
+ import type { StreamChunk, LiveVideoGenerationResult } from '../../types'
34
+
35
+ // ===========================
36
+ // Activity Kind
37
+ // ===========================
38
+
39
+ /** The adapter kind this activity handles */
40
+ export const kind = 'liveVideo' as const
41
+
42
+ // ===========================
43
+ // Type Extraction Helpers
44
+ // ===========================
45
+
46
+ /**
47
+ * Extract provider options from a LiveVideoAdapter via ~types.
48
+ */
49
+ export type LiveVideoProviderOptions<TAdapter> = TAdapter extends {
50
+ '~types': { providerOptions: infer P extends object }
51
+ }
52
+ ? P
53
+ : object
54
+
55
+ // ===========================
56
+ // Activity Options Type
57
+ // ===========================
58
+
59
+ /**
60
+ * Options for the live generation activity.
61
+ * The model is extracted from the adapter's model property.
62
+ *
63
+ * @template TAdapter - The live adapter type
64
+ * @template TStream - Whether to stream the output
65
+ *
66
+ * @experimental Live generation is an experimental feature and may change.
67
+ */
68
+ export interface LiveVideoActivityOptions<
69
+ TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
70
+ TStream extends boolean = false,
71
+ > {
72
+ /** The live adapter to use (must be created with a model) */
73
+ adapter: TAdapter & { kind: typeof kind }
74
+ /** Natural-language description of the shot or scene */
75
+ prompt: string
76
+ /** Provider-specific options for live generation */
77
+ modelOptions?: LiveVideoProviderOptions<TAdapter>
78
+ /**
79
+ * Whether to wrap the token result as StreamChunks for SSE transport.
80
+ * This is not the live video. When false or omitted, returns
81
+ * Promise<LiveVideoGenerationResult>.
82
+ *
83
+ * @default false
84
+ */
85
+ stream?: TStream
86
+ /**
87
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
88
+ * silence everything including errors, or a `DebugConfig` object for granular
89
+ * control and/or a custom `Logger`.
90
+ */
91
+ debug?: DebugOption
92
+ /**
93
+ * Observe-only middleware notified on start, usage, success, and error. Pass
94
+ * `otelMiddleware()` to emit OpenTelemetry spans, or implement the
95
+ * `GenerationMiddleware` contract for a custom backend.
96
+ */
97
+ middleware?: Array<GenerationMiddleware>
98
+ /** Stable conversation/thread id for correlating this run when persisted. */
99
+ threadId?: string
100
+ /** Stable run id for correlating this run when persisted. */
101
+ runId?: string
102
+ /**
103
+ * Maximum duration of the token mint in milliseconds.
104
+ * No SDK-wide default. Composed with {@link abortSignal}; the first abort wins.
105
+ */
106
+ timeout?: number
107
+ /**
108
+ * Caller cancellation signal (request disconnects, job/runtime cancellation).
109
+ * Composed with {@link timeout} into an effective signal forwarded to the
110
+ * adapter. Request-specific — not stored on global provider client config.
111
+ */
112
+ abortSignal?: AbortSignal
113
+ }
114
+
115
+ // ===========================
116
+ // Activity Result Type
117
+ // ===========================
118
+
119
+ /**
120
+ * Result type for the live generation activity.
121
+ * - If stream is true: AsyncIterable<StreamChunk>
122
+ * - Otherwise: Promise<LiveVideoGenerationResult>
123
+ */
124
+ export type LiveVideoActivityResult<TStream extends boolean = false> =
125
+ TStream extends true
126
+ ? AsyncIterable<StreamChunk>
127
+ : Promise<LiveVideoGenerationResult>
128
+
129
+ function createId(prefix: string): string {
130
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
131
+ }
132
+
133
+ // ===========================
134
+ // Activity Implementation
135
+ // ===========================
136
+
137
+ /**
138
+ * Live generation activity - opens a live, prompt-steerable video session.
139
+ *
140
+ * @example Mint a session token on the server
141
+ * ```ts
142
+ * import { generateLiveVideo } from '@tanstack/ai'
143
+ * import { reactorVideo } from '@tanstack/ai-reactor'
144
+ *
145
+ * const live = await generateLiveVideo({
146
+ * adapter: reactorVideo('helios'),
147
+ * prompt: 'A red sports car powerslides a mountain hairpin',
148
+ * })
149
+ *
150
+ * // Hand live.token, live.model, and live.prompt to the browser.
151
+ * ```
152
+ *
153
+ * @experimental Live generation is an experimental feature and may change.
154
+ */
155
+ export function generateLiveVideo<
156
+ TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
157
+ TStream extends boolean = false,
158
+ >(
159
+ options: LiveVideoActivityOptions<TAdapter, TStream>,
160
+ ): LiveVideoActivityResult<TStream> {
161
+ if (options.stream) {
162
+ return streamGenerationResult(
163
+ (resolved) => runGenerateLiveVideo({ ...options, runId: resolved.runId }),
164
+ options,
165
+ ) as LiveVideoActivityResult<TStream>
166
+ }
167
+ return runGenerateLiveVideo(options) as LiveVideoActivityResult<TStream>
168
+ }
169
+
170
+ /**
171
+ * Run the core live generation logic (non-streaming).
172
+ */
173
+ async function runGenerateLiveVideo<
174
+ TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
175
+ >(
176
+ options: LiveVideoActivityOptions<TAdapter, boolean>,
177
+ ): Promise<LiveVideoGenerationResult> {
178
+ const {
179
+ adapter,
180
+ stream: _stream,
181
+ debug: _debug,
182
+ middleware,
183
+ threadId,
184
+ runId,
185
+ timeout,
186
+ abortSignal: callerAbortSignal,
187
+ ...rest
188
+ } = options
189
+ const model = adapter.model
190
+ const requestId = createId('liveVideo')
191
+ const startTime = Date.now()
192
+ const logger: InternalLogger = resolveDebugOption(options.debug)
193
+ const abortControls = createActivityAbortControls({
194
+ timeout,
195
+ abortSignal: callerAbortSignal,
196
+ })
197
+ const providerName =
198
+ (adapter as { name?: string; provider?: string }).provider ??
199
+ (adapter as { name?: string }).name ??
200
+ 'unknown'
201
+
202
+ const mwCtx = createGenerationContext({
203
+ requestId,
204
+ activity: 'liveVideo',
205
+ provider: adapter.name,
206
+ model,
207
+ modelOptions: rest.modelOptions,
208
+ threadId,
209
+ runId,
210
+ artifactInputs: { prompt: rest.prompt },
211
+ createId,
212
+ })
213
+
214
+ await runGenerationStart(middleware, mwCtx)
215
+
216
+ aiEventClient.emit('liveVideo:request:started', {
217
+ requestId,
218
+ provider: adapter.name,
219
+ model,
220
+ prompt: rest.prompt,
221
+ timestamp: startTime,
222
+ ...(rest.modelOptions !== undefined && {
223
+ modelOptions: rest.modelOptions as Record<string, unknown>,
224
+ }),
225
+ })
226
+
227
+ logger.request(`activity=generateLiveVideo provider=${providerName}`, {
228
+ provider: providerName,
229
+ model,
230
+ })
231
+
232
+ try {
233
+ const rawResult = await raceWithAbort(
234
+ adapter.createLiveVideo({
235
+ ...rest,
236
+ model,
237
+ logger,
238
+ ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),
239
+ }),
240
+ abortControls.signal,
241
+ )
242
+ abortControls.clear()
243
+ const result = await applyGenerationResultTransforms(mwCtx, rawResult)
244
+ const elapsedMs = Date.now() - startTime
245
+
246
+ aiEventClient.emit('liveVideo:request:completed', {
247
+ requestId,
248
+ provider: adapter.name,
249
+ model: result.model,
250
+ prompt: result.prompt,
251
+ status: result.status,
252
+ duration: elapsedMs,
253
+ timestamp: Date.now(),
254
+ ...(rest.modelOptions !== undefined && {
255
+ modelOptions: rest.modelOptions as Record<string, unknown>,
256
+ }),
257
+ })
258
+
259
+ if (result.usage) {
260
+ aiEventClient.emit('liveVideo:usage', {
261
+ requestId,
262
+ model: result.model,
263
+ usage: result.usage,
264
+ timestamp: Date.now(),
265
+ ...(rest.modelOptions !== undefined && {
266
+ modelOptions: rest.modelOptions as Record<string, unknown>,
267
+ }),
268
+ })
269
+ }
270
+
271
+ logger.output(`activity=generateLiveVideo provider=${providerName}`, {
272
+ model: result.model,
273
+ status: result.status,
274
+ })
275
+
276
+ if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
277
+ await runGenerationFinish(middleware, mwCtx, {
278
+ duration: elapsedMs,
279
+ usage: result.usage,
280
+ })
281
+
282
+ return result
283
+ } catch (error) {
284
+ abortControls.clear()
285
+ const elapsedMs = Date.now() - startTime
286
+ const err = error as Error
287
+ aiEventClient.emit('liveVideo:request:error', {
288
+ requestId,
289
+ provider: adapter.name,
290
+ model,
291
+ error: { message: err.message, name: err.name },
292
+ duration: elapsedMs,
293
+ timestamp: Date.now(),
294
+ ...(rest.modelOptions !== undefined && {
295
+ modelOptions: rest.modelOptions as Record<string, unknown>,
296
+ }),
297
+ })
298
+ if (isActivityAbortError(error, abortControls.signal)) {
299
+ await runGenerationAbort(middleware, mwCtx, {
300
+ reason: abortReasonMessage(error, abortControls.signal),
301
+ duration: elapsedMs,
302
+ })
303
+ } else {
304
+ await runGenerationError(middleware, mwCtx, {
305
+ error,
306
+ duration: elapsedMs,
307
+ })
308
+ }
309
+ logger.errors('generateLiveVideo activity failed', {
310
+ error,
311
+ source: 'generateLiveVideo',
312
+ })
313
+ throw error
314
+ }
315
+ }
316
+
317
+ // ===========================
318
+ // Options Factory
319
+ // ===========================
320
+
321
+ /**
322
+ * Create typed options for the generateLiveVideo() function without executing.
323
+ */
324
+ export function createLiveVideoOptions<
325
+ TAdapter extends LiveVideoAdapter<string, LiveVideoProviderOptions<TAdapter>>,
326
+ TStream extends boolean = false,
327
+ >(
328
+ options: LiveVideoActivityOptions<TAdapter, TStream>,
329
+ ): LiveVideoActivityOptions<TAdapter, TStream> {
330
+ return options
331
+ }
332
+
333
+ // Re-export adapter types
334
+ export type {
335
+ LiveVideoAdapter,
336
+ LiveVideoAdapterConfig,
337
+ AnyLiveVideoAdapter,
338
+ } from './adapter'
339
+ export { BaseLiveVideoAdapter } from './adapter'
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Video Activity (Experimental)
3
3
  *
4
- * Generates videos from text prompts using a jobs/polling architecture.
4
+ * Generates videos from text prompts. Adapters use a jobs/polling
5
+ * architecture: create a job, poll for status, then fetch a download URL.
6
+ * For a live, prompt-steerable stream, use generateLiveVideo().
5
7
  * This is a self-contained module with implementation, types, and JSDoc.
6
8
  *
7
9
  * @experimental Video generation is an experimental feature and may change.
@@ -537,9 +539,6 @@ async function runCreateVideoJob<
537
539
 
538
540
  const mwCtx = contextFor(videoRunIdForJob(adapter.name, jobResult.jobId))
539
541
  await runGenerationStart(middleware, mwCtx)
540
- // Transforms see the submission result (no url yet, so nothing to copy into a
541
- // blob store) purely so the run record captures the jobId and any prompt
542
- // inputs. No finish hook: the run is still running.
543
542
  return await applyGenerationResultTransforms(mwCtx, jobResult)
544
543
  }
545
544