@tanstack/ai 0.4.2 → 0.5.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.
- package/dist/esm/activities/chat/index.js +3 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +13 -6
- package/dist/esm/activities/chat/messages.js +115 -86
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +143 -26
- package/dist/esm/activities/chat/stream/processor.js +205 -77
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -3
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/types.d.ts +5 -5
- package/package.json +1 -1
- package/src/activities/chat/index.ts +12 -9
- package/src/activities/chat/messages.ts +211 -138
- package/src/activities/chat/stream/processor.ts +240 -116
- package/src/activities/chat/tools/tool-calls.ts +1 -4
- package/src/types.ts +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-calls.js","sources":["../../../../../src/activities/chat/tools/tool-calls.ts"],"sourcesContent":["import { isStandardSchema, parseWithStandardSchema } from './schema-converter'\nimport type {\n ModelMessage,\n RunFinishedEvent,\n Tool,\n ToolCall,\n ToolCallArgsEvent,\n ToolCallEndEvent,\n ToolCallStartEvent,\n} from '../../../types'\n\n/**\n * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.\n *\n * Responsibilities:\n * - Accumulates streaming tool call events (ID, name, arguments)\n * - Validates tool calls (filters out incomplete ones)\n * - Executes tool `execute` functions with parsed arguments\n * - Emits `TOOL_CALL_END` events for client visibility\n * - Returns tool result messages for conversation history\n *\n * This class is used internally by the AI.chat() method to handle the automatic\n * tool execution loop. It can also be used independently for custom tool execution logic.\n *\n * @example\n * ```typescript\n * const manager = new ToolCallManager(tools);\n *\n * // During streaming, accumulate tool calls\n * for await (const chunk of stream) {\n * if (chunk.type === 'TOOL_CALL_START') {\n * manager.addToolCallStartEvent(chunk);\n * } else if (chunk.type === 'TOOL_CALL_ARGS') {\n * manager.addToolCallArgsEvent(chunk);\n * }\n * }\n *\n * // After stream completes, execute tools\n * if (manager.hasToolCalls()) {\n * const toolResults = yield* manager.executeTools(finishEvent);\n * messages = [...messages, ...toolResults];\n * manager.clear();\n * }\n * ```\n */\nexport class ToolCallManager {\n private toolCallsMap = new Map<number, ToolCall>()\n private tools: ReadonlyArray<Tool>\n\n constructor(tools: ReadonlyArray<Tool>) {\n this.tools = tools\n }\n\n /**\n * Add a TOOL_CALL_START event to begin tracking a tool call (AG-UI)\n */\n addToolCallStartEvent(event: ToolCallStartEvent): void {\n const index = event.index ?? this.toolCallsMap.size\n this.toolCallsMap.set(index, {\n id: event.toolCallId,\n type: 'function',\n function: {\n name: event.toolName,\n arguments: '',\n },\n })\n }\n\n /**\n * Add a TOOL_CALL_ARGS event to accumulate arguments (AG-UI)\n */\n addToolCallArgsEvent(event: ToolCallArgsEvent): void {\n // Find the tool call by ID\n for (const [, toolCall] of this.toolCallsMap.entries()) {\n if (toolCall.id === event.toolCallId) {\n toolCall.function.arguments += event.delta\n break\n }\n }\n }\n\n /**\n * Complete a tool call with its final input\n * Called when TOOL_CALL_END is received\n */\n completeToolCall(event: ToolCallEndEvent): void {\n for (const [, toolCall] of this.toolCallsMap.entries()) {\n if (toolCall.id === event.toolCallId) {\n if (event.input !== undefined) {\n toolCall.function.arguments = JSON.stringify(event.input)\n }\n break\n }\n }\n }\n\n /**\n * Check if there are any complete tool calls to execute\n */\n hasToolCalls(): boolean {\n return this.getToolCalls().length > 0\n }\n\n /**\n * Get all complete tool calls (filtered for valid ID and name)\n */\n getToolCalls(): Array<ToolCall> {\n return Array.from(this.toolCallsMap.values()).filter(\n (tc) => tc.id && tc.function.name && tc.function.name.trim().length > 0,\n )\n }\n\n /**\n * Execute all tool calls and return tool result messages\n * Yields TOOL_CALL_END events for streaming\n * @param finishEvent - RUN_FINISHED event from the stream\n */\n async *executeTools(\n finishEvent: RunFinishedEvent,\n ): AsyncGenerator<ToolCallEndEvent, Array<ModelMessage>, void> {\n const toolCallsArray = this.getToolCalls()\n const toolResults: Array<ModelMessage> = []\n\n for (const toolCall of toolCallsArray) {\n const tool = this.tools.find((t) => t.name === toolCall.function.name)\n\n let toolResultContent: string\n if (tool?.execute) {\n try {\n // Parse arguments (normalize \"null\" to \"{}\" for empty tool_use blocks)\n let args: unknown\n try {\n const argsString = toolCall.function.arguments.trim() || '{}'\n args = JSON.parse(argsString === 'null' ? '{}' : argsString)\n } catch (parseError) {\n throw new Error(\n `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`,\n )\n }\n\n // Validate input against inputSchema (for Standard Schema compliant schemas)\n if (tool.inputSchema && isStandardSchema(tool.inputSchema)) {\n try {\n args = parseWithStandardSchema(tool.inputSchema, args)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n throw new Error(\n `Input validation failed for tool ${tool.name}: ${message}`,\n )\n }\n }\n\n // Execute the tool\n let result = await tool.execute(args)\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n try {\n result = parseWithStandardSchema(tool.outputSchema, result)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n throw new Error(\n `Output validation failed for tool ${tool.name}: ${message}`,\n )\n }\n }\n\n toolResultContent =\n typeof result === 'string' ? result : JSON.stringify(result)\n } catch (error: unknown) {\n // If tool execution fails, add error message\n const message =\n error instanceof Error ? error.message : 'Unknown error'\n toolResultContent = `Error executing tool: ${message}`\n }\n } else {\n // Tool doesn't have execute function, add placeholder\n toolResultContent = `Tool ${toolCall.function.name} does not have an execute function`\n }\n\n // Emit TOOL_CALL_END event\n yield {\n type: 'TOOL_CALL_END',\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n model: finishEvent.model,\n timestamp: Date.now(),\n result: toolResultContent,\n }\n\n // Add tool result message\n toolResults.push({\n role: 'tool',\n content: toolResultContent,\n toolCallId: toolCall.id,\n })\n }\n\n return toolResults\n }\n\n /**\n * Clear the tool calls map for the next iteration\n */\n clear(): void {\n this.toolCallsMap.clear()\n }\n}\n\nexport interface ToolResult {\n toolCallId: string\n toolName: string\n result: any\n state?: 'output-available' | 'output-error'\n /** Duration of tool execution in milliseconds (only for server-executed tools) */\n duration?: number\n}\n\nexport interface ApprovalRequest {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n}\n\nexport interface ClientToolRequest {\n toolCallId: string\n toolName: string\n input: any\n}\n\ninterface ExecuteToolCallsResult {\n /** Tool results ready to send to LLM */\n results: Array<ToolResult>\n /** Tools that need user approval before execution */\n needsApproval: Array<ApprovalRequest>\n /** Tools that need client-side execution */\n needsClientExecution: Array<ClientToolRequest>\n}\n\n/**\n * Execute tool calls based on their configuration\n *\n * Handles three cases:\n * 1. Client tools (no execute) - request client to execute\n * 2. Server tools with approval - check approval before executing\n * 3. Normal server tools - execute immediately\n *\n * @param toolCalls - Tool calls from the LLM\n * @param tools - Available tools with their configurations\n * @param approvals - Map of approval decisions (approval.id -> approved boolean)\n * @param clientResults - Map of client-side execution results (toolCallId -> result)\n */\nexport async function executeToolCalls(\n toolCalls: Array<ToolCall>,\n tools: ReadonlyArray<Tool>,\n approvals: Map<string, boolean> = new Map(),\n clientResults: Map<string, any> = new Map(),\n): Promise<ExecuteToolCallsResult> {\n const results: Array<ToolResult> = []\n const needsApproval: Array<ApprovalRequest> = []\n const needsClientExecution: Array<ClientToolRequest> = []\n\n // Create tool lookup map\n const toolMap = new Map<string, Tool>()\n for (const tool of tools) {\n toolMap.set(tool.name, tool)\n }\n\n for (const toolCall of toolCalls) {\n const tool = toolMap.get(toolCall.function.name)\n const toolName = toolCall.function.name\n\n if (!tool) {\n // Unknown tool - return error\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: `Unknown tool: ${toolName}` },\n state: 'output-error',\n })\n continue\n }\n\n // Parse arguments, throwing error if invalid JSON\n let input: unknown = {}\n let argsStr = toolCall.function.arguments.trim() || '{}'\n // Normalize \"null\" to \"{}\" — can occur when the model streams a tool_use\n // block with no input_json_delta events (Anthropic adapter edge case)\n if (argsStr === 'null') argsStr = '{}'\n if (argsStr) {\n try {\n input = JSON.parse(argsStr)\n } catch (parseError) {\n // If parsing fails, throw error to fail fast\n throw new Error(`Failed to parse tool arguments as JSON: ${argsStr}`)\n }\n }\n\n // Validate input against inputSchema (for Standard Schema compliant schemas)\n if (tool.inputSchema && isStandardSchema(tool.inputSchema)) {\n try {\n input = parseWithStandardSchema(tool.inputSchema, input)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: {\n error: `Input validation failed for tool ${tool.name}: ${message}`,\n },\n state: 'output-error',\n })\n continue\n }\n }\n\n // CASE 1: Client-side tool (no execute function)\n if (!tool.execute) {\n // Check if tool needs approval\n if (tool.needsApproval) {\n const approvalId = `approval_${toolCall.id}`\n\n // Check if approval decision exists\n if (approvals.has(approvalId)) {\n const approved = approvals.get(approvalId)\n\n if (approved) {\n // Approved - check if client has executed\n if (clientResults.has(toolCall.id)) {\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: clientResults.get(toolCall.id),\n })\n } else {\n // Approved but not executed yet - request client execution\n needsClientExecution.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n })\n }\n } else {\n // User declined\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: 'User declined tool execution' },\n state: 'output-error',\n })\n }\n } else {\n // Need approval first\n needsApproval.push({\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n input,\n approvalId,\n })\n }\n } else {\n // No approval needed - check if client has executed\n if (clientResults.has(toolCall.id)) {\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: clientResults.get(toolCall.id),\n })\n } else {\n // Request client execution\n needsClientExecution.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n })\n }\n }\n continue\n }\n\n // CASE 2: Server tool with approval required\n if (tool.needsApproval) {\n const approvalId = `approval_${toolCall.id}`\n\n // Check if approval decision exists\n if (approvals.has(approvalId)) {\n const approved = approvals.get(approvalId)\n\n if (approved) {\n // Execute after approval\n const startTime = Date.now()\n try {\n let result = await tool.execute(input)\n const duration = Date.now() - startTime\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n result = parseWithStandardSchema(tool.outputSchema, result)\n }\n\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result:\n typeof result === 'string'\n ? JSON.parse(result)\n : result || null,\n duration,\n })\n } catch (error: unknown) {\n const duration = Date.now() - startTime\n const message =\n error instanceof Error ? error.message : 'Unknown error'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: message },\n state: 'output-error',\n duration,\n })\n }\n } else {\n // User declined\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: 'User declined tool execution' },\n state: 'output-error',\n })\n }\n } else {\n // Need approval\n needsApproval.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n approvalId,\n })\n }\n continue\n }\n\n // CASE 3: Normal server tool - execute immediately\n const startTime = Date.now()\n try {\n let result = await tool.execute(input)\n const duration = Date.now() - startTime\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n result = parseWithStandardSchema(tool.outputSchema, result)\n }\n\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result:\n typeof result === 'string' ? JSON.parse(result) : result || null,\n duration,\n })\n } catch (error: unknown) {\n const duration = Date.now() - startTime\n const message = error instanceof Error ? error.message : 'Unknown error'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: message },\n state: 'output-error',\n duration,\n })\n }\n }\n\n return { results, needsApproval, needsClientExecution }\n}\n"],"names":["startTime"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool-calls.js","sources":["../../../../../src/activities/chat/tools/tool-calls.ts"],"sourcesContent":["import { isStandardSchema, parseWithStandardSchema } from './schema-converter'\nimport type {\n ModelMessage,\n RunFinishedEvent,\n Tool,\n ToolCall,\n ToolCallArgsEvent,\n ToolCallEndEvent,\n ToolCallStartEvent,\n} from '../../../types'\n\n/**\n * Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.\n *\n * Responsibilities:\n * - Accumulates streaming tool call events (ID, name, arguments)\n * - Validates tool calls (filters out incomplete ones)\n * - Executes tool `execute` functions with parsed arguments\n * - Emits `TOOL_CALL_END` events for client visibility\n * - Returns tool result messages for conversation history\n *\n * This class is used internally by the AI.chat() method to handle the automatic\n * tool execution loop. It can also be used independently for custom tool execution logic.\n *\n * @example\n * ```typescript\n * const manager = new ToolCallManager(tools);\n *\n * // During streaming, accumulate tool calls\n * for await (const chunk of stream) {\n * if (chunk.type === 'TOOL_CALL_START') {\n * manager.addToolCallStartEvent(chunk);\n * } else if (chunk.type === 'TOOL_CALL_ARGS') {\n * manager.addToolCallArgsEvent(chunk);\n * }\n * }\n *\n * // After stream completes, execute tools\n * if (manager.hasToolCalls()) {\n * const toolResults = yield* manager.executeTools(finishEvent);\n * messages = [...messages, ...toolResults];\n * manager.clear();\n * }\n * ```\n */\nexport class ToolCallManager {\n private toolCallsMap = new Map<number, ToolCall>()\n private tools: ReadonlyArray<Tool>\n\n constructor(tools: ReadonlyArray<Tool>) {\n this.tools = tools\n }\n\n /**\n * Add a TOOL_CALL_START event to begin tracking a tool call (AG-UI)\n */\n addToolCallStartEvent(event: ToolCallStartEvent): void {\n const index = event.index ?? this.toolCallsMap.size\n this.toolCallsMap.set(index, {\n id: event.toolCallId,\n type: 'function',\n function: {\n name: event.toolName,\n arguments: '',\n },\n })\n }\n\n /**\n * Add a TOOL_CALL_ARGS event to accumulate arguments (AG-UI)\n */\n addToolCallArgsEvent(event: ToolCallArgsEvent): void {\n // Find the tool call by ID\n for (const [, toolCall] of this.toolCallsMap.entries()) {\n if (toolCall.id === event.toolCallId) {\n toolCall.function.arguments += event.delta\n break\n }\n }\n }\n\n /**\n * Complete a tool call with its final input\n * Called when TOOL_CALL_END is received\n */\n completeToolCall(event: ToolCallEndEvent): void {\n for (const [, toolCall] of this.toolCallsMap.entries()) {\n if (toolCall.id === event.toolCallId) {\n if (event.input !== undefined) {\n toolCall.function.arguments = JSON.stringify(event.input)\n }\n break\n }\n }\n }\n\n /**\n * Check if there are any complete tool calls to execute\n */\n hasToolCalls(): boolean {\n return this.getToolCalls().length > 0\n }\n\n /**\n * Get all complete tool calls (filtered for valid ID and name)\n */\n getToolCalls(): Array<ToolCall> {\n return Array.from(this.toolCallsMap.values()).filter(\n (tc) => tc.id && tc.function.name && tc.function.name.trim().length > 0,\n )\n }\n\n /**\n * Execute all tool calls and return tool result messages\n * Yields TOOL_CALL_END events for streaming\n * @param finishEvent - RUN_FINISHED event from the stream\n */\n async *executeTools(\n finishEvent: RunFinishedEvent,\n ): AsyncGenerator<ToolCallEndEvent, Array<ModelMessage>, void> {\n const toolCallsArray = this.getToolCalls()\n const toolResults: Array<ModelMessage> = []\n\n for (const toolCall of toolCallsArray) {\n const tool = this.tools.find((t) => t.name === toolCall.function.name)\n\n let toolResultContent: string\n if (tool?.execute) {\n try {\n // Parse arguments (normalize \"null\" to \"{}\" for empty tool_use blocks)\n let args: unknown\n try {\n const argsString = toolCall.function.arguments.trim() || '{}'\n args = JSON.parse(argsString === 'null' ? '{}' : argsString)\n } catch (parseError) {\n throw new Error(\n `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`,\n )\n }\n\n // Validate input against inputSchema (for Standard Schema compliant schemas)\n if (tool.inputSchema && isStandardSchema(tool.inputSchema)) {\n try {\n args = parseWithStandardSchema(tool.inputSchema, args)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n throw new Error(\n `Input validation failed for tool ${tool.name}: ${message}`,\n )\n }\n }\n\n // Execute the tool\n let result = await tool.execute(args)\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n try {\n result = parseWithStandardSchema(tool.outputSchema, result)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n throw new Error(\n `Output validation failed for tool ${tool.name}: ${message}`,\n )\n }\n }\n\n toolResultContent =\n typeof result === 'string' ? result : JSON.stringify(result)\n } catch (error: unknown) {\n // If tool execution fails, add error message\n const message =\n error instanceof Error ? error.message : 'Unknown error'\n toolResultContent = `Error executing tool: ${message}`\n }\n } else {\n // Tool doesn't have execute function, add placeholder\n toolResultContent = `Tool ${toolCall.function.name} does not have an execute function`\n }\n\n // Emit TOOL_CALL_END event\n yield {\n type: 'TOOL_CALL_END',\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n model: finishEvent.model,\n timestamp: Date.now(),\n result: toolResultContent,\n }\n\n // Add tool result message\n toolResults.push({\n role: 'tool',\n content: toolResultContent,\n toolCallId: toolCall.id,\n })\n }\n\n return toolResults\n }\n\n /**\n * Clear the tool calls map for the next iteration\n */\n clear(): void {\n this.toolCallsMap.clear()\n }\n}\n\nexport interface ToolResult {\n toolCallId: string\n toolName: string\n result: any\n state?: 'output-available' | 'output-error'\n /** Duration of tool execution in milliseconds (only for server-executed tools) */\n duration?: number\n}\n\nexport interface ApprovalRequest {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n}\n\nexport interface ClientToolRequest {\n toolCallId: string\n toolName: string\n input: any\n}\n\ninterface ExecuteToolCallsResult {\n /** Tool results ready to send to LLM */\n results: Array<ToolResult>\n /** Tools that need user approval before execution */\n needsApproval: Array<ApprovalRequest>\n /** Tools that need client-side execution */\n needsClientExecution: Array<ClientToolRequest>\n}\n\n/**\n * Execute tool calls based on their configuration\n *\n * Handles three cases:\n * 1. Client tools (no execute) - request client to execute\n * 2. Server tools with approval - check approval before executing\n * 3. Normal server tools - execute immediately\n *\n * @param toolCalls - Tool calls from the LLM\n * @param tools - Available tools with their configurations\n * @param approvals - Map of approval decisions (approval.id -> approved boolean)\n * @param clientResults - Map of client-side execution results (toolCallId -> result)\n */\nexport async function executeToolCalls(\n toolCalls: Array<ToolCall>,\n tools: ReadonlyArray<Tool>,\n approvals: Map<string, boolean> = new Map(),\n clientResults: Map<string, any> = new Map(),\n): Promise<ExecuteToolCallsResult> {\n const results: Array<ToolResult> = []\n const needsApproval: Array<ApprovalRequest> = []\n const needsClientExecution: Array<ClientToolRequest> = []\n\n // Create tool lookup map\n const toolMap = new Map<string, Tool>()\n for (const tool of tools) {\n toolMap.set(tool.name, tool)\n }\n\n for (const toolCall of toolCalls) {\n const tool = toolMap.get(toolCall.function.name)\n const toolName = toolCall.function.name\n\n if (!tool) {\n // Unknown tool - return error\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: `Unknown tool: ${toolName}` },\n state: 'output-error',\n })\n continue\n }\n\n // Parse arguments, throwing error if invalid JSON\n let input: unknown = {}\n const argsStr = toolCall.function.arguments.trim() || '{}'\n if (argsStr) {\n try {\n input = JSON.parse(argsStr)\n } catch (parseError) {\n // If parsing fails, throw error to fail fast\n throw new Error(`Failed to parse tool arguments as JSON: ${argsStr}`)\n }\n }\n\n // Validate input against inputSchema (for Standard Schema compliant schemas)\n if (tool.inputSchema && isStandardSchema(tool.inputSchema)) {\n try {\n input = parseWithStandardSchema(tool.inputSchema, input)\n } catch (validationError: unknown) {\n const message =\n validationError instanceof Error\n ? validationError.message\n : 'Validation failed'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: {\n error: `Input validation failed for tool ${tool.name}: ${message}`,\n },\n state: 'output-error',\n })\n continue\n }\n }\n\n // CASE 1: Client-side tool (no execute function)\n if (!tool.execute) {\n // Check if tool needs approval\n if (tool.needsApproval) {\n const approvalId = `approval_${toolCall.id}`\n\n // Check if approval decision exists\n if (approvals.has(approvalId)) {\n const approved = approvals.get(approvalId)\n\n if (approved) {\n // Approved - check if client has executed\n if (clientResults.has(toolCall.id)) {\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: clientResults.get(toolCall.id),\n })\n } else {\n // Approved but not executed yet - request client execution\n needsClientExecution.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n })\n }\n } else {\n // User declined\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: 'User declined tool execution' },\n state: 'output-error',\n })\n }\n } else {\n // Need approval first\n needsApproval.push({\n toolCallId: toolCall.id,\n toolName: toolCall.function.name,\n input,\n approvalId,\n })\n }\n } else {\n // No approval needed - check if client has executed\n if (clientResults.has(toolCall.id)) {\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: clientResults.get(toolCall.id),\n })\n } else {\n // Request client execution\n needsClientExecution.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n })\n }\n }\n continue\n }\n\n // CASE 2: Server tool with approval required\n if (tool.needsApproval) {\n const approvalId = `approval_${toolCall.id}`\n\n // Check if approval decision exists\n if (approvals.has(approvalId)) {\n const approved = approvals.get(approvalId)\n\n if (approved) {\n // Execute after approval\n const startTime = Date.now()\n try {\n let result = await tool.execute(input)\n const duration = Date.now() - startTime\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n result = parseWithStandardSchema(tool.outputSchema, result)\n }\n\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result:\n typeof result === 'string'\n ? JSON.parse(result)\n : result || null,\n duration,\n })\n } catch (error: unknown) {\n const duration = Date.now() - startTime\n const message =\n error instanceof Error ? error.message : 'Unknown error'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: message },\n state: 'output-error',\n duration,\n })\n }\n } else {\n // User declined\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: 'User declined tool execution' },\n state: 'output-error',\n })\n }\n } else {\n // Need approval\n needsApproval.push({\n toolCallId: toolCall.id,\n toolName,\n input,\n approvalId,\n })\n }\n continue\n }\n\n // CASE 3: Normal server tool - execute immediately\n const startTime = Date.now()\n try {\n let result = await tool.execute(input)\n const duration = Date.now() - startTime\n\n // Validate output against outputSchema if provided (for Standard Schema compliant schemas)\n if (\n tool.outputSchema &&\n isStandardSchema(tool.outputSchema) &&\n result !== undefined &&\n result !== null\n ) {\n result = parseWithStandardSchema(tool.outputSchema, result)\n }\n\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result:\n typeof result === 'string' ? JSON.parse(result) : result || null,\n duration,\n })\n } catch (error: unknown) {\n const duration = Date.now() - startTime\n const message = error instanceof Error ? error.message : 'Unknown error'\n results.push({\n toolCallId: toolCall.id,\n toolName,\n result: { error: message },\n state: 'output-error',\n duration,\n })\n }\n }\n\n return { results, needsApproval, needsClientExecution }\n}\n"],"names":["startTime"],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -652,9 +652,9 @@ export interface TextMessageContentEvent extends BaseAGUIEvent {
|
|
|
652
652
|
type: 'TEXT_MESSAGE_CONTENT';
|
|
653
653
|
/** Message identifier */
|
|
654
654
|
messageId: string;
|
|
655
|
-
/** The incremental content token
|
|
656
|
-
delta
|
|
657
|
-
/** Full accumulated content so far */
|
|
655
|
+
/** The incremental content token */
|
|
656
|
+
delta: string;
|
|
657
|
+
/** Full accumulated content so far (optional, for debugging) */
|
|
658
658
|
content?: string;
|
|
659
659
|
}
|
|
660
660
|
/**
|
|
@@ -721,8 +721,8 @@ export interface StepFinishedEvent extends BaseAGUIEvent {
|
|
|
721
721
|
/** Step identifier */
|
|
722
722
|
stepId: string;
|
|
723
723
|
/** Incremental thinking content */
|
|
724
|
-
delta
|
|
725
|
-
/** Full accumulated thinking content */
|
|
724
|
+
delta: string;
|
|
725
|
+
/** Full accumulated thinking content (optional, for debugging) */
|
|
726
726
|
content?: string;
|
|
727
727
|
}
|
|
728
728
|
/**
|
package/package.json
CHANGED
|
@@ -555,15 +555,6 @@ class TextEngine<
|
|
|
555
555
|
})
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
// Don't overwrite a tool_calls finishReason with a stop finishReason
|
|
559
|
-
if (
|
|
560
|
-
this.finishedEvent?.finishReason === 'tool_calls' &&
|
|
561
|
-
chunk.finishReason === 'stop'
|
|
562
|
-
) {
|
|
563
|
-
this.lastFinishReason = chunk.finishReason
|
|
564
|
-
return
|
|
565
|
-
}
|
|
566
|
-
|
|
567
558
|
this.finishedEvent = chunk
|
|
568
559
|
this.lastFinishReason = chunk.finishReason
|
|
569
560
|
}
|
|
@@ -800,6 +791,18 @@ class TextEngine<
|
|
|
800
791
|
} catch {
|
|
801
792
|
output = message.content
|
|
802
793
|
}
|
|
794
|
+
// Skip approval response messages (they have pendingExecution marker)
|
|
795
|
+
// These are NOT real client tool results — they are synthetic tool messages
|
|
796
|
+
// created by uiMessageToModelMessages for approved-but-not-yet-executed tools.
|
|
797
|
+
// Treating them as results would prevent the server from requesting actual
|
|
798
|
+
// client-side execution after approval (see GitHub issue #225).
|
|
799
|
+
if (
|
|
800
|
+
output &&
|
|
801
|
+
typeof output === 'object' &&
|
|
802
|
+
(output as any).pendingExecution === true
|
|
803
|
+
) {
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
803
806
|
clientToolResults.set(message.toolCallId, output)
|
|
804
807
|
}
|
|
805
808
|
}
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
AudioPart,
|
|
3
2
|
ContentPart,
|
|
4
|
-
DocumentPart,
|
|
5
|
-
ImagePart,
|
|
6
3
|
MessagePart,
|
|
7
4
|
ModelMessage,
|
|
8
5
|
TextPart,
|
|
9
6
|
ToolCallPart,
|
|
10
|
-
ToolResultPart,
|
|
11
7
|
UIMessage,
|
|
12
|
-
VideoPart,
|
|
13
8
|
} from '../../types'
|
|
14
9
|
// ===========================
|
|
15
10
|
// Message Converters
|
|
16
11
|
// ===========================
|
|
17
12
|
|
|
18
13
|
/**
|
|
19
|
-
*
|
|
14
|
+
* Check if a MessagePart is a content part (text, image, audio, video, document)
|
|
15
|
+
* that maps directly to a ModelMessage ContentPart.
|
|
20
16
|
*/
|
|
21
|
-
function
|
|
22
|
-
part: MessagePart,
|
|
23
|
-
): part is ImagePart | AudioPart | VideoPart | DocumentPart {
|
|
17
|
+
function isContentPart(part: MessagePart): part is ContentPart {
|
|
24
18
|
return (
|
|
19
|
+
part.type === 'text' ||
|
|
25
20
|
part.type === 'image' ||
|
|
26
21
|
part.type === 'audio' ||
|
|
27
22
|
part.type === 'video' ||
|
|
@@ -30,19 +25,34 @@ function isMultimodalPart(
|
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
28
|
+
* Collapse an array of ContentParts into the most compact ModelMessage content:
|
|
29
|
+
* - Empty array → null
|
|
30
|
+
* - All text parts → joined string (or null if empty)
|
|
31
|
+
* - Mixed content → ContentPart array as-is
|
|
35
32
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
function collapseContentParts(
|
|
34
|
+
parts: Array<ContentPart>,
|
|
35
|
+
): string | null | Array<ContentPart> {
|
|
36
|
+
if (parts.length === 0) return null
|
|
37
|
+
|
|
38
|
+
const allText = parts.every((p) => p.type === 'text')
|
|
39
|
+
if (allText) {
|
|
40
|
+
const joined = parts.map((p) => p.content).join('')
|
|
41
|
+
return joined || null
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
return parts
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract text content from ModelMessage content (string, null, or ContentPart array).
|
|
49
|
+
* Used when only the text portion is needed (e.g., tool result content).
|
|
50
|
+
*/
|
|
51
|
+
function getTextContent(content: string | null | Array<ContentPart>): string {
|
|
52
|
+
if (content === null) return ''
|
|
53
|
+
if (typeof content === 'string') return content
|
|
44
54
|
return content
|
|
45
|
-
.filter((part) => part.type === 'text')
|
|
55
|
+
.filter((part): part is TextPart => part.type === 'text')
|
|
46
56
|
.map((part) => part.content)
|
|
47
57
|
.join('')
|
|
48
58
|
}
|
|
@@ -69,157 +79,214 @@ export function convertMessagesToModelMessages(
|
|
|
69
79
|
/**
|
|
70
80
|
* Convert a UIMessage to ModelMessage(s)
|
|
71
81
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
82
|
+
* Walks the parts array IN ORDER to preserve the interleaving of text,
|
|
83
|
+
* tool calls, and tool results. This is critical for multi-round tool
|
|
84
|
+
* flows where the model generates text, calls a tool, gets the result,
|
|
85
|
+
* then generates more text and calls another tool.
|
|
86
|
+
*
|
|
87
|
+
* The output preserves the sequential structure:
|
|
88
|
+
* text1 → toolCall1 → toolResult1 → text2 → toolCall2 → toolResult2
|
|
89
|
+
* becomes:
|
|
90
|
+
* assistant: {content: "text1", toolCalls: [toolCall1]}
|
|
91
|
+
* tool: toolResult1
|
|
92
|
+
* assistant: {content: "text2", toolCalls: [toolCall2]}
|
|
93
|
+
* tool: toolResult2
|
|
77
94
|
*
|
|
78
95
|
* @param uiMessage - The UIMessage to convert
|
|
79
|
-
* @returns An array of ModelMessages
|
|
96
|
+
* @returns An array of ModelMessages preserving part ordering
|
|
80
97
|
*/
|
|
81
98
|
export function uiMessageToModelMessages(
|
|
82
99
|
uiMessage: UIMessage,
|
|
83
100
|
): Array<ModelMessage> {
|
|
84
|
-
const messageList: Array<ModelMessage> = []
|
|
85
|
-
|
|
86
101
|
// Skip system messages - they're handled via systemPrompts, not ModelMessages
|
|
87
102
|
if (uiMessage.role === 'system') {
|
|
88
|
-
return
|
|
103
|
+
return []
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
> = []
|
|
97
|
-
const toolCallParts: Array<ToolCallPart> = []
|
|
98
|
-
const toolResultParts: Array<ToolResultPart> = []
|
|
106
|
+
// For non-assistant messages (user), use the simpler path since they
|
|
107
|
+
// don't have tool calls or tool results to interleave
|
|
108
|
+
if (uiMessage.role !== 'assistant') {
|
|
109
|
+
return [buildUserOrToolMessage(uiMessage)]
|
|
110
|
+
}
|
|
99
111
|
|
|
112
|
+
// For assistant messages, walk parts in order to preserve interleaving
|
|
113
|
+
return buildAssistantMessages(uiMessage)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a single ModelMessage for user messages (simple path).
|
|
118
|
+
* Preserves ordering of text and multimodal content parts.
|
|
119
|
+
*/
|
|
120
|
+
function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {
|
|
121
|
+
const contentParts: Array<ContentPart> = []
|
|
100
122
|
for (const part of uiMessage.parts) {
|
|
101
|
-
if (part
|
|
102
|
-
|
|
103
|
-
} else if (isMultimodalPart(part)) {
|
|
104
|
-
multimodalParts.push(part)
|
|
105
|
-
} else if (part.type === 'tool-call') {
|
|
106
|
-
toolCallParts.push(part)
|
|
107
|
-
} else if (part.type === 'tool-result') {
|
|
108
|
-
toolResultParts.push(part)
|
|
123
|
+
if (isContentPart(part)) {
|
|
124
|
+
contentParts.push(part)
|
|
109
125
|
}
|
|
110
|
-
// thinking parts are skipped - they're UI-only
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
return {
|
|
129
|
+
role: uiMessage.role as 'user' | 'assistant' | 'tool',
|
|
130
|
+
content: collapseContentParts(contentParts),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Accumulator for building an assistant segment (content + tool calls)
|
|
135
|
+
interface AssistantSegment {
|
|
136
|
+
contentParts: Array<ContentPart>
|
|
137
|
+
toolCalls: Array<{
|
|
138
|
+
id: string
|
|
139
|
+
type: 'function'
|
|
140
|
+
function: { name: string; arguments: string }
|
|
141
|
+
}>
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createSegment(): AssistantSegment {
|
|
145
|
+
return { contentParts: [], toolCalls: [] }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isToolCallIncluded(part: ToolCallPart): boolean {
|
|
149
|
+
return (
|
|
150
|
+
part.state === 'input-complete' ||
|
|
151
|
+
part.state === 'approval-responded' ||
|
|
152
|
+
part.output !== undefined
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build ModelMessages for an assistant UIMessage, preserving the
|
|
158
|
+
* sequential interleaving of text, tool calls, and tool results.
|
|
159
|
+
*
|
|
160
|
+
* Walks parts in order. Text and tool-call parts accumulate into the
|
|
161
|
+
* current "segment". When a tool-result part is encountered, the
|
|
162
|
+
* current segment is flushed as an assistant message, then the tool
|
|
163
|
+
* result is emitted as a tool message.
|
|
164
|
+
*/
|
|
165
|
+
function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
|
|
166
|
+
const messageList: Array<ModelMessage> = []
|
|
167
|
+
let current = createSegment()
|
|
168
|
+
|
|
169
|
+
// Track emitted tool result IDs to avoid duplicates.
|
|
170
|
+
// A tool call can have BOTH an explicit tool-result part AND an output
|
|
171
|
+
// field on the tool-call part. We only want one per tool call ID.
|
|
172
|
+
const emittedToolResultIds = new Set<string>()
|
|
173
|
+
|
|
174
|
+
function flushSegment(): void {
|
|
175
|
+
const content = collapseContentParts(current.contentParts)
|
|
176
|
+
const hasContent = content !== null
|
|
177
|
+
const hasToolCalls = current.toolCalls.length > 0
|
|
178
|
+
|
|
179
|
+
if (hasContent || hasToolCalls) {
|
|
180
|
+
messageList.push({
|
|
181
|
+
role: 'assistant',
|
|
182
|
+
content,
|
|
183
|
+
...(hasToolCalls && { toolCalls: current.toolCalls }),
|
|
184
|
+
})
|
|
126
185
|
}
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
// Simple string content for text-only messages
|
|
130
|
-
content = textParts.map((p) => p.content).join('') || null
|
|
186
|
+
current = createSegment()
|
|
131
187
|
}
|
|
132
188
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
189
|
+
for (const part of uiMessage.parts) {
|
|
190
|
+
switch (part.type) {
|
|
191
|
+
case 'text':
|
|
192
|
+
case 'image':
|
|
193
|
+
case 'audio':
|
|
194
|
+
case 'video':
|
|
195
|
+
case 'document':
|
|
196
|
+
current.contentParts.push(part)
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
case 'tool-call':
|
|
200
|
+
if (isToolCallIncluded(part)) {
|
|
201
|
+
current.toolCalls.push({
|
|
202
|
+
id: part.id,
|
|
144
203
|
type: 'function' as const,
|
|
145
204
|
function: {
|
|
146
|
-
name:
|
|
147
|
-
arguments:
|
|
205
|
+
name: part.name,
|
|
206
|
+
arguments: part.arguments,
|
|
148
207
|
},
|
|
149
|
-
})
|
|
150
|
-
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
break
|
|
151
211
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (uiMessage.role !== 'assistant' || hasContent || !toolCalls) {
|
|
156
|
-
messageList.push({
|
|
157
|
-
role: uiMessage.role,
|
|
158
|
-
content,
|
|
159
|
-
...(toolCalls && toolCalls.length > 0 && { toolCalls }),
|
|
160
|
-
})
|
|
161
|
-
} else if (toolCalls.length > 0) {
|
|
162
|
-
// Assistant message with only tool calls
|
|
163
|
-
messageList.push({
|
|
164
|
-
role: 'assistant',
|
|
165
|
-
content,
|
|
166
|
-
toolCalls,
|
|
167
|
-
})
|
|
168
|
-
}
|
|
212
|
+
case 'tool-result':
|
|
213
|
+
// Flush the current assistant segment before emitting the tool result
|
|
214
|
+
flushSegment()
|
|
169
215
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
// Emit the tool result
|
|
217
|
+
if (
|
|
218
|
+
(part.state === 'complete' || part.state === 'error') &&
|
|
219
|
+
!emittedToolResultIds.has(part.toolCallId)
|
|
220
|
+
) {
|
|
221
|
+
messageList.push({
|
|
222
|
+
role: 'tool',
|
|
223
|
+
content: part.content,
|
|
224
|
+
toolCallId: part.toolCallId,
|
|
225
|
+
})
|
|
226
|
+
emittedToolResultIds.add(part.toolCallId)
|
|
227
|
+
}
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
// thinking parts are skipped - they're UI-only
|
|
231
|
+
default:
|
|
232
|
+
break
|
|
185
233
|
}
|
|
186
234
|
}
|
|
187
235
|
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
// Flush any remaining accumulated content
|
|
237
|
+
flushSegment()
|
|
238
|
+
|
|
239
|
+
// Emit tool results from client tool-call parts with output or approval,
|
|
240
|
+
// but only if not already covered by an explicit tool-result part above.
|
|
241
|
+
// These are appended at the end since they don't have explicit tool-result
|
|
242
|
+
// parts in the parts array to trigger inline emission.
|
|
243
|
+
for (const part of uiMessage.parts) {
|
|
244
|
+
if (part.type !== 'tool-call') continue
|
|
245
|
+
|
|
246
|
+
// Client tool with output - add as tool result (if not already emitted)
|
|
247
|
+
if (
|
|
248
|
+
part.output !== undefined &&
|
|
249
|
+
!part.approval &&
|
|
250
|
+
!emittedToolResultIds.has(part.id)
|
|
251
|
+
) {
|
|
193
252
|
messageList.push({
|
|
194
253
|
role: 'tool',
|
|
195
|
-
content: JSON.stringify(
|
|
196
|
-
toolCallId:
|
|
254
|
+
content: JSON.stringify(part.output),
|
|
255
|
+
toolCallId: part.id,
|
|
197
256
|
})
|
|
257
|
+
emittedToolResultIds.add(part.id)
|
|
198
258
|
}
|
|
199
259
|
|
|
200
260
|
// Approval response - add as tool result for iteration tracking
|
|
201
|
-
// For APPROVED: includes pendingExecution marker so the tool still executes
|
|
202
|
-
// For DENIED: just marks the tool as complete (no execution needed)
|
|
203
261
|
if (
|
|
204
|
-
|
|
205
|
-
|
|
262
|
+
part.state === 'approval-responded' &&
|
|
263
|
+
part.approval?.approved !== undefined &&
|
|
264
|
+
!emittedToolResultIds.has(part.id)
|
|
206
265
|
) {
|
|
207
|
-
const approved =
|
|
266
|
+
const approved = part.approval.approved
|
|
208
267
|
messageList.push({
|
|
209
268
|
role: 'tool',
|
|
210
269
|
content: JSON.stringify({
|
|
211
270
|
approved,
|
|
212
|
-
// Mark approved tools as pending execution - they still need to run
|
|
213
271
|
...(approved && { pendingExecution: true }),
|
|
214
272
|
message: approved
|
|
215
273
|
? 'User approved this action'
|
|
216
274
|
: 'User denied this action',
|
|
217
275
|
}),
|
|
218
|
-
toolCallId:
|
|
276
|
+
toolCallId: part.id,
|
|
219
277
|
})
|
|
278
|
+
emittedToolResultIds.add(part.id)
|
|
220
279
|
}
|
|
221
280
|
}
|
|
222
281
|
|
|
282
|
+
// If no messages were produced (e.g., empty parts), emit a minimal assistant message
|
|
283
|
+
if (messageList.length === 0) {
|
|
284
|
+
messageList.push({
|
|
285
|
+
role: 'assistant',
|
|
286
|
+
content: null,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
223
290
|
return messageList
|
|
224
291
|
}
|
|
225
292
|
|
|
@@ -241,13 +308,29 @@ export function modelMessageToUIMessage(
|
|
|
241
308
|
): UIMessage {
|
|
242
309
|
const parts: Array<MessagePart> = []
|
|
243
310
|
|
|
244
|
-
// Handle
|
|
245
|
-
|
|
246
|
-
if (
|
|
311
|
+
// Handle tool results (when role is "tool") - only produce tool-result part,
|
|
312
|
+
// not a text part (the content IS the tool result, not display text)
|
|
313
|
+
if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
|
|
247
314
|
parts.push({
|
|
248
|
-
type: '
|
|
249
|
-
|
|
315
|
+
type: 'tool-result',
|
|
316
|
+
toolCallId: modelMessage.toolCallId,
|
|
317
|
+
content: getTextContent(modelMessage.content),
|
|
318
|
+
state: 'complete',
|
|
250
319
|
})
|
|
320
|
+
} else if (Array.isArray(modelMessage.content)) {
|
|
321
|
+
// Multimodal content - preserve all content parts as MessageParts
|
|
322
|
+
for (const part of modelMessage.content) {
|
|
323
|
+
parts.push(part)
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
// String or null content
|
|
327
|
+
const textContent = getTextContent(modelMessage.content)
|
|
328
|
+
if (textContent) {
|
|
329
|
+
parts.push({
|
|
330
|
+
type: 'text',
|
|
331
|
+
content: textContent,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
251
334
|
}
|
|
252
335
|
|
|
253
336
|
// Handle tool calls
|
|
@@ -263,16 +346,6 @@ export function modelMessageToUIMessage(
|
|
|
263
346
|
}
|
|
264
347
|
}
|
|
265
348
|
|
|
266
|
-
// Handle tool results (when role is "tool")
|
|
267
|
-
if (modelMessage.role === 'tool' && modelMessage.toolCallId) {
|
|
268
|
-
parts.push({
|
|
269
|
-
type: 'tool-result',
|
|
270
|
-
toolCallId: modelMessage.toolCallId,
|
|
271
|
-
content: getTextContent(modelMessage.content),
|
|
272
|
-
state: 'complete',
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
349
|
return {
|
|
277
350
|
id: id || generateMessageId(),
|
|
278
351
|
role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,
|