@tanstack/ai 0.10.2 → 0.11.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 (36) hide show
  1. package/dist/esm/activities/chat/index.d.ts +4 -0
  2. package/dist/esm/activities/chat/index.js +50 -16
  3. package/dist/esm/activities/chat/index.js.map +1 -1
  4. package/dist/esm/activities/chat/middleware/compose.js +5 -4
  5. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  6. package/dist/esm/activities/chat/stream/processor.d.ts +16 -1
  7. package/dist/esm/activities/chat/stream/processor.js +120 -21
  8. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  9. package/dist/esm/activities/chat/stream/types.d.ts +1 -0
  10. package/dist/esm/activities/chat/tools/tool-calls.js +9 -4
  11. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  12. package/dist/esm/activities/generateVideo/index.js +6 -0
  13. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  14. package/dist/esm/activities/stream-generation-result.d.ts +3 -2
  15. package/dist/esm/activities/stream-generation-result.js +12 -4
  16. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  17. package/dist/esm/index.js +2 -0
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/stream-to-response.d.ts +2 -2
  20. package/dist/esm/stream-to-response.js +0 -1
  21. package/dist/esm/stream-to-response.js.map +1 -1
  22. package/dist/esm/strip-to-spec-middleware.d.ts +18 -0
  23. package/dist/esm/strip-to-spec-middleware.js +20 -0
  24. package/dist/esm/strip-to-spec-middleware.js.map +1 -0
  25. package/dist/esm/types.d.ts +229 -95
  26. package/package.json +3 -2
  27. package/src/activities/chat/index.ts +90 -25
  28. package/src/activities/chat/middleware/compose.ts +6 -4
  29. package/src/activities/chat/stream/processor.ts +152 -25
  30. package/src/activities/chat/stream/types.ts +1 -0
  31. package/src/activities/chat/tools/tool-calls.ts +14 -6
  32. package/src/activities/generateVideo/index.ts +13 -6
  33. package/src/activities/stream-generation-result.ts +19 -11
  34. package/src/stream-to-response.ts +2 -4
  35. package/src/strip-to-spec-middleware.ts +37 -0
  36. package/src/types.ts +276 -109
@@ -93,11 +93,12 @@ export class ToolCallManager {
93
93
  */
94
94
  addToolCallStartEvent(event: ToolCallStartEvent): void {
95
95
  const index = event.index ?? this.toolCallsMap.size
96
+ const name = event.toolCallName
96
97
  this.toolCallsMap.set(index, {
97
98
  id: event.toolCallId,
98
99
  type: 'function',
99
100
  function: {
100
- name: event.toolName,
101
+ name,
101
102
  arguments: '',
102
103
  },
103
104
  ...(event.providerMetadata && {
@@ -127,7 +128,10 @@ export class ToolCallManager {
127
128
  for (const [, toolCall] of this.toolCallsMap.entries()) {
128
129
  if (toolCall.id === event.toolCallId) {
129
130
  if (event.input !== undefined) {
130
- toolCall.function.arguments = JSON.stringify(event.input)
131
+ // Normalize null/non-object to {} (e.g. Anthropic empty tool_use blocks)
132
+ const normalized =
133
+ event.input && typeof event.input === 'object' ? event.input : {}
134
+ toolCall.function.arguments = JSON.stringify(normalized)
131
135
  }
132
136
  break
133
137
  }
@@ -167,11 +171,12 @@ export class ToolCallManager {
167
171
  let toolResultContent: string
168
172
  if (tool?.execute) {
169
173
  try {
170
- // Parse arguments (normalize "null" to "{}" for empty tool_use blocks)
174
+ // Parse arguments (normalize null/non-object to {} for empty tool_use blocks)
171
175
  let args: unknown
172
176
  try {
173
177
  const argsString = toolCall.function.arguments.trim() || '{}'
174
- args = JSON.parse(argsString === 'null' ? '{}' : argsString)
178
+ const parsed = JSON.parse(argsString)
179
+ args = parsed && typeof parsed === 'object' ? parsed : {}
175
180
  } catch (parseError) {
176
181
  throw new Error(
177
182
  `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`,
@@ -233,11 +238,12 @@ export class ToolCallManager {
233
238
  yield {
234
239
  type: 'TOOL_CALL_END',
235
240
  toolCallId: toolCall.id,
241
+ toolCallName: toolCall.function.name,
236
242
  toolName: toolCall.function.name,
237
243
  model: finishEvent.model,
238
244
  timestamp: Date.now(),
239
245
  result: toolResultContent,
240
- }
246
+ } as ToolCallEndEvent
241
247
 
242
248
  // Add tool result message
243
249
  toolResults.push({
@@ -543,7 +549,9 @@ export async function* executeToolCalls(
543
549
  const argsStr = toolCall.function.arguments.trim() || '{}'
544
550
  if (argsStr) {
545
551
  try {
546
- input = JSON.parse(argsStr)
552
+ const parsed = JSON.parse(argsStr)
553
+ // Normalize null/non-object to {} (e.g. Anthropic empty tool_use blocks)
554
+ input = parsed && typeof parsed === 'object' ? parsed : {}
547
555
  } catch (parseError) {
548
556
  // If parsing fails, throw error to fail fast
549
557
  throw new Error(`Failed to parse tool arguments as JSON: ${argsStr}`)
@@ -268,11 +268,14 @@ async function* runStreamingVideoGeneration<
268
268
  const pollingInterval = options.pollingInterval ?? 2000
269
269
  const maxDuration = options.maxDuration ?? 600_000
270
270
 
271
+ const threadId = createId('thread')
272
+
271
273
  yield {
272
274
  type: 'RUN_STARTED',
273
275
  runId,
276
+ threadId,
274
277
  timestamp: Date.now(),
275
- }
278
+ } as StreamChunk
276
279
 
277
280
  try {
278
281
  // Create the video generation job
@@ -289,7 +292,7 @@ async function* runStreamingVideoGeneration<
289
292
  name: 'video:job:created',
290
293
  value: { jobId: jobResult.jobId },
291
294
  timestamp: Date.now(),
292
- }
295
+ } as StreamChunk
293
296
 
294
297
  // Poll for completion
295
298
  const startTime = Date.now()
@@ -308,7 +311,7 @@ async function* runStreamingVideoGeneration<
308
311
  error: statusResult.error,
309
312
  },
310
313
  timestamp: Date.now(),
311
- }
314
+ } as StreamChunk
312
315
 
313
316
  if (statusResult.status === 'completed') {
314
317
  const urlResult = await adapter.getVideoUrl(jobResult.jobId)
@@ -323,14 +326,15 @@ async function* runStreamingVideoGeneration<
323
326
  expiresAt: urlResult.expiresAt,
324
327
  },
325
328
  timestamp: Date.now(),
326
- }
329
+ } as StreamChunk
327
330
 
328
331
  yield {
329
332
  type: 'RUN_FINISHED',
330
333
  runId,
334
+ threadId,
331
335
  finishReason: 'stop',
332
336
  timestamp: Date.now(),
333
- }
337
+ } as StreamChunk
334
338
  return
335
339
  }
336
340
 
@@ -344,12 +348,15 @@ async function* runStreamingVideoGeneration<
344
348
  yield {
345
349
  type: 'RUN_ERROR',
346
350
  runId,
351
+ threadId,
352
+ message: error.message || 'Video generation failed',
353
+ code: error.code,
347
354
  error: {
348
355
  message: error.message || 'Video generation failed',
349
356
  code: error.code,
350
357
  },
351
358
  timestamp: Date.now(),
352
- }
359
+ } as StreamChunk
353
360
  }
354
361
  }
355
362
 
@@ -4,6 +4,7 @@
4
4
  * implementations to support `stream: true`.
5
5
  */
6
6
 
7
+ import { EventType } from '@ag-ui/core'
7
8
  import type { StreamChunk } from '../types'
8
9
 
9
10
  function createId(prefix: string): string {
@@ -17,46 +18,53 @@ function createId(prefix: string): string {
17
18
  * to be sent over the same streaming transport as chat.
18
19
  *
19
20
  * @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
21
+ * @param options - Optional configuration (runId, threadId)
22
+ * @returns An AsyncIterable of StreamChunks with RUN_STARTED, CUSTOM(generation:result), and RUN_FINISHED events on success, or RUN_STARTED and RUN_ERROR on failure
22
23
  */
23
24
  export async function* streamGenerationResult<TResult>(
24
25
  generator: () => Promise<TResult>,
25
- options?: { runId?: string },
26
+ options?: { runId?: string; threadId?: string },
26
27
  ): AsyncIterable<StreamChunk> {
27
28
  const runId = options?.runId ?? createId('run')
29
+ const threadId = options?.threadId ?? createId('thread')
28
30
 
29
31
  yield {
30
- type: 'RUN_STARTED',
32
+ type: EventType.RUN_STARTED,
31
33
  runId,
34
+ threadId,
32
35
  timestamp: Date.now(),
33
- }
36
+ } as StreamChunk
34
37
 
35
38
  try {
36
39
  const result = await generator()
37
40
 
38
41
  yield {
39
- type: 'CUSTOM',
42
+ type: EventType.CUSTOM,
40
43
  name: 'generation:result',
41
44
  value: result as unknown,
42
45
  timestamp: Date.now(),
43
- }
46
+ } as StreamChunk
44
47
 
45
48
  yield {
46
- type: 'RUN_FINISHED',
49
+ type: EventType.RUN_FINISHED,
47
50
  runId,
51
+ threadId,
48
52
  finishReason: 'stop',
49
53
  timestamp: Date.now(),
50
- }
54
+ } as StreamChunk
51
55
  } catch (error: any) {
52
56
  yield {
53
- type: 'RUN_ERROR',
57
+ type: EventType.RUN_ERROR,
54
58
  runId,
59
+ threadId,
60
+ message: error.message || 'Generation failed',
61
+ code: error.code,
62
+ // Deprecated nested form for backward compatibility
55
63
  error: {
56
64
  message: error.message || 'Generation failed',
57
65
  code: error.code,
58
66
  },
59
67
  timestamp: Date.now(),
60
- }
68
+ } as StreamChunk
61
69
  }
62
70
  }
@@ -40,7 +40,7 @@ export async function streamToText(
40
40
  * This creates a ReadableStream that emits chunks in SSE format:
41
41
  * - Each chunk is prefixed with "data: "
42
42
  * - Each chunk is followed by "\n\n"
43
- * - Stream ends with "data: [DONE]\n\n"
43
+ * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
44
44
  *
45
45
  * @param stream - AsyncIterable of StreamChunks from chat()
46
46
  * @param abortController - Optional AbortController to abort when stream is cancelled
@@ -67,8 +67,6 @@ export function toServerSentEventsStream(
67
67
  )
68
68
  }
69
69
 
70
- // Send completion marker
71
- controller.enqueue(encoder.encode('data: [DONE]\n\n'))
72
70
  controller.close()
73
71
  } catch (error: any) {
74
72
  // Don't send error if aborted
@@ -109,7 +107,7 @@ export function toServerSentEventsStream(
109
107
  * This creates a Response that emits chunks in SSE format:
110
108
  * - Each chunk is prefixed with "data: "
111
109
  * - Each chunk is followed by "\n\n"
112
- * - Stream ends with "data: [DONE]\n\n"
110
+ * - Stream ends when the underlying iterable is exhausted (RUN_FINISHED is the terminal event)
113
111
  *
114
112
  * @param stream - AsyncIterable of StreamChunks from chat()
115
113
  * @param init - Optional Response initialization options (including `abortController`)
@@ -0,0 +1,37 @@
1
+ import type { ChatMiddleware } from './activities/chat/middleware/types'
2
+ import type { StreamChunk } from './types'
3
+
4
+ /**
5
+ * Strip only the deprecated nested `error` object from RUN_ERROR events.
6
+ * The flat `message`/`code` fields are the spec-compliant form.
7
+ *
8
+ * All other fields pass through unchanged. @ag-ui/core's BaseEventSchema
9
+ * uses `.passthrough()`, so extra fields (model, content, usage,
10
+ * finishReason, toolName, stepId, etc.) are allowed and won't break
11
+ * spec validation or verifyEvents.
12
+ */
13
+ export function stripToSpec(chunk: StreamChunk): StreamChunk {
14
+ // Only strip the deprecated nested error object from RUN_ERROR
15
+ if (
16
+ (chunk as StreamChunk & { type: string }).type === 'RUN_ERROR' &&
17
+ 'error' in chunk
18
+ ) {
19
+ const { error: _deprecated, ...rest } = chunk as Record<string, unknown>
20
+ return rest as StreamChunk
21
+ }
22
+ return chunk
23
+ }
24
+
25
+ /**
26
+ * Middleware that ensures events are AG-UI spec compliant.
27
+ * Currently only strips the deprecated nested `error` object from RUN_ERROR.
28
+ * All other fields pass through unchanged (passthrough allowed by spec).
29
+ */
30
+ export function stripToSpecMiddleware(): ChatMiddleware {
31
+ return {
32
+ name: 'strip-to-spec',
33
+ onChunk(_ctx, chunk) {
34
+ return stripToSpec(chunk)
35
+ },
36
+ }
37
+ }