@tanstack/ai 0.5.1 → 0.6.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.
@@ -24,6 +24,7 @@ import type { AnyTextAdapter } from './adapter'
24
24
  import type {
25
25
  AgentLoopStrategy,
26
26
  ConstrainedModelMessage,
27
+ CustomEvent,
27
28
  InferSchemaType,
28
29
  ModelMessage,
29
30
  RunFinishedEvent,
@@ -601,17 +602,30 @@ class TextEngine<
601
602
 
602
603
  const { approvals, clientToolResults } = this.collectClientState()
603
604
 
604
- const executionResult = await executeToolCalls(
605
+ const generator = executeToolCalls(
605
606
  pendingToolCalls,
606
607
  this.tools,
607
608
  approvals,
608
609
  clientToolResults,
610
+ (eventName, data) => this.createCustomEventChunk(eventName, data),
609
611
  )
610
612
 
613
+ // Consume the async generator, yielding custom events and collecting the return value
614
+ const executionResult = yield* this.drainToolCallGenerator(generator)
615
+
611
616
  if (
612
617
  executionResult.needsApproval.length > 0 ||
613
618
  executionResult.needsClientExecution.length > 0
614
619
  ) {
620
+ if (executionResult.results.length > 0) {
621
+ for (const chunk of this.emitToolResults(
622
+ executionResult.results,
623
+ finishEvent,
624
+ )) {
625
+ yield chunk
626
+ }
627
+ }
628
+
615
629
  for (const chunk of this.emitApprovalRequests(
616
630
  executionResult.needsApproval,
617
631
  finishEvent,
@@ -660,17 +674,30 @@ class TextEngine<
660
674
 
661
675
  const { approvals, clientToolResults } = this.collectClientState()
662
676
 
663
- const executionResult = await executeToolCalls(
677
+ const generator = executeToolCalls(
664
678
  toolCalls,
665
679
  this.tools,
666
680
  approvals,
667
681
  clientToolResults,
682
+ (eventName, data) => this.createCustomEventChunk(eventName, data),
668
683
  )
669
684
 
685
+ // Consume the async generator, yielding custom events and collecting the return value
686
+ const executionResult = yield* this.drainToolCallGenerator(generator)
687
+
670
688
  if (
671
689
  executionResult.needsApproval.length > 0 ||
672
690
  executionResult.needsClientExecution.length > 0
673
691
  ) {
692
+ if (executionResult.results.length > 0) {
693
+ for (const chunk of this.emitToolResults(
694
+ executionResult.results,
695
+ finishEvent,
696
+ )) {
697
+ yield chunk
698
+ }
699
+ }
700
+
674
701
  for (const chunk of this.emitApprovalRequests(
675
702
  executionResult.needsApproval,
676
703
  finishEvent,
@@ -833,7 +860,7 @@ class TextEngine<
833
860
  timestamp: Date.now(),
834
861
  model: finishEvent.model,
835
862
  name: 'approval-requested',
836
- data: {
863
+ value: {
837
864
  toolCallId: approval.toolCallId,
838
865
  toolName: approval.toolName,
839
866
  input: approval.input,
@@ -870,7 +897,7 @@ class TextEngine<
870
897
  timestamp: Date.now(),
871
898
  model: finishEvent.model,
872
899
  name: 'tool-input-available',
873
- data: {
900
+ value: {
874
901
  toolCallId: clientTool.toolCallId,
875
902
  toolName: clientTool.toolName,
876
903
  input: clientTool.input,
@@ -1052,6 +1079,50 @@ class TextEngine<
1052
1079
  }
1053
1080
  }
1054
1081
 
1082
+ /**
1083
+ * Drain an executeToolCalls async generator, yielding any CustomEvent chunks
1084
+ * and returning the final ExecuteToolCallsResult.
1085
+ */
1086
+ private async *drainToolCallGenerator(
1087
+ generator: AsyncGenerator<
1088
+ CustomEvent,
1089
+ {
1090
+ results: Array<ToolResult>
1091
+ needsApproval: Array<ApprovalRequest>
1092
+ needsClientExecution: Array<ClientToolRequest>
1093
+ },
1094
+ void
1095
+ >,
1096
+ ): AsyncGenerator<
1097
+ StreamChunk,
1098
+ {
1099
+ results: Array<ToolResult>
1100
+ needsApproval: Array<ApprovalRequest>
1101
+ needsClientExecution: Array<ClientToolRequest>
1102
+ },
1103
+ void
1104
+ > {
1105
+ let next = await generator.next()
1106
+ while (!next.done) {
1107
+ yield next.value
1108
+ next = await generator.next()
1109
+ }
1110
+ return next.value
1111
+ }
1112
+
1113
+ private createCustomEventChunk(
1114
+ eventName: string,
1115
+ value: Record<string, any>,
1116
+ ): CustomEvent {
1117
+ return {
1118
+ type: 'CUSTOM',
1119
+ timestamp: Date.now(),
1120
+ model: this.params.model,
1121
+ name: eventName,
1122
+ value,
1123
+ }
1124
+ }
1125
+
1055
1126
  private createId(prefix: string): string {
1056
1127
  return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
1057
1128
  }
@@ -73,6 +73,13 @@ export interface StreamProcessorEvents {
73
73
  approvalId: string
74
74
  }) => void
75
75
 
76
+ // Custom events from server-side tools
77
+ onCustomEvent?: (
78
+ eventType: string,
79
+ data: unknown,
80
+ context: { toolCallId?: string },
81
+ ) => void
82
+
76
83
  // Granular events for UI optimization (character-by-character, state tracking)
77
84
  onTextUpdate?: (messageId: string, content: string) => void
78
85
  onToolCallStateChange?: (
@@ -807,8 +814,8 @@ export class StreamProcessor {
807
814
  chunk: Extract<StreamChunk, { type: 'CUSTOM' }>,
808
815
  ): void {
809
816
  // Handle client tool input availability - trigger client-side execution
810
- if (chunk.name === 'tool-input-available' && chunk.data) {
811
- const { toolCallId, toolName, input } = chunk.data as {
817
+ if (chunk.name === 'tool-input-available' && chunk.value) {
818
+ const { toolCallId, toolName, input } = chunk.value as {
812
819
  toolCallId: string
813
820
  toolName: string
814
821
  input: any
@@ -820,11 +827,12 @@ export class StreamProcessor {
820
827
  toolName,
821
828
  input,
822
829
  })
830
+ return
823
831
  }
824
832
 
825
833
  // Handle approval requests
826
- if (chunk.name === 'approval-requested' && chunk.data) {
827
- const { toolCallId, toolName, input, approval } = chunk.data as {
834
+ if (chunk.name === 'approval-requested' && chunk.value) {
835
+ const { toolCallId, toolName, input, approval } = chunk.value as {
828
836
  toolCallId: string
829
837
  toolName: string
830
838
  input: any
@@ -849,6 +857,16 @@ export class StreamProcessor {
849
857
  input,
850
858
  approvalId: approval.id,
851
859
  })
860
+ return
861
+ }
862
+
863
+ // Forward non-system custom events to onCustomEvent callback
864
+ if (this.events.onCustomEvent) {
865
+ const toolCallId =
866
+ chunk.value && typeof chunk.value === 'object'
867
+ ? (chunk.value as any).toolCallId
868
+ : undefined
869
+ this.events.onCustomEvent(chunk.name, chunk.value, { toolCallId })
852
870
  }
853
871
  }
854
872
 
@@ -1,5 +1,6 @@
1
1
  import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
2
2
  import type {
3
+ CustomEvent,
3
4
  ModelMessage,
4
5
  RunFinishedEvent,
5
6
  Tool,
@@ -7,6 +8,7 @@ import type {
7
8
  ToolCallArgsEvent,
8
9
  ToolCallEndEvent,
9
10
  ToolCallStartEvent,
11
+ ToolExecutionContext,
10
12
  } from '../../../types'
11
13
 
12
14
  /**
@@ -250,7 +252,46 @@ interface ExecuteToolCallsResult {
250
252
  }
251
253
 
252
254
  /**
253
- * Execute tool calls based on their configuration
255
+ * Helper that runs a tool execution promise while polling for pending custom events.
256
+ * Yields any custom events that are emitted during execution, then returns the
257
+ * execution result.
258
+ */
259
+ async function* executeWithEventPolling<T>(
260
+ executionPromise: Promise<T>,
261
+ pendingEvents: Array<CustomEvent>,
262
+ ): AsyncGenerator<CustomEvent, T, void> {
263
+ // Use an object to track mutable state across the async boundary
264
+ const state = { done: false, result: undefined as T }
265
+ const executionWithFlag = executionPromise.then((r) => {
266
+ state.done = true
267
+ state.result = r
268
+ return r
269
+ })
270
+
271
+ while (!state.done) {
272
+ // Wait for either the execution to complete or a short timeout
273
+ await Promise.race([
274
+ executionWithFlag,
275
+ new Promise((resolve) => setTimeout(resolve, 10)),
276
+ ])
277
+
278
+ // Flush any pending events
279
+ while (pendingEvents.length > 0) {
280
+ yield pendingEvents.shift()!
281
+ }
282
+ }
283
+
284
+ // Final flush in case events were emitted right at completion
285
+ while (pendingEvents.length > 0) {
286
+ yield pendingEvents.shift()!
287
+ }
288
+
289
+ return state.result
290
+ }
291
+
292
+ /**
293
+ * Execute tool calls based on their configuration.
294
+ * Yields CustomEvent chunks during tool execution for real-time progress updates.
254
295
  *
255
296
  * Handles three cases:
256
297
  * 1. Client tools (no execute) - request client to execute
@@ -261,13 +302,18 @@ interface ExecuteToolCallsResult {
261
302
  * @param tools - Available tools with their configurations
262
303
  * @param approvals - Map of approval decisions (approval.id -> approved boolean)
263
304
  * @param clientResults - Map of client-side execution results (toolCallId -> result)
305
+ * @param createCustomEventChunk - Factory to create CustomEvent chunks (optional)
264
306
  */
265
- export async function executeToolCalls(
307
+ export async function* executeToolCalls(
266
308
  toolCalls: Array<ToolCall>,
267
309
  tools: ReadonlyArray<Tool>,
268
310
  approvals: Map<string, boolean> = new Map(),
269
311
  clientResults: Map<string, any> = new Map(),
270
- ): Promise<ExecuteToolCallsResult> {
312
+ createCustomEventChunk?: (
313
+ eventName: string,
314
+ value: Record<string, any>,
315
+ ) => CustomEvent,
316
+ ): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
271
317
  const results: Array<ToolResult> = []
272
318
  const needsApproval: Array<ApprovalRequest> = []
273
319
  const needsClientExecution: Array<ClientToolRequest> = []
@@ -326,6 +372,29 @@ export async function executeToolCalls(
326
372
  }
327
373
  }
328
374
 
375
+ // Create a ToolExecutionContext for this tool call with event emission
376
+ const pendingEvents: Array<CustomEvent> = []
377
+ const context: ToolExecutionContext = {
378
+ toolCallId: toolCall.id,
379
+ emitCustomEvent: (eventName: string, value: Record<string, any>) => {
380
+ if (createCustomEventChunk) {
381
+ pendingEvents.push(
382
+ createCustomEventChunk(eventName, {
383
+ ...value,
384
+ toolCallId: toolCall.id,
385
+ }),
386
+ )
387
+ }
388
+ },
389
+ }
390
+
391
+ // Helper to flush any pending events
392
+ function* flushEvents(): Generator<CustomEvent> {
393
+ while (pendingEvents.length > 0) {
394
+ yield pendingEvents.shift()!
395
+ }
396
+ }
397
+
329
398
  // CASE 1: Client-side tool (no execute function)
330
399
  if (!tool.execute) {
331
400
  // Check if tool needs approval
@@ -402,8 +471,15 @@ export async function executeToolCalls(
402
471
  // Execute after approval
403
472
  const startTime = Date.now()
404
473
  try {
405
- let result = await tool.execute(input)
474
+ const executionPromise = Promise.resolve(
475
+ tool.execute(input, context),
476
+ )
477
+ let result = yield* executeWithEventPolling(
478
+ executionPromise,
479
+ pendingEvents,
480
+ )
406
481
  const duration = Date.now() - startTime
482
+ yield* flushEvents()
407
483
 
408
484
  // Validate output against outputSchema if provided (for Standard Schema compliant schemas)
409
485
  if (
@@ -426,6 +502,7 @@ export async function executeToolCalls(
426
502
  })
427
503
  } catch (error: unknown) {
428
504
  const duration = Date.now() - startTime
505
+ yield* flushEvents()
429
506
  const message =
430
507
  error instanceof Error ? error.message : 'Unknown error'
431
508
  results.push({
@@ -460,8 +537,13 @@ export async function executeToolCalls(
460
537
  // CASE 3: Normal server tool - execute immediately
461
538
  const startTime = Date.now()
462
539
  try {
463
- let result = await tool.execute(input)
540
+ const executionPromise = Promise.resolve(tool.execute(input, context))
541
+ let result = yield* executeWithEventPolling(
542
+ executionPromise,
543
+ pendingEvents,
544
+ )
464
545
  const duration = Date.now() - startTime
546
+ yield* flushEvents()
465
547
 
466
548
  // Validate output against outputSchema if provided (for Standard Schema compliant schemas)
467
549
  if (
@@ -482,6 +564,7 @@ export async function executeToolCalls(
482
564
  })
483
565
  } catch (error: unknown) {
484
566
  const duration = Date.now() - startTime
567
+ yield* flushEvents()
485
568
  const message = error instanceof Error ? error.message : 'Unknown error'
486
569
  results.push({
487
570
  toolCallId: toolCall.id,
@@ -4,6 +4,7 @@ import type {
4
4
  JSONSchema,
5
5
  SchemaInput,
6
6
  Tool,
7
+ ToolExecutionContext,
7
8
  } from '../../../types'
8
9
 
9
10
  /**
@@ -112,6 +113,7 @@ export interface ToolDefinition<
112
113
  server: (
113
114
  execute: (
114
115
  args: InferSchemaType<TInput>,
116
+ context?: ToolExecutionContext,
115
117
  ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
116
118
  ) => ServerTool<TInput, TOutput, TName>
117
119
 
@@ -193,6 +195,7 @@ export function toolDefinition<
193
195
  server(
194
196
  execute: (
195
197
  args: InferSchemaType<TInput>,
198
+ context?: ToolExecutionContext,
196
199
  ) => Promise<InferSchemaType<TOutput>> | InferSchemaType<TOutput>,
197
200
  ): ServerTool<TInput, TOutput, TName> {
198
201
  return {
package/src/types.ts CHANGED
@@ -344,6 +344,34 @@ export type ConstrainedModelMessage<
344
344
  content: ConstrainedContent<TInputModalitiesTypes>
345
345
  }
346
346
 
347
+ /**
348
+ * Context passed to tool execute functions, providing capabilities like
349
+ * emitting custom events during execution.
350
+ */
351
+ export interface ToolExecutionContext {
352
+ /** The ID of the tool call being executed */
353
+ toolCallId?: string
354
+ /**
355
+ * Emit a custom event during tool execution.
356
+ * Events are streamed to the client in real-time as AG-UI CUSTOM events.
357
+ *
358
+ * @param eventName - Name of the custom event
359
+ * @param value - Event payload value
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const tool = toolDefinition({ ... }).server(async (args, context) => {
364
+ * context?.emitCustomEvent('progress', { step: 1, total: 3 })
365
+ * // ... do work ...
366
+ * context?.emitCustomEvent('progress', { step: 2, total: 3 })
367
+ * // ... do more work ...
368
+ * return result
369
+ * })
370
+ * ```
371
+ */
372
+ emitCustomEvent: (eventName: string, value: Record<string, any>) => void
373
+ }
374
+
347
375
  /**
348
376
  * Tool/Function definition for function calling.
349
377
  *
@@ -460,7 +488,7 @@ export interface Tool<
460
488
  * return weather; // Can return object or string
461
489
  * }
462
490
  */
463
- execute?: (args: any) => Promise<any> | any
491
+ execute?: (args: any, context?: ToolExecutionContext) => Promise<any> | any
464
492
 
465
493
  /** If true, tool execution requires user approval before running. Works with both server and client tools. */
466
494
  needsApproval?: boolean
@@ -894,8 +922,8 @@ export interface CustomEvent extends BaseAGUIEvent {
894
922
  type: 'CUSTOM'
895
923
  /** Custom event name */
896
924
  name: string
897
- /** Custom event data */
898
- data?: unknown
925
+ /** Custom event value */
926
+ value?: unknown
899
927
  }
900
928
 
901
929
  /**