@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.
- package/README.md +1 -1
- package/dist/esm/activities/chat/index.js +47 -6
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +3 -0
- package/dist/esm/activities/chat/stream/processor.js +10 -4
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +5 -3
- package/dist/esm/activities/chat/tools/tool-calls.js +57 -3
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +2 -2
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/types.d.ts +30 -3
- package/package.json +1 -1
- package/src/activities/chat/index.ts +75 -4
- package/src/activities/chat/stream/processor.ts +22 -4
- package/src/activities/chat/tools/tool-calls.ts +88 -5
- package/src/activities/chat/tools/tool-definition.ts +3 -0
- package/src/types.ts +31 -3
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
811
|
-
const { toolCallId, toolName, input } = chunk.
|
|
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.
|
|
827
|
-
const { toolCallId, toolName, input, approval } = chunk.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
898
|
-
|
|
925
|
+
/** Custom event value */
|
|
926
|
+
value?: unknown
|
|
899
927
|
}
|
|
900
928
|
|
|
901
929
|
/**
|