@tanstack/ai 0.2.2 → 0.3.1

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 (44) hide show
  1. package/README.md +1 -0
  2. package/dist/esm/activities/chat/index.js +264 -186
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/messages.js +22 -0
  5. package/dist/esm/activities/chat/messages.js.map +1 -1
  6. package/dist/esm/activities/chat/stream/index.d.ts +1 -1
  7. package/dist/esm/activities/chat/stream/processor.d.ts +22 -43
  8. package/dist/esm/activities/chat/stream/processor.js +93 -157
  9. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  10. package/dist/esm/activities/chat/tools/tool-calls.d.ts +22 -21
  11. package/dist/esm/activities/chat/tools/tool-calls.js +40 -27
  12. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  13. package/dist/esm/activities/generateImage/index.js +41 -1
  14. package/dist/esm/activities/generateImage/index.js.map +1 -1
  15. package/dist/esm/activities/generateSpeech/index.js +33 -1
  16. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  17. package/dist/esm/activities/generateTranscription/index.js +29 -1
  18. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  19. package/dist/esm/activities/generateVideo/index.js +50 -0
  20. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  21. package/dist/esm/activities/summarize/index.js +9 -8
  22. package/dist/esm/activities/summarize/index.js.map +1 -1
  23. package/dist/esm/event-client.d.ts +378 -254
  24. package/dist/esm/event-client.js.map +1 -1
  25. package/dist/esm/index.d.ts +2 -2
  26. package/dist/esm/stream-to-response.d.ts +1 -1
  27. package/dist/esm/stream-to-response.js +5 -3
  28. package/dist/esm/stream-to-response.js.map +1 -1
  29. package/dist/esm/types.d.ts +162 -46
  30. package/package.json +1 -1
  31. package/src/activities/chat/index.ts +362 -216
  32. package/src/activities/chat/messages.ts +40 -1
  33. package/src/activities/chat/stream/index.ts +1 -5
  34. package/src/activities/chat/stream/processor.ts +142 -270
  35. package/src/activities/chat/tools/tool-calls.ts +55 -49
  36. package/src/activities/generateImage/index.ts +47 -2
  37. package/src/activities/generateSpeech/index.ts +37 -1
  38. package/src/activities/generateTranscription/index.ts +33 -1
  39. package/src/activities/generateVideo/index.ts +54 -0
  40. package/src/activities/summarize/index.ts +10 -9
  41. package/src/event-client.ts +460 -256
  42. package/src/index.ts +2 -3
  43. package/src/stream-to-response.ts +8 -6
  44. package/src/types.ts +209 -65
@@ -1,20 +1,22 @@
1
1
  import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
2
2
  import type {
3
- DoneStreamChunk,
4
3
  ModelMessage,
4
+ RunFinishedEvent,
5
5
  Tool,
6
6
  ToolCall,
7
- ToolResultStreamChunk,
7
+ ToolCallArgsEvent,
8
+ ToolCallEndEvent,
9
+ ToolCallStartEvent,
8
10
  } from '../../../types'
9
11
 
10
12
  /**
11
13
  * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.
12
14
  *
13
15
  * Responsibilities:
14
- * - Accumulates streaming tool call chunks (ID, name, arguments)
16
+ * - Accumulates streaming tool call events (ID, name, arguments)
15
17
  * - Validates tool calls (filters out incomplete ones)
16
18
  * - Executes tool `execute` functions with parsed arguments
17
- * - Emits `tool_result` chunks for client visibility
19
+ * - Emits `TOOL_CALL_END` events for client visibility
18
20
  * - Returns tool result messages for conversation history
19
21
  *
20
22
  * This class is used internally by the AI.chat() method to handle the automatic
@@ -26,14 +28,16 @@ import type {
26
28
  *
27
29
  * // During streaming, accumulate tool calls
28
30
  * for await (const chunk of stream) {
29
- * if (chunk.type === "tool_call") {
30
- * manager.addToolCallChunk(chunk);
31
+ * if (chunk.type === 'TOOL_CALL_START') {
32
+ * manager.addToolCallStartEvent(chunk);
33
+ * } else if (chunk.type === 'TOOL_CALL_ARGS') {
34
+ * manager.addToolCallArgsEvent(chunk);
31
35
  * }
32
36
  * }
33
37
  *
34
38
  * // After stream completes, execute tools
35
39
  * if (manager.hasToolCalls()) {
36
- * const toolResults = yield* manager.executeTools(doneChunk);
40
+ * const toolResults = yield* manager.executeTools(finishEvent);
37
41
  * messages = [...messages, ...toolResults];
38
42
  * manager.clear();
39
43
  * }
@@ -48,43 +52,44 @@ export class ToolCallManager {
48
52
  }
49
53
 
50
54
  /**
51
- * Add a tool call chunk to the accumulator
52
- * Handles streaming tool calls by accumulating arguments
55
+ * Add a TOOL_CALL_START event to begin tracking a tool call (AG-UI)
53
56
  */
54
- addToolCallChunk(chunk: {
55
- toolCall: {
56
- id: string
57
- type: 'function'
57
+ addToolCallStartEvent(event: ToolCallStartEvent): void {
58
+ const index = event.index ?? this.toolCallsMap.size
59
+ this.toolCallsMap.set(index, {
60
+ id: event.toolCallId,
61
+ type: 'function',
58
62
  function: {
59
- name: string
60
- arguments: string
63
+ name: event.toolName,
64
+ arguments: '',
65
+ },
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Add a TOOL_CALL_ARGS event to accumulate arguments (AG-UI)
71
+ */
72
+ addToolCallArgsEvent(event: ToolCallArgsEvent): void {
73
+ // Find the tool call by ID
74
+ for (const [, toolCall] of this.toolCallsMap.entries()) {
75
+ if (toolCall.id === event.toolCallId) {
76
+ toolCall.function.arguments += event.delta
77
+ break
61
78
  }
62
79
  }
63
- index: number
64
- }): void {
65
- const index = chunk.index
66
- const existing = this.toolCallsMap.get(index)
67
-
68
- if (!existing) {
69
- // Only create entry if we have a tool call ID and name
70
- if (chunk.toolCall.id && chunk.toolCall.function.name) {
71
- this.toolCallsMap.set(index, {
72
- id: chunk.toolCall.id,
73
- type: 'function',
74
- function: {
75
- name: chunk.toolCall.function.name,
76
- arguments: chunk.toolCall.function.arguments || '',
77
- },
78
- })
79
- }
80
- } else {
81
- // Update name if it wasn't set before
82
- if (chunk.toolCall.function.name && !existing.function.name) {
83
- existing.function.name = chunk.toolCall.function.name
84
- }
85
- // Accumulate arguments for streaming tool calls
86
- if (chunk.toolCall.function.arguments) {
87
- existing.function.arguments += chunk.toolCall.function.arguments
80
+ }
81
+
82
+ /**
83
+ * Complete a tool call with its final input
84
+ * Called when TOOL_CALL_END is received
85
+ */
86
+ completeToolCall(event: ToolCallEndEvent): void {
87
+ for (const [, toolCall] of this.toolCallsMap.entries()) {
88
+ if (toolCall.id === event.toolCallId) {
89
+ if (event.input !== undefined) {
90
+ toolCall.function.arguments = JSON.stringify(event.input)
91
+ }
92
+ break
88
93
  }
89
94
  }
90
95
  }
@@ -107,11 +112,12 @@ export class ToolCallManager {
107
112
 
108
113
  /**
109
114
  * Execute all tool calls and return tool result messages
110
- * Also yields tool_result chunks for streaming
115
+ * Yields TOOL_CALL_END events for streaming
116
+ * @param finishEvent - RUN_FINISHED event from the stream
111
117
  */
112
118
  async *executeTools(
113
- doneChunk: DoneStreamChunk,
114
- ): AsyncGenerator<ToolResultStreamChunk, Array<ModelMessage>, void> {
119
+ finishEvent: RunFinishedEvent,
120
+ ): AsyncGenerator<ToolCallEndEvent, Array<ModelMessage>, void> {
115
121
  const toolCallsArray = this.getToolCalls()
116
122
  const toolResults: Array<ModelMessage> = []
117
123
 
@@ -182,14 +188,14 @@ export class ToolCallManager {
182
188
  toolResultContent = `Tool ${toolCall.function.name} does not have an execute function`
183
189
  }
184
190
 
185
- // Emit tool_result chunk so callers can track tool execution
191
+ // Emit TOOL_CALL_END event
186
192
  yield {
187
- type: 'tool_result',
188
- id: doneChunk.id,
189
- model: doneChunk.model,
190
- timestamp: Date.now(),
193
+ type: 'TOOL_CALL_END',
191
194
  toolCallId: toolCall.id,
192
- content: toolResultContent,
195
+ toolName: toolCall.function.name,
196
+ model: finishEvent.model,
197
+ timestamp: Date.now(),
198
+ result: toolResultContent,
193
199
  }
194
200
 
195
201
  // Add tool result message
@@ -5,6 +5,7 @@
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
9
  import type { ImageAdapter } from './adapter'
9
10
  import type { ImageGenerationResult } from '../../types'
10
11
 
@@ -82,6 +83,10 @@ export interface ImageActivityOptions<
82
83
  /** Result type for the image activity */
83
84
  export type ImageActivityResult = Promise<ImageGenerationResult>
84
85
 
86
+ function createId(prefix: string): string {
87
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
88
+ }
89
+
85
90
  // ===========================
86
91
  // Activity Implementation
87
92
  // ===========================
@@ -136,8 +141,48 @@ export async function generateImage<
136
141
  >(options: ImageActivityOptions<TAdapter>): ImageActivityResult {
137
142
  const { adapter, ...rest } = options
138
143
  const model = adapter.model
139
-
140
- return adapter.generateImages({ ...rest, model })
144
+ const requestId = createId('image')
145
+ const startTime = Date.now()
146
+
147
+ aiEventClient.emit('image:request:started', {
148
+ requestId,
149
+ provider: adapter.name,
150
+ model,
151
+ prompt: rest.prompt,
152
+ numberOfImages: rest.numberOfImages,
153
+ size: rest.size as string | undefined,
154
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
155
+ timestamp: startTime,
156
+ })
157
+
158
+ return adapter.generateImages({ ...rest, model }).then((result) => {
159
+ const duration = Date.now() - startTime
160
+
161
+ aiEventClient.emit('image:request:completed', {
162
+ requestId,
163
+ provider: adapter.name,
164
+ model,
165
+ images: result.images.map((image) => ({
166
+ url: image.url,
167
+ b64Json: image.b64Json,
168
+ })),
169
+ duration,
170
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
171
+ timestamp: Date.now(),
172
+ })
173
+
174
+ if (result.usage) {
175
+ aiEventClient.emit('image:usage', {
176
+ requestId,
177
+ model,
178
+ usage: result.usage,
179
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
180
+ timestamp: Date.now(),
181
+ })
182
+ }
183
+
184
+ return result
185
+ })
141
186
  }
142
187
 
143
188
  // ===========================
@@ -5,6 +5,7 @@
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
9
  import type { TTSAdapter } from './adapter'
9
10
  import type { TTSResult } from '../../types'
10
11
 
@@ -61,6 +62,10 @@ export interface TTSActivityOptions<
61
62
  /** Result type for the TTS activity */
62
63
  export type TTSActivityResult = Promise<TTSResult>
63
64
 
65
+ function createId(prefix: string): string {
66
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
67
+ }
68
+
64
69
  // ===========================
65
70
  // Activity Implementation
66
71
  // ===========================
@@ -100,8 +105,39 @@ export async function generateSpeech<
100
105
  >(options: TTSActivityOptions<TAdapter>): TTSActivityResult {
101
106
  const { adapter, ...rest } = options
102
107
  const model = adapter.model
108
+ const requestId = createId('speech')
109
+ const startTime = Date.now()
110
+
111
+ aiEventClient.emit('speech:request:started', {
112
+ requestId,
113
+ provider: adapter.name,
114
+ model,
115
+ text: rest.text,
116
+ voice: rest.voice,
117
+ format: rest.format,
118
+ speed: rest.speed,
119
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
120
+ timestamp: startTime,
121
+ })
122
+
123
+ return adapter.generateSpeech({ ...rest, model }).then((result) => {
124
+ const duration = Date.now() - startTime
125
+
126
+ aiEventClient.emit('speech:request:completed', {
127
+ requestId,
128
+ provider: adapter.name,
129
+ model,
130
+ audio: result.audio,
131
+ format: result.format,
132
+ audioDuration: result.duration,
133
+ contentType: result.contentType,
134
+ duration,
135
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
136
+ timestamp: Date.now(),
137
+ })
103
138
 
104
- return adapter.generateSpeech({ ...rest, model })
139
+ return result
140
+ })
105
141
  }
106
142
 
107
143
  // ===========================
@@ -5,6 +5,7 @@
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
9
  import type { TranscriptionAdapter } from './adapter'
9
10
  import type { TranscriptionResult } from '../../types'
10
11
 
@@ -61,6 +62,10 @@ export interface TranscriptionActivityOptions<
61
62
  /** Result type for the transcription activity */
62
63
  export type TranscriptionActivityResult = Promise<TranscriptionResult>
63
64
 
65
+ function createId(prefix: string): string {
66
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
67
+ }
68
+
64
69
  // ===========================
65
70
  // Activity Implementation
66
71
  // ===========================
@@ -104,8 +109,35 @@ export async function generateTranscription<
104
109
  ): TranscriptionActivityResult {
105
110
  const { adapter, ...rest } = options
106
111
  const model = adapter.model
112
+ const requestId = createId('transcription')
113
+ const startTime = Date.now()
114
+
115
+ aiEventClient.emit('transcription:request:started', {
116
+ requestId,
117
+ provider: adapter.name,
118
+ model,
119
+ language: rest.language,
120
+ prompt: rest.prompt,
121
+ responseFormat: rest.responseFormat,
122
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
123
+ timestamp: startTime,
124
+ })
125
+
126
+ const result = await adapter.transcribe({ ...rest, model })
127
+ const duration = Date.now() - startTime
128
+
129
+ aiEventClient.emit('transcription:request:completed', {
130
+ requestId,
131
+ provider: adapter.name,
132
+ model,
133
+ text: result.text,
134
+ language: result.language,
135
+ duration,
136
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
137
+ timestamp: Date.now(),
138
+ })
107
139
 
108
- return adapter.transcribe({ ...rest, model })
140
+ return result
109
141
  }
110
142
 
111
143
  // ===========================
@@ -7,6 +7,7 @@
7
7
  * @experimental Video generation is an experimental feature and may change.
8
8
  */
9
9
 
10
+ import { aiEventClient } from '../../event-client.js'
10
11
  import type { VideoAdapter } from './adapter'
11
12
  import type {
12
13
  VideoJobResult,
@@ -35,6 +36,10 @@ export type VideoProviderOptions<TAdapter> =
35
36
 
36
37
  // ===========================
37
38
  // Activity Options Types
39
+
40
+ function createId(prefix: string): string {
41
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
42
+ }
38
43
  // ===========================
39
44
 
40
45
  /**
@@ -207,6 +212,17 @@ export async function getVideoJobStatus<
207
212
  error?: string
208
213
  }> {
209
214
  const { adapter, jobId } = options
215
+ const requestId = createId('video-status')
216
+ const startTime = Date.now()
217
+
218
+ aiEventClient.emit('video:request:started', {
219
+ requestId,
220
+ provider: adapter.name,
221
+ model: adapter.model,
222
+ requestType: 'status',
223
+ jobId,
224
+ timestamp: startTime,
225
+ })
210
226
 
211
227
  // Get status first
212
228
  const statusResult = await adapter.getVideoStatus(jobId)
@@ -215,12 +231,37 @@ export async function getVideoJobStatus<
215
231
  if (statusResult.status === 'completed') {
216
232
  try {
217
233
  const urlResult = await adapter.getVideoUrl(jobId)
234
+ aiEventClient.emit('video:request:completed', {
235
+ requestId,
236
+ provider: adapter.name,
237
+ model: adapter.model,
238
+ requestType: 'status',
239
+ jobId,
240
+ status: statusResult.status,
241
+ progress: statusResult.progress,
242
+ url: urlResult.url,
243
+ duration: Date.now() - startTime,
244
+ timestamp: Date.now(),
245
+ })
218
246
  return {
219
247
  status: statusResult.status,
220
248
  progress: statusResult.progress,
221
249
  url: urlResult.url,
222
250
  }
223
251
  } catch (error) {
252
+ aiEventClient.emit('video:request:completed', {
253
+ requestId,
254
+ provider: adapter.name,
255
+ model: adapter.model,
256
+ requestType: 'status',
257
+ jobId,
258
+ status: statusResult.status,
259
+ progress: statusResult.progress,
260
+ error:
261
+ error instanceof Error ? error.message : 'Failed to get video URL',
262
+ duration: Date.now() - startTime,
263
+ timestamp: Date.now(),
264
+ })
224
265
  // If URL fetch fails, still return status
225
266
  return {
226
267
  status: statusResult.status,
@@ -231,6 +272,19 @@ export async function getVideoJobStatus<
231
272
  }
232
273
  }
233
274
 
275
+ aiEventClient.emit('video:request:completed', {
276
+ requestId,
277
+ provider: adapter.name,
278
+ model: adapter.model,
279
+ requestType: 'status',
280
+ jobId,
281
+ status: statusResult.status,
282
+ progress: statusResult.progress,
283
+ error: statusResult.error,
284
+ duration: Date.now() - startTime,
285
+ timestamp: Date.now(),
286
+ })
287
+
234
288
  // Return status for non-completed jobs
235
289
  return {
236
290
  status: statusResult.status,
@@ -180,8 +180,9 @@ async function runSummarize(
180
180
  const inputLength = text.length
181
181
  const startTime = Date.now()
182
182
 
183
- aiEventClient.emit('summarize:started', {
183
+ aiEventClient.emit('summarize:request:started', {
184
184
  requestId,
185
+ provider: adapter.name,
185
186
  model,
186
187
  inputLength,
187
188
  timestamp: startTime,
@@ -200,8 +201,9 @@ async function runSummarize(
200
201
  const duration = Date.now() - startTime
201
202
  const outputLength = result.summary.length
202
203
 
203
- aiEventClient.emit('summarize:completed', {
204
+ aiEventClient.emit('summarize:request:completed', {
204
205
  requestId,
206
+ provider: adapter.name,
205
207
  model,
206
208
  inputLength,
207
209
  outputLength,
@@ -240,21 +242,20 @@ async function* runStreamingSummarize(
240
242
  // Fall back to non-streaming and yield as a single chunk
241
243
  const result = await adapter.summarize(summarizeOptions)
242
244
 
243
- // Yield content chunk with the summary
245
+ // Yield TEXT_MESSAGE_CONTENT event with the summary
244
246
  yield {
245
- type: 'content',
246
- id: result.id,
247
+ type: 'TEXT_MESSAGE_CONTENT',
248
+ messageId: result.id,
247
249
  model: result.model,
248
250
  timestamp: Date.now(),
249
251
  delta: result.summary,
250
252
  content: result.summary,
251
- role: 'assistant',
252
253
  }
253
254
 
254
- // Yield done chunk
255
+ // Yield RUN_FINISHED event
255
256
  yield {
256
- type: 'done',
257
- id: result.id,
257
+ type: 'RUN_FINISHED',
258
+ runId: result.id,
258
259
  model: result.model,
259
260
  timestamp: Date.now(),
260
261
  finishReason: 'stop',