@tanstack/ai 0.6.2 → 0.8.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 (67) hide show
  1. package/dist/esm/activities/chat/index.d.ts +20 -0
  2. package/dist/esm/activities/chat/index.js +248 -213
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/middleware/compose.d.ts +66 -0
  5. package/dist/esm/activities/chat/middleware/compose.js +327 -0
  6. package/dist/esm/activities/chat/middleware/compose.js.map +1 -0
  7. package/dist/esm/activities/chat/middleware/index.d.ts +2 -0
  8. package/dist/esm/activities/chat/middleware/tool-cache-middleware.d.ts +89 -0
  9. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js +76 -0
  10. package/dist/esm/activities/chat/middleware/tool-cache-middleware.js.map +1 -0
  11. package/dist/esm/activities/chat/middleware/types.d.ts +307 -0
  12. package/dist/esm/activities/chat/tools/tool-calls.d.ts +16 -1
  13. package/dist/esm/activities/chat/tools/tool-calls.js +148 -64
  14. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  15. package/dist/esm/activities/generateImage/index.d.ts +19 -6
  16. package/dist/esm/activities/generateImage/index.js +12 -3
  17. package/dist/esm/activities/generateImage/index.js.map +1 -1
  18. package/dist/esm/activities/generateSpeech/index.d.ts +19 -6
  19. package/dist/esm/activities/generateSpeech/index.js +12 -3
  20. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  21. package/dist/esm/activities/generateTranscription/index.d.ts +30 -6
  22. package/dist/esm/activities/generateTranscription/index.js +14 -3
  23. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  24. package/dist/esm/activities/generateVideo/index.d.ts +45 -7
  25. package/dist/esm/activities/generateVideo/index.js +91 -2
  26. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  27. package/dist/esm/activities/stream-generation-result.d.ts +14 -0
  28. package/dist/esm/activities/stream-generation-result.js +40 -0
  29. package/dist/esm/activities/stream-generation-result.js.map +1 -0
  30. package/dist/esm/activities/summarize/index.js +3 -18
  31. package/dist/esm/activities/summarize/index.js.map +1 -1
  32. package/dist/esm/index.d.ts +3 -1
  33. package/dist/esm/index.js +2 -2
  34. package/dist/esm/middlewares/content-guard.d.ts +77 -0
  35. package/dist/esm/middlewares/content-guard.js +155 -0
  36. package/dist/esm/middlewares/content-guard.js.map +1 -0
  37. package/dist/esm/middlewares/index.d.ts +2 -0
  38. package/dist/esm/middlewares/index.js +7 -0
  39. package/dist/esm/middlewares/index.js.map +1 -0
  40. package/dist/esm/middlewares/tool-cache.d.ts +1 -0
  41. package/dist/esm/realtime/index.d.ts +30 -0
  42. package/dist/esm/realtime/index.js +8 -0
  43. package/dist/esm/realtime/index.js.map +1 -0
  44. package/dist/esm/realtime/types.d.ts +234 -0
  45. package/package.json +6 -6
  46. package/src/activities/chat/index.ts +322 -256
  47. package/src/activities/chat/middleware/compose.ts +392 -0
  48. package/src/activities/chat/middleware/index.ts +17 -0
  49. package/src/activities/chat/middleware/tool-cache-middleware.ts +189 -0
  50. package/src/activities/chat/middleware/types.ts +419 -0
  51. package/src/activities/chat/tools/tool-calls.ts +225 -87
  52. package/src/activities/generateImage/index.ts +50 -8
  53. package/src/activities/generateSpeech/index.ts +42 -8
  54. package/src/activities/generateTranscription/index.ts +60 -10
  55. package/src/activities/generateVideo/index.ts +174 -7
  56. package/src/activities/stream-generation-result.ts +62 -0
  57. package/src/activities/summarize/index.ts +4 -23
  58. package/src/index.ts +41 -2
  59. package/src/middlewares/content-guard.ts +285 -0
  60. package/src/middlewares/index.ts +13 -0
  61. package/src/middlewares/tool-cache.ts +6 -0
  62. package/src/realtime/index.ts +38 -0
  63. package/src/realtime/types.ts +294 -0
  64. package/dist/esm/event-client.d.ts +0 -394
  65. package/dist/esm/event-client.js +0 -13
  66. package/dist/esm/event-client.js.map +0 -1
  67. package/src/event-client.ts +0 -497
@@ -10,6 +10,41 @@ import type {
10
10
  ToolCallStartEvent,
11
11
  ToolExecutionContext,
12
12
  } from '../../../types'
13
+ import type {
14
+ AfterToolCallInfo,
15
+ BeforeToolCallDecision,
16
+ } from '../middleware/types'
17
+
18
+ function safeJsonParse(value: string): unknown {
19
+ try {
20
+ return JSON.parse(value)
21
+ } catch {
22
+ return value
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Optional middleware hooks for tool execution.
28
+ * When provided, these callbacks are invoked before/after each tool execution.
29
+ */
30
+ export interface ToolExecutionMiddlewareHooks {
31
+ onBeforeToolCall?: (
32
+ toolCall: ToolCall,
33
+ tool: Tool | undefined,
34
+ args: unknown,
35
+ ) => Promise<BeforeToolCallDecision>
36
+ onAfterToolCall?: (info: AfterToolCallInfo) => Promise<void>
37
+ }
38
+
39
+ /**
40
+ * Error thrown when middleware decides to abort the chat run during tool execution.
41
+ */
42
+ export class MiddlewareAbortError extends Error {
43
+ constructor(reason: string) {
44
+ super(reason)
45
+ this.name = 'MiddlewareAbortError'
46
+ }
47
+ }
13
48
 
14
49
  /**
15
50
  * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.
@@ -289,6 +324,152 @@ async function* executeWithEventPolling<T>(
289
324
  return state.result
290
325
  }
291
326
 
327
+ /**
328
+ * Apply a middleware onBeforeToolCall decision.
329
+ * Returns the (possibly transformed) input if execution should proceed,
330
+ * or undefined if the tool call was skipped (result already pushed).
331
+ * Throws MiddlewareAbortError if the decision is 'abort'.
332
+ */
333
+ async function applyBeforeToolCallDecision(
334
+ toolCall: ToolCall,
335
+ tool: Tool,
336
+ input: unknown,
337
+ toolName: string,
338
+ middlewareHooks: ToolExecutionMiddlewareHooks,
339
+ results: Array<ToolResult>,
340
+ ): Promise<{ proceed: true; input: unknown } | { proceed: false }> {
341
+ if (!middlewareHooks.onBeforeToolCall) {
342
+ return { proceed: true, input }
343
+ }
344
+
345
+ const decision = await middlewareHooks.onBeforeToolCall(toolCall, tool, input)
346
+ if (!decision) {
347
+ return { proceed: true, input }
348
+ }
349
+
350
+ if (decision.type === 'abort') {
351
+ throw new MiddlewareAbortError(decision.reason || 'Aborted by middleware')
352
+ }
353
+
354
+ if (decision.type === 'skip') {
355
+ const skipResult = decision.result
356
+ results.push({
357
+ toolCallId: toolCall.id,
358
+ toolName,
359
+ result:
360
+ typeof skipResult === 'string'
361
+ ? safeJsonParse(skipResult)
362
+ : skipResult || null,
363
+ duration: 0,
364
+ })
365
+ if (middlewareHooks.onAfterToolCall) {
366
+ await middlewareHooks.onAfterToolCall({
367
+ toolCall,
368
+ tool,
369
+ toolName,
370
+ toolCallId: toolCall.id,
371
+ ok: true,
372
+ duration: 0,
373
+ result: skipResult,
374
+ })
375
+ }
376
+ return { proceed: false }
377
+ }
378
+
379
+ return { proceed: true, input: decision.args }
380
+ }
381
+
382
+ /**
383
+ * Execute a server-side tool with event polling, output validation, and middleware hooks.
384
+ * Yields CustomEvent chunks during execution and pushes the result to the results array.
385
+ */
386
+ async function* executeServerTool(
387
+ toolCall: ToolCall,
388
+ tool: Tool,
389
+ toolName: string,
390
+ input: unknown,
391
+ context: ToolExecutionContext,
392
+ pendingEvents: Array<CustomEvent>,
393
+ results: Array<ToolResult>,
394
+ middlewareHooks?: ToolExecutionMiddlewareHooks,
395
+ ): AsyncGenerator<CustomEvent, void, void> {
396
+ const startTime = Date.now()
397
+ try {
398
+ const executionPromise = Promise.resolve(tool.execute!(input, context))
399
+ let result = yield* executeWithEventPolling(executionPromise, pendingEvents)
400
+ const duration = Date.now() - startTime
401
+
402
+ // Flush remaining events
403
+ while (pendingEvents.length > 0) {
404
+ yield pendingEvents.shift()!
405
+ }
406
+
407
+ // Validate output against outputSchema if provided
408
+ if (
409
+ tool.outputSchema &&
410
+ isStandardSchema(tool.outputSchema) &&
411
+ result !== undefined &&
412
+ result !== null
413
+ ) {
414
+ result = parseWithStandardSchema(tool.outputSchema, result)
415
+ }
416
+
417
+ const finalResult =
418
+ typeof result === 'string' ? safeJsonParse(result) : result || null
419
+
420
+ results.push({
421
+ toolCallId: toolCall.id,
422
+ toolName,
423
+ result: finalResult,
424
+ duration,
425
+ })
426
+
427
+ if (middlewareHooks?.onAfterToolCall) {
428
+ await middlewareHooks.onAfterToolCall({
429
+ toolCall,
430
+ tool,
431
+ toolName,
432
+ toolCallId: toolCall.id,
433
+ ok: true,
434
+ duration,
435
+ result: finalResult,
436
+ })
437
+ }
438
+ } catch (error: unknown) {
439
+ const duration = Date.now() - startTime
440
+
441
+ // Flush remaining events
442
+ while (pendingEvents.length > 0) {
443
+ yield pendingEvents.shift()!
444
+ }
445
+
446
+ if (error instanceof MiddlewareAbortError) {
447
+ throw error
448
+ }
449
+
450
+ const message = error instanceof Error ? error.message : 'Unknown error'
451
+ results.push({
452
+ toolCallId: toolCall.id,
453
+ toolName,
454
+ result: { error: message },
455
+ state: 'output-error',
456
+ duration,
457
+ })
458
+
459
+ if (middlewareHooks?.onAfterToolCall) {
460
+ await middlewareHooks.onAfterToolCall({
461
+ toolCall,
462
+ tool,
463
+ toolName,
464
+ toolCallId: toolCall.id,
465
+ ok: false,
466
+ duration,
467
+ error,
468
+ })
469
+ }
470
+ }
471
+ }
472
+
292
473
  /**
293
474
  * Execute tool calls based on their configuration.
294
475
  * Yields CustomEvent chunks during tool execution for real-time progress updates.
@@ -313,6 +494,7 @@ export async function* executeToolCalls(
313
494
  eventName: string,
314
495
  value: Record<string, any>,
315
496
  ) => CustomEvent,
497
+ middlewareHooks?: ToolExecutionMiddlewareHooks,
316
498
  ): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
317
499
  const results: Array<ToolResult> = []
318
500
  const needsApproval: Array<ApprovalRequest> = []
@@ -402,13 +584,6 @@ export async function* executeToolCalls(
402
584
  },
403
585
  }
404
586
 
405
- // Helper to flush any pending events
406
- function* flushEvents(): Generator<CustomEvent> {
407
- while (pendingEvents.length > 0) {
408
- yield pendingEvents.shift()!
409
- }
410
- }
411
-
412
587
  // CASE 1: Client-side tool (no execute function)
413
588
  if (!tool.execute) {
414
589
  // Check if tool needs approval
@@ -482,51 +657,30 @@ export async function* executeToolCalls(
482
657
  const approved = approvals.get(approvalId)
483
658
 
484
659
  if (approved) {
485
- // Execute after approval
486
- const startTime = Date.now()
487
- try {
488
- const executionPromise = Promise.resolve(
489
- tool.execute(input, context),
490
- )
491
- let result = yield* executeWithEventPolling(
492
- executionPromise,
493
- pendingEvents,
494
- )
495
- const duration = Date.now() - startTime
496
- yield* flushEvents()
497
-
498
- // Validate output against outputSchema if provided (for Standard Schema compliant schemas)
499
- if (
500
- tool.outputSchema &&
501
- isStandardSchema(tool.outputSchema) &&
502
- result !== undefined &&
503
- result !== null
504
- ) {
505
- result = parseWithStandardSchema(tool.outputSchema, result)
506
- }
507
-
508
- results.push({
509
- toolCallId: toolCall.id,
510
- toolName,
511
- result:
512
- typeof result === 'string'
513
- ? JSON.parse(result)
514
- : result || null,
515
- duration,
516
- })
517
- } catch (error: unknown) {
518
- const duration = Date.now() - startTime
519
- yield* flushEvents()
520
- const message =
521
- error instanceof Error ? error.message : 'Unknown error'
522
- results.push({
523
- toolCallId: toolCall.id,
660
+ // Apply middleware before-hook for approved tools
661
+ if (middlewareHooks) {
662
+ const decision = await applyBeforeToolCallDecision(
663
+ toolCall,
664
+ tool,
665
+ input,
524
666
  toolName,
525
- result: { error: message },
526
- state: 'output-error',
527
- duration,
528
- })
667
+ middlewareHooks,
668
+ results,
669
+ )
670
+ if (!decision.proceed) continue
671
+ input = decision.input
529
672
  }
673
+
674
+ yield* executeServerTool(
675
+ toolCall,
676
+ tool,
677
+ toolName,
678
+ input,
679
+ context,
680
+ pendingEvents,
681
+ results,
682
+ middlewareHooks,
683
+ )
530
684
  } else {
531
685
  // User declined
532
686
  results.push({
@@ -549,45 +703,29 @@ export async function* executeToolCalls(
549
703
  }
550
704
 
551
705
  // CASE 3: Normal server tool - execute immediately
552
- const startTime = Date.now()
553
- try {
554
- const executionPromise = Promise.resolve(tool.execute(input, context))
555
- let result = yield* executeWithEventPolling(
556
- executionPromise,
557
- pendingEvents,
558
- )
559
- const duration = Date.now() - startTime
560
- yield* flushEvents()
561
-
562
- // Validate output against outputSchema if provided (for Standard Schema compliant schemas)
563
- if (
564
- tool.outputSchema &&
565
- isStandardSchema(tool.outputSchema) &&
566
- result !== undefined &&
567
- result !== null
568
- ) {
569
- result = parseWithStandardSchema(tool.outputSchema, result)
570
- }
571
-
572
- results.push({
573
- toolCallId: toolCall.id,
574
- toolName,
575
- result:
576
- typeof result === 'string' ? JSON.parse(result) : result || null,
577
- duration,
578
- })
579
- } catch (error: unknown) {
580
- const duration = Date.now() - startTime
581
- yield* flushEvents()
582
- const message = error instanceof Error ? error.message : 'Unknown error'
583
- results.push({
584
- toolCallId: toolCall.id,
706
+ if (middlewareHooks) {
707
+ const decision = await applyBeforeToolCallDecision(
708
+ toolCall,
709
+ tool,
710
+ input,
585
711
  toolName,
586
- result: { error: message },
587
- state: 'output-error',
588
- duration,
589
- })
712
+ middlewareHooks,
713
+ results,
714
+ )
715
+ if (!decision.proceed) continue
716
+ input = decision.input
590
717
  }
718
+
719
+ yield* executeServerTool(
720
+ toolCall,
721
+ tool,
722
+ toolName,
723
+ input,
724
+ context,
725
+ pendingEvents,
726
+ results,
727
+ middlewareHooks,
728
+ )
591
729
  }
592
730
 
593
731
  return { results, needsApproval, needsClientExecution }
@@ -5,9 +5,10 @@
5
5
  * This is a self-contained module with implementation, types, and JSDoc.
6
6
  */
7
7
 
8
- import { aiEventClient } from '../../event-client.js'
8
+ import { aiEventClient } from '@tanstack/ai-event-client'
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
 
@@ -5,9 +5,10 @@
5
5
  * This is a self-contained module with implementation, types, and JSDoc.
6
6
  */
7
7
 
8
- import { aiEventClient } from '../../event-client.js'
8
+ import { aiEventClient } from '@tanstack/ai-event-client'
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
 
@@ -5,9 +5,10 @@
5
5
  * This is a self-contained module with implementation, types, and JSDoc.
6
6
  */
7
7
 
8
- import { aiEventClient } from '../../event-client.js'
8
+ import { aiEventClient } from '@tanstack/ai-event-client'
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