@tanstack/ai 0.0.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/esm/base-adapter.d.ts +35 -0
  4. package/dist/esm/base-adapter.js +12 -0
  5. package/dist/esm/base-adapter.js.map +1 -0
  6. package/dist/esm/core/chat-common-options.d.ts +52 -0
  7. package/dist/esm/core/chat.d.ts +30 -0
  8. package/dist/esm/core/chat.js +533 -0
  9. package/dist/esm/core/chat.js.map +1 -0
  10. package/dist/esm/core/embedding.d.ts +8 -0
  11. package/dist/esm/core/embedding.js +33 -0
  12. package/dist/esm/core/embedding.js.map +1 -0
  13. package/dist/esm/core/summarize.d.ts +9 -0
  14. package/dist/esm/core/summarize.js +36 -0
  15. package/dist/esm/core/summarize.js.map +1 -0
  16. package/dist/esm/event-client.d.ts +311 -0
  17. package/dist/esm/event-client.js +62 -0
  18. package/dist/esm/event-client.js.map +1 -0
  19. package/dist/esm/index.d.ts +16 -0
  20. package/dist/esm/index.js +50 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/message-converters.d.ts +52 -0
  23. package/dist/esm/message-converters.js +162 -0
  24. package/dist/esm/message-converters.js.map +1 -0
  25. package/dist/esm/stream/index.d.ts +11 -0
  26. package/dist/esm/stream/json-parser.d.ts +38 -0
  27. package/dist/esm/stream/json-parser.js +28 -0
  28. package/dist/esm/stream/json-parser.js.map +1 -0
  29. package/dist/esm/stream/message-updaters.d.ts +44 -0
  30. package/dist/esm/stream/message-updaters.js +141 -0
  31. package/dist/esm/stream/message-updaters.js.map +1 -0
  32. package/dist/esm/stream/processor.d.ts +242 -0
  33. package/dist/esm/stream/processor.js +693 -0
  34. package/dist/esm/stream/processor.js.map +1 -0
  35. package/dist/esm/stream/strategies.d.ts +43 -0
  36. package/dist/esm/stream/strategies.js +54 -0
  37. package/dist/esm/stream/strategies.js.map +1 -0
  38. package/dist/esm/stream/types.d.ts +71 -0
  39. package/dist/esm/tools/tool-calls.d.ts +112 -0
  40. package/dist/esm/tools/tool-calls.js +302 -0
  41. package/dist/esm/tools/tool-calls.js.map +1 -0
  42. package/dist/esm/tools/tool-definition.d.ts +125 -0
  43. package/dist/esm/tools/tool-definition.js +25 -0
  44. package/dist/esm/tools/tool-definition.js.map +1 -0
  45. package/dist/esm/tools/zod-converter.d.ts +30 -0
  46. package/dist/esm/tools/zod-converter.js +36 -0
  47. package/dist/esm/tools/zod-converter.js.map +1 -0
  48. package/dist/esm/types.d.ts +619 -0
  49. package/dist/esm/utilities/agent-loop-strategies.d.ts +59 -0
  50. package/dist/esm/utilities/agent-loop-strategies.js +23 -0
  51. package/dist/esm/utilities/agent-loop-strategies.js.map +1 -0
  52. package/dist/esm/utilities/chat-options.d.ts +6 -0
  53. package/dist/esm/utilities/chat-options.js +7 -0
  54. package/dist/esm/utilities/chat-options.js.map +1 -0
  55. package/dist/esm/utilities/messages.d.ts +30 -0
  56. package/dist/esm/utilities/messages.js +7 -0
  57. package/dist/esm/utilities/messages.js.map +1 -0
  58. package/dist/esm/utilities/stream-to-response.d.ts +48 -0
  59. package/dist/esm/utilities/stream-to-response.js +62 -0
  60. package/dist/esm/utilities/stream-to-response.js.map +1 -0
  61. package/package.json +65 -0
  62. package/src/base-adapter.ts +85 -0
  63. package/src/core/chat-common-options.ts +55 -0
  64. package/src/core/chat.ts +771 -0
  65. package/src/core/embedding.ts +54 -0
  66. package/src/core/summarize.ts +56 -0
  67. package/src/event-client.ts +389 -0
  68. package/src/index.ts +68 -0
  69. package/src/message-converters.ts +285 -0
  70. package/src/stream/index.ts +41 -0
  71. package/src/stream/json-parser.ts +58 -0
  72. package/src/stream/message-updaters.ts +275 -0
  73. package/src/stream/processor.ts +1092 -0
  74. package/src/stream/strategies.ts +78 -0
  75. package/src/stream/types.ts +94 -0
  76. package/src/tools/tool-calls.ts +471 -0
  77. package/src/tools/tool-definition.ts +206 -0
  78. package/src/tools/zod-converter.ts +85 -0
  79. package/src/types.ts +872 -0
  80. package/src/utilities/agent-loop-strategies.ts +85 -0
  81. package/src/utilities/chat-options.ts +35 -0
  82. package/src/utilities/messages.ts +63 -0
  83. package/src/utilities/stream-to-response.ts +116 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Built-in Chunk Strategies
3
+ *
4
+ * Strategies for controlling when text updates are emitted to the UI
5
+ */
6
+
7
+ import type { ChunkStrategy } from './types'
8
+
9
+ /**
10
+ * Immediate Strategy - emit on every chunk (default behavior)
11
+ */
12
+ export class ImmediateStrategy implements ChunkStrategy {
13
+ shouldEmit(_chunk: string, _accumulated: string): boolean {
14
+ return true
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Punctuation Strategy - emit when chunk contains punctuation
20
+ * Useful for natural text flow in UI
21
+ */
22
+ export class PunctuationStrategy implements ChunkStrategy {
23
+ private punctuation = /[.,!?;:\n]/
24
+
25
+ shouldEmit(chunk: string, _accumulated: string): boolean {
26
+ return this.punctuation.test(chunk)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Batch Strategy - emit every N chunks
32
+ * Useful for reducing UI update frequency
33
+ */
34
+ export class BatchStrategy implements ChunkStrategy {
35
+ private chunkCount = 0
36
+
37
+ constructor(private batchSize: number = 5) {}
38
+
39
+ shouldEmit(_chunk: string, _accumulated: string): boolean {
40
+ this.chunkCount++
41
+ if (this.chunkCount >= this.batchSize) {
42
+ this.chunkCount = 0
43
+ return true
44
+ }
45
+ return false
46
+ }
47
+
48
+ reset(): void {
49
+ this.chunkCount = 0
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Word Boundary Strategy - emit at word boundaries
55
+ * Prevents cutting words in half
56
+ */
57
+ export class WordBoundaryStrategy implements ChunkStrategy {
58
+ shouldEmit(chunk: string, _accumulated: string): boolean {
59
+ // Emit if chunk ends with whitespace
60
+ return /\s$/.test(chunk)
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Composite Strategy - combine multiple strategies (OR logic)
66
+ * Emits if ANY strategy says to emit
67
+ */
68
+ export class CompositeStrategy implements ChunkStrategy {
69
+ constructor(private strategies: Array<ChunkStrategy>) {}
70
+
71
+ shouldEmit(chunk: string, accumulated: string): boolean {
72
+ return this.strategies.some((s) => s.shouldEmit(chunk, accumulated))
73
+ }
74
+
75
+ reset(): void {
76
+ this.strategies.forEach((s) => s.reset?.())
77
+ }
78
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Stream Processor Types
3
+ *
4
+ * Unified types for stream processing used by both server and client.
5
+ * The canonical chunk format is StreamChunk from @tanstack/ai types.
6
+ */
7
+
8
+ import type { StreamChunk, ToolCall } from '../types'
9
+
10
+ /**
11
+ * Tool call states - track the lifecycle of a tool call
12
+ */
13
+ export type ToolCallState =
14
+ | 'awaiting-input' // Received start but no arguments yet
15
+ | 'input-streaming' // Partial arguments received
16
+ | 'input-complete' // All arguments received
17
+ | 'approval-requested' // Waiting for user approval
18
+ | 'approval-responded' // User has approved/denied
19
+
20
+ /**
21
+ * Tool result states - track the lifecycle of a tool result
22
+ */
23
+ export type ToolResultState =
24
+ | 'streaming' // Placeholder for future streamed output
25
+ | 'complete' // Result is complete
26
+ | 'error' // Error occurred
27
+
28
+ /**
29
+ * Internal state for a tool call being tracked
30
+ */
31
+ export interface InternalToolCallState {
32
+ id: string
33
+ name: string
34
+ arguments: string
35
+ state: ToolCallState
36
+ parsedArguments?: any
37
+ index: number
38
+ }
39
+
40
+ /**
41
+ * Strategy for determining when to emit text updates
42
+ */
43
+ export interface ChunkStrategy {
44
+ /**
45
+ * Called for each text chunk received
46
+ * @param chunk - The new chunk of text (delta)
47
+ * @param accumulated - All text accumulated so far
48
+ * @returns true if an update should be emitted now
49
+ */
50
+ shouldEmit: (chunk: string, accumulated: string) => boolean
51
+
52
+ /**
53
+ * Optional: Reset strategy state (called when streaming starts)
54
+ */
55
+ reset?: () => void
56
+ }
57
+
58
+ /**
59
+ * Result from processing a stream
60
+ */
61
+ export interface ProcessorResult {
62
+ content: string
63
+ thinking?: string
64
+ toolCalls?: Array<ToolCall>
65
+ finishReason?: string | null
66
+ }
67
+
68
+ /**
69
+ * Current state of the processor
70
+ */
71
+ export interface ProcessorState {
72
+ content: string
73
+ thinking: string
74
+ toolCalls: Map<string, InternalToolCallState>
75
+ toolCallOrder: Array<string>
76
+ finishReason: string | null
77
+ done: boolean
78
+ }
79
+
80
+ /**
81
+ * Recording format for replay testing
82
+ */
83
+ export interface ChunkRecording {
84
+ version: '1.0'
85
+ timestamp: number
86
+ model?: string
87
+ provider?: string
88
+ chunks: Array<{
89
+ chunk: StreamChunk
90
+ timestamp: number
91
+ index: number
92
+ }>
93
+ result?: ProcessorResult
94
+ }
@@ -0,0 +1,471 @@
1
+ import type {
2
+ DoneStreamChunk,
3
+ ModelMessage,
4
+ Tool,
5
+ ToolCall,
6
+ ToolResultStreamChunk,
7
+ } from '../types'
8
+
9
+ /**
10
+ * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.
11
+ *
12
+ * Responsibilities:
13
+ * - Accumulates streaming tool call chunks (ID, name, arguments)
14
+ * - Validates tool calls (filters out incomplete ones)
15
+ * - Executes tool `execute` functions with parsed arguments
16
+ * - Emits `tool_result` chunks for client visibility
17
+ * - Returns tool result messages for conversation history
18
+ *
19
+ * This class is used internally by the AI.chat() method to handle the automatic
20
+ * tool execution loop. It can also be used independently for custom tool execution logic.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const manager = new ToolCallManager(tools);
25
+ *
26
+ * // During streaming, accumulate tool calls
27
+ * for await (const chunk of stream) {
28
+ * if (chunk.type === "tool_call") {
29
+ * manager.addToolCallChunk(chunk);
30
+ * }
31
+ * }
32
+ *
33
+ * // After stream completes, execute tools
34
+ * if (manager.hasToolCalls()) {
35
+ * const toolResults = yield* manager.executeTools(doneChunk);
36
+ * messages = [...messages, ...toolResults];
37
+ * manager.clear();
38
+ * }
39
+ * ```
40
+ */
41
+ export class ToolCallManager {
42
+ private toolCallsMap = new Map<number, ToolCall>()
43
+ private tools: ReadonlyArray<Tool>
44
+
45
+ constructor(tools: ReadonlyArray<Tool>) {
46
+ this.tools = tools
47
+ }
48
+
49
+ /**
50
+ * Add a tool call chunk to the accumulator
51
+ * Handles streaming tool calls by accumulating arguments
52
+ */
53
+ addToolCallChunk(chunk: {
54
+ toolCall: {
55
+ id: string
56
+ type: 'function'
57
+ function: {
58
+ name: string
59
+ arguments: string
60
+ }
61
+ }
62
+ index: number
63
+ }): void {
64
+ const index = chunk.index
65
+ const existing = this.toolCallsMap.get(index)
66
+
67
+ if (!existing) {
68
+ // Only create entry if we have a tool call ID and name
69
+ if (chunk.toolCall.id && chunk.toolCall.function.name) {
70
+ this.toolCallsMap.set(index, {
71
+ id: chunk.toolCall.id,
72
+ type: 'function',
73
+ function: {
74
+ name: chunk.toolCall.function.name,
75
+ arguments: chunk.toolCall.function.arguments || '',
76
+ },
77
+ })
78
+ }
79
+ } else {
80
+ // Update name if it wasn't set before
81
+ if (chunk.toolCall.function.name && !existing.function.name) {
82
+ existing.function.name = chunk.toolCall.function.name
83
+ }
84
+ // Accumulate arguments for streaming tool calls
85
+ if (chunk.toolCall.function.arguments) {
86
+ existing.function.arguments += chunk.toolCall.function.arguments
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check if there are any complete tool calls to execute
93
+ */
94
+ hasToolCalls(): boolean {
95
+ return this.getToolCalls().length > 0
96
+ }
97
+
98
+ /**
99
+ * Get all complete tool calls (filtered for valid ID and name)
100
+ */
101
+ getToolCalls(): Array<ToolCall> {
102
+ return Array.from(this.toolCallsMap.values()).filter(
103
+ (tc) => tc.id && tc.function.name && tc.function.name.trim().length > 0,
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Execute all tool calls and return tool result messages
109
+ * Also yields tool_result chunks for streaming
110
+ */
111
+ async *executeTools(
112
+ doneChunk: DoneStreamChunk,
113
+ ): AsyncGenerator<ToolResultStreamChunk, Array<ModelMessage>, void> {
114
+ const toolCallsArray = this.getToolCalls()
115
+ const toolResults: Array<ModelMessage> = []
116
+
117
+ for (const toolCall of toolCallsArray) {
118
+ const tool = this.tools.find((t) => t.name === toolCall.function.name)
119
+
120
+ let toolResultContent: string
121
+ if (tool?.execute) {
122
+ try {
123
+ // Parse arguments
124
+ let args: any
125
+ try {
126
+ args = JSON.parse(toolCall.function.arguments)
127
+ } catch (parseError) {
128
+ throw new Error(
129
+ `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`,
130
+ )
131
+ }
132
+
133
+ // Validate input against inputSchema
134
+ if (tool.inputSchema) {
135
+ try {
136
+ args = tool.inputSchema.parse(args)
137
+ } catch (validationError: any) {
138
+ throw new Error(
139
+ `Input validation failed for tool ${tool.name}: ${validationError.message}`,
140
+ )
141
+ }
142
+ }
143
+
144
+ // Execute the tool
145
+ let result = await tool.execute(args)
146
+
147
+ // Validate output against outputSchema if provided
148
+ if (tool.outputSchema && result !== undefined && result !== null) {
149
+ try {
150
+ result = tool.outputSchema.parse(result)
151
+ } catch (validationError: any) {
152
+ throw new Error(
153
+ `Output validation failed for tool ${tool.name}: ${validationError.message}`,
154
+ )
155
+ }
156
+ }
157
+
158
+ toolResultContent =
159
+ typeof result === 'string' ? result : JSON.stringify(result)
160
+ } catch (error: any) {
161
+ // If tool execution fails, add error message
162
+ toolResultContent = `Error executing tool: ${error.message}`
163
+ }
164
+ } else {
165
+ // Tool doesn't have execute function, add placeholder
166
+ toolResultContent = `Tool ${toolCall.function.name} does not have an execute function`
167
+ }
168
+
169
+ // Emit tool_result chunk so callers can track tool execution
170
+ yield {
171
+ type: 'tool_result',
172
+ id: doneChunk.id,
173
+ model: doneChunk.model,
174
+ timestamp: Date.now(),
175
+ toolCallId: toolCall.id,
176
+ content: toolResultContent,
177
+ }
178
+
179
+ // Add tool result message
180
+ toolResults.push({
181
+ role: 'tool',
182
+ content: toolResultContent,
183
+ toolCallId: toolCall.id,
184
+ })
185
+ }
186
+
187
+ return toolResults
188
+ }
189
+
190
+ /**
191
+ * Clear the tool calls map for the next iteration
192
+ */
193
+ clear(): void {
194
+ this.toolCallsMap.clear()
195
+ }
196
+ }
197
+
198
+ export interface ToolResult {
199
+ toolCallId: string
200
+ toolName: string
201
+ result: any
202
+ state?: 'output-available' | 'output-error'
203
+ /** Duration of tool execution in milliseconds (only for server-executed tools) */
204
+ duration?: number
205
+ }
206
+
207
+ export interface ApprovalRequest {
208
+ toolCallId: string
209
+ toolName: string
210
+ input: any
211
+ approvalId: string
212
+ }
213
+
214
+ export interface ClientToolRequest {
215
+ toolCallId: string
216
+ toolName: string
217
+ input: any
218
+ }
219
+
220
+ interface ExecuteToolCallsResult {
221
+ /** Tool results ready to send to LLM */
222
+ results: Array<ToolResult>
223
+ /** Tools that need user approval before execution */
224
+ needsApproval: Array<ApprovalRequest>
225
+ /** Tools that need client-side execution */
226
+ needsClientExecution: Array<ClientToolRequest>
227
+ }
228
+
229
+ /**
230
+ * Execute tool calls based on their configuration
231
+ *
232
+ * Handles three cases:
233
+ * 1. Client tools (no execute) - request client to execute
234
+ * 2. Server tools with approval - check approval before executing
235
+ * 3. Normal server tools - execute immediately
236
+ *
237
+ * @param toolCalls - Tool calls from the LLM
238
+ * @param tools - Available tools with their configurations
239
+ * @param approvals - Map of approval decisions (approval.id -> approved boolean)
240
+ * @param clientResults - Map of client-side execution results (toolCallId -> result)
241
+ */
242
+ export async function executeToolCalls(
243
+ toolCalls: Array<ToolCall>,
244
+ tools: ReadonlyArray<Tool>,
245
+ approvals: Map<string, boolean> = new Map(),
246
+ clientResults: Map<string, any> = new Map(),
247
+ ): Promise<ExecuteToolCallsResult> {
248
+ const results: Array<ToolResult> = []
249
+ const needsApproval: Array<ApprovalRequest> = []
250
+ const needsClientExecution: Array<ClientToolRequest> = []
251
+
252
+ // Create tool lookup map
253
+ const toolMap = new Map<string, Tool>()
254
+ for (const tool of tools) {
255
+ toolMap.set(tool.name, tool)
256
+ }
257
+
258
+ for (const toolCall of toolCalls) {
259
+ const tool = toolMap.get(toolCall.function.name)
260
+ const toolName = toolCall.function.name
261
+
262
+ if (!tool) {
263
+ // Unknown tool - return error
264
+ results.push({
265
+ toolCallId: toolCall.id,
266
+ toolName,
267
+ result: { error: `Unknown tool: ${toolName}` },
268
+ state: 'output-error',
269
+ })
270
+ continue
271
+ }
272
+
273
+ // Parse arguments, throwing error if invalid JSON
274
+ let input: any = {}
275
+ const argsStr = toolCall.function.arguments.trim() || '{}'
276
+ if (argsStr) {
277
+ try {
278
+ input = JSON.parse(argsStr)
279
+ } catch (parseError) {
280
+ // If parsing fails, throw error to fail fast
281
+ throw new Error(`Failed to parse tool arguments as JSON: ${argsStr}`)
282
+ }
283
+ }
284
+
285
+ // Validate input against inputSchema
286
+ if (tool.inputSchema) {
287
+ try {
288
+ input = tool.inputSchema.parse(input)
289
+ } catch (validationError: any) {
290
+ results.push({
291
+ toolCallId: toolCall.id,
292
+ toolName,
293
+ result: {
294
+ error: `Input validation failed for tool ${tool.name}: ${validationError.message}`,
295
+ },
296
+ state: 'output-error',
297
+ })
298
+ continue
299
+ }
300
+ }
301
+
302
+ // CASE 1: Client-side tool (no execute function)
303
+ if (!tool.execute) {
304
+ // Check if tool needs approval
305
+ if (tool.needsApproval) {
306
+ const approvalId = `approval_${toolCall.id}`
307
+
308
+ // Check if approval decision exists
309
+ if (approvals.has(approvalId)) {
310
+ const approved = approvals.get(approvalId)
311
+
312
+ if (approved) {
313
+ // Approved - check if client has executed
314
+ if (clientResults.has(toolCall.id)) {
315
+ results.push({
316
+ toolCallId: toolCall.id,
317
+ toolName,
318
+ result: clientResults.get(toolCall.id),
319
+ })
320
+ } else {
321
+ // Approved but not executed yet - request client execution
322
+ needsClientExecution.push({
323
+ toolCallId: toolCall.id,
324
+ toolName,
325
+ input,
326
+ })
327
+ }
328
+ } else {
329
+ // User declined
330
+ results.push({
331
+ toolCallId: toolCall.id,
332
+ toolName,
333
+ result: { error: 'User declined tool execution' },
334
+ state: 'output-error',
335
+ })
336
+ }
337
+ } else {
338
+ // Need approval first
339
+ needsApproval.push({
340
+ toolCallId: toolCall.id,
341
+ toolName: toolCall.function.name,
342
+ input,
343
+ approvalId,
344
+ })
345
+ }
346
+ } else {
347
+ // No approval needed - check if client has executed
348
+ if (clientResults.has(toolCall.id)) {
349
+ results.push({
350
+ toolCallId: toolCall.id,
351
+ toolName,
352
+ result: clientResults.get(toolCall.id),
353
+ })
354
+ } else {
355
+ // Request client execution
356
+ needsClientExecution.push({
357
+ toolCallId: toolCall.id,
358
+ toolName,
359
+ input,
360
+ })
361
+ }
362
+ }
363
+ continue
364
+ }
365
+
366
+ // CASE 2: Server tool with approval required
367
+ if (tool.needsApproval) {
368
+ const approvalId = `approval_${toolCall.id}`
369
+
370
+ // Check if approval decision exists
371
+ if (approvals.has(approvalId)) {
372
+ const approved = approvals.get(approvalId)
373
+
374
+ if (approved) {
375
+ // Execute after approval
376
+ const startTime = Date.now()
377
+ try {
378
+ let result = await tool.execute(input)
379
+ const duration = Date.now() - startTime
380
+
381
+ // Validate output against outputSchema if provided
382
+ if (tool.outputSchema && result !== undefined && result !== null) {
383
+ const parsed = tool.outputSchema.safeParse(result)
384
+ if (parsed.success) {
385
+ result = parsed.data
386
+ } else {
387
+ throw new Error(
388
+ `Output validation failed for tool ${tool.name}: ${parsed.error.message}`,
389
+ )
390
+ }
391
+ }
392
+
393
+ results.push({
394
+ toolCallId: toolCall.id,
395
+ toolName,
396
+ result:
397
+ typeof result === 'string'
398
+ ? JSON.parse(result)
399
+ : result || null,
400
+ duration,
401
+ })
402
+ } catch (error: any) {
403
+ const duration = Date.now() - startTime
404
+ results.push({
405
+ toolCallId: toolCall.id,
406
+ toolName,
407
+ result: { error: error.message },
408
+ state: 'output-error',
409
+ duration,
410
+ })
411
+ }
412
+ } else {
413
+ // User declined
414
+ results.push({
415
+ toolCallId: toolCall.id,
416
+ toolName,
417
+ result: { error: 'User declined tool execution' },
418
+ state: 'output-error',
419
+ })
420
+ }
421
+ } else {
422
+ // Need approval
423
+ needsApproval.push({
424
+ toolCallId: toolCall.id,
425
+ toolName,
426
+ input,
427
+ approvalId,
428
+ })
429
+ }
430
+ continue
431
+ }
432
+
433
+ // CASE 3: Normal server tool - execute immediately
434
+ const startTime = Date.now()
435
+ try {
436
+ let result = await tool.execute(input)
437
+ const duration = Date.now() - startTime
438
+
439
+ // Validate output against outputSchema if provided
440
+ if (tool.outputSchema && result !== undefined && result !== null) {
441
+ const parsed = tool.outputSchema.safeParse(result)
442
+ if (parsed.success) {
443
+ result = parsed.data
444
+ } else {
445
+ throw new Error(
446
+ `Output validation failed for tool ${tool.name}: ${parsed.error.message}`,
447
+ )
448
+ }
449
+ }
450
+
451
+ results.push({
452
+ toolCallId: toolCall.id,
453
+ toolName,
454
+ result:
455
+ typeof result === 'string' ? JSON.parse(result) : result || null,
456
+ duration,
457
+ })
458
+ } catch (error: any) {
459
+ const duration = Date.now() - startTime
460
+ results.push({
461
+ toolCallId: toolCall.id,
462
+ toolName,
463
+ result: { error: error.message },
464
+ state: 'output-error',
465
+ duration,
466
+ })
467
+ }
468
+ }
469
+
470
+ return { results, needsApproval, needsClientExecution }
471
+ }