@tanstack/ai 0.6.1 → 0.6.3

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 (33) hide show
  1. package/dist/esm/activities/chat/messages.js +2 -2
  2. package/dist/esm/activities/chat/messages.js.map +1 -1
  3. package/dist/esm/activities/chat/stream/message-updaters.js +7 -4
  4. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  5. package/dist/esm/activities/chat/tools/tool-calls.js +9 -0
  6. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  7. package/dist/esm/activities/generateImage/index.d.ts +19 -6
  8. package/dist/esm/activities/generateImage/index.js +11 -2
  9. package/dist/esm/activities/generateImage/index.js.map +1 -1
  10. package/dist/esm/activities/generateSpeech/index.d.ts +19 -6
  11. package/dist/esm/activities/generateSpeech/index.js +11 -2
  12. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  13. package/dist/esm/activities/generateTranscription/index.d.ts +30 -6
  14. package/dist/esm/activities/generateTranscription/index.js +13 -2
  15. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  16. package/dist/esm/activities/generateVideo/index.d.ts +45 -7
  17. package/dist/esm/activities/generateVideo/index.js +90 -1
  18. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  19. package/dist/esm/activities/stream-generation-result.d.ts +14 -0
  20. package/dist/esm/activities/stream-generation-result.js +40 -0
  21. package/dist/esm/activities/stream-generation-result.js.map +1 -0
  22. package/dist/esm/activities/summarize/index.js +2 -17
  23. package/dist/esm/activities/summarize/index.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/activities/chat/messages.ts +5 -7
  26. package/src/activities/chat/stream/message-updaters.ts +6 -4
  27. package/src/activities/chat/tools/tool-calls.ts +14 -0
  28. package/src/activities/generateImage/index.ts +49 -7
  29. package/src/activities/generateSpeech/index.ts +41 -7
  30. package/src/activities/generateTranscription/index.ts +59 -9
  31. package/src/activities/generateVideo/index.ts +173 -6
  32. package/src/activities/stream-generation-result.ts +62 -0
  33. package/src/activities/summarize/index.ts +3 -22
@@ -324,6 +324,13 @@ export async function* executeToolCalls(
324
324
  toolMap.set(tool.name, tool)
325
325
  }
326
326
 
327
+ // Batch gating: when any tool in the batch still needs an approval decision,
328
+ // defer all execution so side effects don't happen before the user decides.
329
+ const hasPendingApprovals = toolCalls.some((tc) => {
330
+ const t = toolMap.get(tc.function.name)
331
+ return t?.needsApproval && !approvals.has(`approval_${tc.id}`)
332
+ })
333
+
327
334
  for (const toolCall of toolCalls) {
328
335
  const tool = toolMap.get(toolCall.function.name)
329
336
  const toolName = toolCall.function.name
@@ -339,6 +346,13 @@ export async function* executeToolCalls(
339
346
  continue
340
347
  }
341
348
 
349
+ // Skip non-pending tools while approvals are outstanding
350
+ if (hasPendingApprovals) {
351
+ if (!tool.needsApproval || approvals.has(`approval_${toolCall.id}`)) {
352
+ continue
353
+ }
354
+ }
355
+
342
356
  // Parse arguments, throwing error if invalid JSON
343
357
  let input: unknown = {}
344
358
  const argsStr = toolCall.function.arguments.trim() || '{}'
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import { aiEventClient } from '../../event-client.js'
9
+ import { streamGenerationResult } from '../stream-generation-result.js'
9
10
  import type { ImageAdapter } from './adapter'
10
- import type { ImageGenerationResult } from '../../types'
11
+ import type { ImageGenerationResult, StreamChunk } from '../../types'
11
12
 
12
13
  // ===========================
13
14
  // Activity Kind
@@ -60,9 +61,11 @@ export type ImageSizeForModel<TAdapter, TModel extends string> =
60
61
  * The model is extracted from the adapter's model property.
61
62
  *
62
63
  * @template TAdapter - The image adapter type
64
+ * @template TStream - Whether to stream the output
63
65
  */
64
66
  export type ImageActivityOptions<
65
67
  TAdapter extends ImageAdapter<string, any, any, any>,
68
+ TStream extends boolean = false,
66
69
  > = {
67
70
  /** The image adapter to use (must be created with a model) */
68
71
  adapter: TAdapter & { kind: typeof kind }
@@ -72,6 +75,14 @@ export type ImageActivityOptions<
72
75
  numberOfImages?: number
73
76
  /** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") */
74
77
  size?: ImageSizeForModel<TAdapter, TAdapter['model']>
78
+ /**
79
+ * Whether to stream the image generation result.
80
+ * When true, returns an AsyncIterable<StreamChunk> for streaming transport.
81
+ * When false or not provided, returns a Promise<ImageGenerationResult>.
82
+ *
83
+ * @default false
84
+ */
85
+ stream?: TStream
75
86
  } & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>
76
87
  ? {
77
88
  /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<
@@ -90,8 +101,15 @@ export type ImageActivityOptions<
90
101
  // Activity Result Type
91
102
  // ===========================
92
103
 
93
- /** Result type for the image activity */
94
- export type ImageActivityResult = Promise<ImageGenerationResult>
104
+ /**
105
+ * Result type for the image activity.
106
+ * - If stream is true: AsyncIterable<StreamChunk>
107
+ * - Otherwise: Promise<ImageGenerationResult>
108
+ */
109
+ export type ImageActivityResult<TStream extends boolean = false> =
110
+ TStream extends true
111
+ ? AsyncIterable<StreamChunk>
112
+ : Promise<ImageGenerationResult>
95
113
 
96
114
  function createId(prefix: string): string {
97
115
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
@@ -146,10 +164,31 @@ function createId(prefix: string): string {
146
164
  * })
147
165
  * ```
148
166
  */
149
- export async function generateImage<
167
+ export function generateImage<
168
+ TAdapter extends ImageAdapter<string, any, any, any>,
169
+ TStream extends boolean = false,
170
+ >(
171
+ options: ImageActivityOptions<TAdapter, TStream>,
172
+ ): ImageActivityResult<TStream> {
173
+ if (options.stream) {
174
+ return streamGenerationResult(() =>
175
+ runGenerateImage(options),
176
+ ) as ImageActivityResult<TStream>
177
+ }
178
+
179
+ return runGenerateImage(options) as ImageActivityResult<TStream>
180
+ }
181
+
182
+ /**
183
+ * Internal implementation of image generation (always non-streaming).
184
+ * Contains all devtools event emission logic.
185
+ */
186
+ async function runGenerateImage<
150
187
  TAdapter extends ImageAdapter<string, any, any, any>,
151
- >(options: ImageActivityOptions<TAdapter>): ImageActivityResult {
152
- const { adapter, ...rest } = options
188
+ >(
189
+ options: ImageActivityOptions<TAdapter, boolean>,
190
+ ): Promise<ImageGenerationResult> {
191
+ const { adapter, stream: _stream, ...rest } = options
153
192
  const model = adapter.model
154
193
  const requestId = createId('image')
155
194
  const startTime = Date.now()
@@ -204,7 +243,10 @@ export async function generateImage<
204
243
  */
205
244
  export function createImageOptions<
206
245
  TAdapter extends ImageAdapter<string, any, any, any>,
207
- >(options: ImageActivityOptions<TAdapter>): ImageActivityOptions<TAdapter> {
246
+ TStream extends boolean = false,
247
+ >(
248
+ options: ImageActivityOptions<TAdapter, TStream>,
249
+ ): ImageActivityOptions<TAdapter, TStream> {
208
250
  return options
209
251
  }
210
252
 
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import { aiEventClient } from '../../event-client.js'
9
+ import { streamGenerationResult } from '../stream-generation-result.js'
9
10
  import type { TTSAdapter } from './adapter'
10
- import type { TTSResult } from '../../types'
11
+ import type { StreamChunk, TTSResult } from '../../types'
11
12
 
12
13
  // ===========================
13
14
  // Activity Kind
@@ -37,9 +38,11 @@ export type TTSProviderOptions<TAdapter> =
37
38
  * The model is extracted from the adapter's model property.
38
39
  *
39
40
  * @template TAdapter - The TTS adapter type
41
+ * @template TStream - Whether to stream the output
40
42
  */
41
43
  export interface TTSActivityOptions<
42
44
  TAdapter extends TTSAdapter<string, object>,
45
+ TStream extends boolean = false,
43
46
  > {
44
47
  /** The TTS adapter to use (must be created with a model) */
45
48
  adapter: TAdapter & { kind: typeof kind }
@@ -53,14 +56,27 @@ export interface TTSActivityOptions<
53
56
  speed?: number
54
57
  /** Provider-specific options for TTS generation */
55
58
  modelOptions?: TTSProviderOptions<TAdapter>
59
+ /**
60
+ * Whether to stream the generation result.
61
+ * When true, returns an AsyncIterable<StreamChunk> for streaming transport.
62
+ * When false or not provided, returns a Promise<TTSResult>.
63
+ *
64
+ * @default false
65
+ */
66
+ stream?: TStream
56
67
  }
57
68
 
58
69
  // ===========================
59
70
  // Activity Result Type
60
71
  // ===========================
61
72
 
62
- /** Result type for the TTS activity */
63
- export type TTSActivityResult = Promise<TTSResult>
73
+ /**
74
+ * Result type for the TTS activity.
75
+ * - If stream is true: AsyncIterable<StreamChunk>
76
+ * - Otherwise: Promise<TTSResult>
77
+ */
78
+ export type TTSActivityResult<TStream extends boolean = false> =
79
+ TStream extends true ? AsyncIterable<StreamChunk> : Promise<TTSResult>
64
80
 
65
81
  function createId(prefix: string): string {
66
82
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
@@ -100,10 +116,25 @@ function createId(prefix: string): string {
100
116
  * })
101
117
  * ```
102
118
  */
103
- export async function generateSpeech<
119
+ export function generateSpeech<
104
120
  TAdapter extends TTSAdapter<string, object>,
105
- >(options: TTSActivityOptions<TAdapter>): TTSActivityResult {
106
- const { adapter, ...rest } = options
121
+ TStream extends boolean = false,
122
+ >(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {
123
+ if (options.stream) {
124
+ return streamGenerationResult(() =>
125
+ runGenerateSpeech(options),
126
+ ) as TTSActivityResult<TStream>
127
+ }
128
+ return runGenerateSpeech(options) as TTSActivityResult<TStream>
129
+ }
130
+
131
+ /**
132
+ * Run the core TTS generation logic (non-streaming).
133
+ */
134
+ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
135
+ options: TTSActivityOptions<TAdapter, boolean>,
136
+ ): Promise<TTSResult> {
137
+ const { adapter, stream: _stream, ...rest } = options
107
138
  const model = adapter.model
108
139
  const requestId = createId('speech')
109
140
  const startTime = Date.now()
@@ -149,7 +180,10 @@ export async function generateSpeech<
149
180
  */
150
181
  export function createSpeechOptions<
151
182
  TAdapter extends TTSAdapter<string, object>,
152
- >(options: TTSActivityOptions<TAdapter>): TTSActivityOptions<TAdapter> {
183
+ TStream extends boolean = false,
184
+ >(
185
+ options: TTSActivityOptions<TAdapter, TStream>,
186
+ ): TTSActivityOptions<TAdapter, TStream> {
153
187
  return options
154
188
  }
155
189
 
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import { aiEventClient } from '../../event-client.js'
9
+ import { streamGenerationResult } from '../stream-generation-result.js'
9
10
  import type { TranscriptionAdapter } from './adapter'
10
- import type { TranscriptionResult } from '../../types'
11
+ import type { StreamChunk, TranscriptionResult } from '../../types'
11
12
 
12
13
  // ===========================
13
14
  // Activity Kind
@@ -37,9 +38,11 @@ export type TranscriptionProviderOptions<TAdapter> =
37
38
  * The model is extracted from the adapter's model property.
38
39
  *
39
40
  * @template TAdapter - The transcription adapter type
41
+ * @template TStream - Whether to stream the output
40
42
  */
41
43
  export interface TranscriptionActivityOptions<
42
44
  TAdapter extends TranscriptionAdapter<string, object>,
45
+ TStream extends boolean = false,
43
46
  > {
44
47
  /** The transcription adapter to use (must be created with a model) */
45
48
  adapter: TAdapter & { kind: typeof kind }
@@ -53,14 +56,29 @@ export interface TranscriptionActivityOptions<
53
56
  responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'
54
57
  /** Provider-specific options for transcription */
55
58
  modelOptions?: TranscriptionProviderOptions<TAdapter>
59
+ /**
60
+ * Whether to stream the transcription result.
61
+ * When true, returns an AsyncIterable<StreamChunk> for streaming transport.
62
+ * When false or not provided, returns a Promise<TranscriptionResult>.
63
+ *
64
+ * @default false
65
+ */
66
+ stream?: TStream
56
67
  }
57
68
 
58
69
  // ===========================
59
70
  // Activity Result Type
60
71
  // ===========================
61
72
 
62
- /** Result type for the transcription activity */
63
- export type TranscriptionActivityResult = Promise<TranscriptionResult>
73
+ /**
74
+ * Result type for the transcription activity.
75
+ * - If stream is true: AsyncIterable<StreamChunk>
76
+ * - Otherwise: Promise<TranscriptionResult>
77
+ */
78
+ export type TranscriptionActivityResult<TStream extends boolean = false> =
79
+ TStream extends true
80
+ ? AsyncIterable<StreamChunk>
81
+ : Promise<TranscriptionResult>
64
82
 
65
83
  function createId(prefix: string): string {
66
84
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
@@ -101,13 +119,44 @@ function createId(prefix: string): string {
101
119
  * console.log(`[${segment.start}s - ${segment.end}s]: ${segment.text}`)
102
120
  * })
103
121
  * ```
122
+ *
123
+ * @example Streaming transcription result
124
+ * ```ts
125
+ * for await (const chunk of generateTranscription({
126
+ * adapter: openaiTranscription('whisper-1'),
127
+ * audio: audioFile,
128
+ * stream: true
129
+ * })) {
130
+ * console.log(chunk)
131
+ * }
132
+ * ```
133
+ */
134
+ export function generateTranscription<
135
+ TAdapter extends TranscriptionAdapter<string, object>,
136
+ TStream extends boolean = false,
137
+ >(
138
+ options: TranscriptionActivityOptions<TAdapter, TStream>,
139
+ ): TranscriptionActivityResult<TStream> {
140
+ if (options.stream) {
141
+ return streamGenerationResult(() =>
142
+ runGenerateTranscription(options),
143
+ ) as TranscriptionActivityResult<TStream>
144
+ }
145
+
146
+ return runGenerateTranscription(
147
+ options,
148
+ ) as TranscriptionActivityResult<TStream>
149
+ }
150
+
151
+ /**
152
+ * Run non-streaming transcription
104
153
  */
105
- export async function generateTranscription<
154
+ async function runGenerateTranscription<
106
155
  TAdapter extends TranscriptionAdapter<string, object>,
107
156
  >(
108
- options: TranscriptionActivityOptions<TAdapter>,
109
- ): TranscriptionActivityResult {
110
- const { adapter, ...rest } = options
157
+ options: TranscriptionActivityOptions<TAdapter, boolean>,
158
+ ): Promise<TranscriptionResult> {
159
+ const { adapter, stream: _stream, ...rest } = options
111
160
  const model = adapter.model
112
161
  const requestId = createId('transcription')
113
162
  const startTime = Date.now()
@@ -149,9 +198,10 @@ export async function generateTranscription<
149
198
  */
150
199
  export function createTranscriptionOptions<
151
200
  TAdapter extends TranscriptionAdapter<string, object>,
201
+ TStream extends boolean = false,
152
202
  >(
153
- options: TranscriptionActivityOptions<TAdapter>,
154
- ): TranscriptionActivityOptions<TAdapter> {
203
+ options: TranscriptionActivityOptions<TAdapter, TStream>,
204
+ ): TranscriptionActivityOptions<TAdapter, TStream> {
155
205
  return options
156
206
  }
157
207
 
@@ -10,6 +10,7 @@
10
10
  import { aiEventClient } from '../../event-client.js'
11
11
  import type { VideoAdapter } from './adapter'
12
12
  import type {
13
+ StreamChunk,
13
14
  VideoJobResult,
14
15
  VideoStatusResult,
15
16
  VideoUrlResult,
@@ -67,10 +68,14 @@ interface VideoActivityBaseOptions<
67
68
  * Options for creating a new video generation job.
68
69
  * The model is extracted from the adapter's model property.
69
70
  *
71
+ * @template TAdapter - The video adapter type
72
+ * @template TStream - Whether to stream the output
73
+ *
70
74
  * @experimental Video generation is an experimental feature and may change.
71
75
  */
72
76
  export type VideoCreateOptions<
73
77
  TAdapter extends VideoAdapter<string, any, any, any>,
78
+ TStream extends boolean = false,
74
79
  > = VideoActivityBaseOptions<TAdapter> & {
75
80
  /** Request type - create a new job (default if not specified) */
76
81
  request?: 'create'
@@ -80,6 +85,21 @@ export type VideoCreateOptions<
80
85
  size?: VideoSizeForAdapter<TAdapter>
81
86
  /** Video duration in seconds */
82
87
  duration?: number
88
+ /**
89
+ * Whether to stream the video generation lifecycle.
90
+ * When true, returns an AsyncIterable<StreamChunk> that handles the full
91
+ * job lifecycle: create job, poll for status, yield updates, and yield final result.
92
+ * When false or not provided, returns a Promise<VideoJobResult>.
93
+ *
94
+ * @default false
95
+ */
96
+ stream?: TStream
97
+ /** Polling interval in milliseconds (stream mode only). @default 2000 */
98
+ pollingInterval?: number
99
+ /** Maximum time to wait before timing out in milliseconds (stream mode only). @default 600000 */
100
+ maxDuration?: number
101
+ /** Custom run ID (stream mode only) */
102
+ runId?: string
83
103
  } & ({} extends VideoProviderOptions<TAdapter>
84
104
  ? {
85
105
  /** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>
@@ -125,28 +145,34 @@ export interface VideoUrlOptions<
125
145
  export type VideoActivityOptions<
126
146
  TAdapter extends VideoAdapter<string, any, any, any>,
127
147
  TRequest extends 'create' | 'status' | 'url' = 'create',
148
+ TStream extends boolean = false,
128
149
  > = TRequest extends 'status'
129
150
  ? VideoStatusOptions<TAdapter>
130
151
  : TRequest extends 'url'
131
152
  ? VideoUrlOptions<TAdapter>
132
- : VideoCreateOptions<TAdapter>
153
+ : VideoCreateOptions<TAdapter, TStream>
133
154
 
134
155
  // ===========================
135
156
  // Activity Result Types
136
157
  // ===========================
137
158
 
138
159
  /**
139
- * Result type for the video activity, based on request type.
160
+ * Result type for the video activity, based on request type and streaming.
161
+ * - If stream is true (create request): AsyncIterable<StreamChunk>
162
+ * - Otherwise: Promise<VideoJobResult | VideoStatusResult | VideoUrlResult>
140
163
  *
141
164
  * @experimental Video generation is an experimental feature and may change.
142
165
  */
143
166
  export type VideoActivityResult<
144
167
  TRequest extends 'create' | 'status' | 'url' = 'create',
168
+ TStream extends boolean = false,
145
169
  > = TRequest extends 'status'
146
170
  ? Promise<VideoStatusResult>
147
171
  : TRequest extends 'url'
148
172
  ? Promise<VideoUrlResult>
149
- : Promise<VideoJobResult>
173
+ : TStream extends true
174
+ ? AsyncIterable<StreamChunk>
175
+ : Promise<VideoJobResult>
150
176
 
151
177
  // ===========================
152
178
  // Activity Implementation
@@ -158,6 +184,9 @@ export type VideoActivityResult<
158
184
  * Uses AI video generation models to create videos based on natural language descriptions.
159
185
  * Unlike image generation, video generation is asynchronous and requires polling for completion.
160
186
  *
187
+ * When `stream: true` is passed, handles the full job lifecycle automatically:
188
+ * create job → poll for status → stream updates → yield final result.
189
+ *
161
190
  * @experimental Video generation is an experimental feature and may change.
162
191
  *
163
192
  * @example Create a video generation job
@@ -173,10 +202,43 @@ export type VideoActivityResult<
173
202
  *
174
203
  * console.log('Job started:', jobId)
175
204
  * ```
205
+ *
206
+ * @example Stream the full video generation lifecycle
207
+ * ```ts
208
+ * import { generateVideo, toServerSentEventsResponse } from '@tanstack/ai'
209
+ * import { openaiVideo } from '@tanstack/ai-openai'
210
+ *
211
+ * const stream = generateVideo({
212
+ * adapter: openaiVideo('sora-2'),
213
+ * prompt: 'A cat chasing a dog in a sunny park',
214
+ * stream: true,
215
+ * pollingInterval: 3000,
216
+ * })
217
+ *
218
+ * return toServerSentEventsResponse(stream)
219
+ * ```
220
+ */
221
+ export function generateVideo<
222
+ TAdapter extends VideoAdapter<string, any, any, any>,
223
+ TStream extends boolean = false,
224
+ >(
225
+ options: VideoCreateOptions<TAdapter, TStream>,
226
+ ): VideoActivityResult<'create', TStream> {
227
+ if (options.stream) {
228
+ return runStreamingVideoGeneration(
229
+ options as VideoCreateOptions<TAdapter, true>,
230
+ ) as VideoActivityResult<'create', TStream>
231
+ }
232
+
233
+ return runCreateVideoJob(options) as VideoActivityResult<'create', TStream>
234
+ }
235
+
236
+ /**
237
+ * Internal implementation of non-streaming video job creation.
176
238
  */
177
- export async function generateVideo<
239
+ async function runCreateVideoJob<
178
240
  TAdapter extends VideoAdapter<string, any, any, any>,
179
- >(options: VideoCreateOptions<TAdapter>): Promise<VideoJobResult> {
241
+ >(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> {
180
242
  const { adapter, prompt, size, duration, modelOptions } = options
181
243
  const model = adapter.model
182
244
 
@@ -189,6 +251,108 @@ export async function generateVideo<
189
251
  })
190
252
  }
191
253
 
254
+ function sleep(ms: number): Promise<void> {
255
+ return new Promise((resolve) => setTimeout(resolve, ms))
256
+ }
257
+
258
+ /**
259
+ * Internal streaming implementation for video generation.
260
+ * Handles the full job lifecycle: create job → poll for status → stream updates → yield final result.
261
+ */
262
+ async function* runStreamingVideoGeneration<
263
+ TAdapter extends VideoAdapter<string, any, any, any>,
264
+ >(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> {
265
+ const { adapter, prompt, size, duration, modelOptions } = options
266
+ const model = adapter.model
267
+ const runId = options.runId ?? createId('run')
268
+ const pollingInterval = options.pollingInterval ?? 2000
269
+ const maxDuration = options.maxDuration ?? 600_000
270
+
271
+ yield {
272
+ type: 'RUN_STARTED',
273
+ runId,
274
+ timestamp: Date.now(),
275
+ }
276
+
277
+ try {
278
+ // Create the video generation job
279
+ const jobResult = await adapter.createVideoJob({
280
+ model,
281
+ prompt,
282
+ size,
283
+ duration,
284
+ modelOptions,
285
+ })
286
+
287
+ yield {
288
+ type: 'CUSTOM',
289
+ name: 'video:job:created',
290
+ value: { jobId: jobResult.jobId },
291
+ timestamp: Date.now(),
292
+ }
293
+
294
+ // Poll for completion
295
+ const startTime = Date.now()
296
+ while (Date.now() - startTime < maxDuration) {
297
+ await sleep(pollingInterval)
298
+
299
+ const statusResult = await adapter.getVideoStatus(jobResult.jobId)
300
+
301
+ yield {
302
+ type: 'CUSTOM',
303
+ name: 'video:status',
304
+ value: {
305
+ jobId: jobResult.jobId,
306
+ status: statusResult.status,
307
+ progress: statusResult.progress,
308
+ error: statusResult.error,
309
+ },
310
+ timestamp: Date.now(),
311
+ }
312
+
313
+ if (statusResult.status === 'completed') {
314
+ const urlResult = await adapter.getVideoUrl(jobResult.jobId)
315
+
316
+ yield {
317
+ type: 'CUSTOM',
318
+ name: 'generation:result',
319
+ value: {
320
+ jobId: jobResult.jobId,
321
+ status: 'completed',
322
+ url: urlResult.url,
323
+ expiresAt: urlResult.expiresAt,
324
+ },
325
+ timestamp: Date.now(),
326
+ }
327
+
328
+ yield {
329
+ type: 'RUN_FINISHED',
330
+ runId,
331
+ finishReason: 'stop',
332
+ timestamp: Date.now(),
333
+ }
334
+ return
335
+ }
336
+
337
+ if (statusResult.status === 'failed') {
338
+ throw new Error(statusResult.error || 'Video generation failed')
339
+ }
340
+ }
341
+
342
+ throw new Error('Video generation timed out')
343
+ } catch (error: any) {
344
+ yield {
345
+ type: 'RUN_ERROR',
346
+ runId,
347
+ error: {
348
+ message: error.message || 'Video generation failed',
349
+ code: error.code,
350
+ },
351
+ timestamp: Date.now(),
352
+ }
353
+ }
354
+ }
355
+
192
356
  /**
193
357
  * Get video job status - returns the current status, progress, and URL if available.
194
358
  *
@@ -316,7 +480,10 @@ export async function getVideoJobStatus<
316
480
  */
317
481
  export function createVideoOptions<
318
482
  TAdapter extends VideoAdapter<string, any, any, any>,
319
- >(options: VideoCreateOptions<TAdapter>): VideoCreateOptions<TAdapter> {
483
+ TStream extends boolean = false,
484
+ >(
485
+ options: VideoCreateOptions<TAdapter, TStream>,
486
+ ): VideoCreateOptions<TAdapter, TStream> {
320
487
  return options
321
488
  }
322
489
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Internal helper for wrapping one-shot generation results as StreamChunk
3
+ * async iterables. NOT exported from the package — used only by activity
4
+ * implementations to support `stream: true`.
5
+ */
6
+
7
+ import type { StreamChunk } from '../types'
8
+
9
+ function createId(prefix: string): string {
10
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
11
+ }
12
+
13
+ /**
14
+ * Wrap a one-shot generation result as a StreamChunk async iterable.
15
+ *
16
+ * This allows non-streaming activities (image, speech, transcription, summarize)
17
+ * to be sent over the same streaming transport as chat.
18
+ *
19
+ * @param generator - An async function that performs the generation and returns the result
20
+ * @param options - Optional configuration (runId)
21
+ * @returns An AsyncIterable of StreamChunks with RUN_STARTED, CUSTOM(generation:result), and RUN_FINISHED events
22
+ */
23
+ export async function* streamGenerationResult<TResult>(
24
+ generator: () => Promise<TResult>,
25
+ options?: { runId?: string },
26
+ ): AsyncIterable<StreamChunk> {
27
+ const runId = options?.runId ?? createId('run')
28
+
29
+ yield {
30
+ type: 'RUN_STARTED',
31
+ runId,
32
+ timestamp: Date.now(),
33
+ }
34
+
35
+ try {
36
+ const result = await generator()
37
+
38
+ yield {
39
+ type: 'CUSTOM',
40
+ name: 'generation:result',
41
+ value: result as unknown,
42
+ timestamp: Date.now(),
43
+ }
44
+
45
+ yield {
46
+ type: 'RUN_FINISHED',
47
+ runId,
48
+ finishReason: 'stop',
49
+ timestamp: Date.now(),
50
+ }
51
+ } catch (error: any) {
52
+ yield {
53
+ type: 'RUN_ERROR',
54
+ runId,
55
+ error: {
56
+ message: error.message || 'Generation failed',
57
+ code: error.code,
58
+ },
59
+ timestamp: Date.now(),
60
+ }
61
+ }
62
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { aiEventClient } from '../../event-client.js'
9
+ import { streamGenerationResult } from '../stream-generation-result.js'
9
10
  import type { SummarizeAdapter } from './adapter'
10
11
  import type {
11
12
  StreamChunk,
@@ -239,28 +240,8 @@ async function* runStreamingSummarize(
239
240
  return
240
241
  }
241
242
 
242
- // Fall back to non-streaming and yield as a single chunk
243
- const result = await adapter.summarize(summarizeOptions)
244
-
245
- // Yield TEXT_MESSAGE_CONTENT event with the summary
246
- yield {
247
- type: 'TEXT_MESSAGE_CONTENT',
248
- messageId: result.id,
249
- model: result.model,
250
- timestamp: Date.now(),
251
- delta: result.summary,
252
- content: result.summary,
253
- }
254
-
255
- // Yield RUN_FINISHED event
256
- yield {
257
- type: 'RUN_FINISHED',
258
- runId: result.id,
259
- model: result.model,
260
- timestamp: Date.now(),
261
- finishReason: 'stop',
262
- usage: result.usage,
263
- }
243
+ // Fall back to non-streaming — wrap result with streamGenerationResult
244
+ yield* streamGenerationResult(() => adapter.summarize(summarizeOptions))
264
245
  }
265
246
 
266
247
  // ===========================