@tanstack/ai 0.3.1 → 0.4.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.
@@ -7,7 +7,8 @@ export declare function convertMessagesToModelMessages(messages: Array<UIMessage
7
7
  * Convert a UIMessage to ModelMessage(s)
8
8
  *
9
9
  * This conversion handles the parts-based structure:
10
- * - Text parts → content field
10
+ * - Text parts → content field (string or as part of ContentPart array)
11
+ * - Multimodal parts (image, audio, video, document) → ContentPart array
11
12
  * - ToolCall parts → toolCalls array
12
13
  * - ToolResult parts → separate role="tool" messages
13
14
  *
@@ -1,3 +1,6 @@
1
+ function isMultimodalPart(part) {
2
+ return part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "document";
3
+ }
1
4
  function getTextContent(content) {
2
5
  if (content === null) {
3
6
  return "";
@@ -24,18 +27,34 @@ function uiMessageToModelMessages(uiMessage) {
24
27
  return messageList;
25
28
  }
26
29
  const textParts = [];
30
+ const multimodalParts = [];
27
31
  const toolCallParts = [];
28
32
  const toolResultParts = [];
29
33
  for (const part of uiMessage.parts) {
30
34
  if (part.type === "text") {
31
35
  textParts.push(part);
36
+ } else if (isMultimodalPart(part)) {
37
+ multimodalParts.push(part);
32
38
  } else if (part.type === "tool-call") {
33
39
  toolCallParts.push(part);
34
40
  } else if (part.type === "tool-result") {
35
41
  toolResultParts.push(part);
36
42
  }
37
43
  }
38
- const content = textParts.map((p) => p.content).join("") || null;
44
+ let content;
45
+ if (multimodalParts.length > 0) {
46
+ const contentParts = [];
47
+ for (const part of uiMessage.parts) {
48
+ if (part.type === "text") {
49
+ contentParts.push(part);
50
+ } else if (isMultimodalPart(part)) {
51
+ contentParts.push(part);
52
+ }
53
+ }
54
+ content = contentParts;
55
+ } else {
56
+ content = textParts.map((p) => p.content).join("") || null;
57
+ }
39
58
  const toolCalls = toolCallParts.length > 0 ? toolCallParts.filter(
40
59
  (p) => p.state === "input-complete" || p.state === "approval-responded" || p.output !== void 0
41
60
  // Include if has output (client tool result)
@@ -47,7 +66,8 @@ function uiMessageToModelMessages(uiMessage) {
47
66
  arguments: p.arguments
48
67
  }
49
68
  })) : void 0;
50
- if (uiMessage.role !== "assistant" || content || !toolCalls) {
69
+ const hasContent = Array.isArray(content) ? true : content !== null;
70
+ if (uiMessage.role !== "assistant" || hasContent || !toolCalls) {
51
71
  messageList.push({
52
72
  role: uiMessage.role,
53
73
  content,
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sources":["../../../../src/activities/chat/messages.ts"],"sourcesContent":["import type {\n ContentPart,\n MessagePart,\n ModelMessage,\n TextPart,\n ToolCallPart,\n ToolResultPart,\n UIMessage,\n} from '../../types'\n// ===========================\n// Message Converters\n// ===========================\n\n/**\n * Helper to extract text content from string or ContentPart array\n * For multimodal content, this extracts only the text parts\n */\nfunction getTextContent(content: string | null | Array<ContentPart>): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // Extract text from ContentPart array\n return content\n .filter((part) => part.type === 'text')\n .map((part) => part.content)\n .join('')\n}\n\n/**\n * Convert UIMessages or ModelMessages to ModelMessages\n */\nexport function convertMessagesToModelMessages(\n messages: Array<UIMessage | ModelMessage>,\n): Array<ModelMessage> {\n const modelMessages: Array<ModelMessage> = []\n for (const msg of messages) {\n if ('parts' in msg) {\n // UIMessage - convert to ModelMessages\n modelMessages.push(...uiMessageToModelMessages(msg))\n } else {\n // Already ModelMessage\n modelMessages.push(msg)\n }\n }\n return modelMessages\n}\n\n/**\n * Convert a UIMessage to ModelMessage(s)\n *\n * This conversion handles the parts-based structure:\n * - Text parts → content field\n * - ToolCall parts → toolCalls array\n * - ToolResult parts → separate role=\"tool\" messages\n *\n * @param uiMessage - The UIMessage to convert\n * @returns An array of ModelMessages (may be multiple if tool results are present)\n */\nexport function uiMessageToModelMessages(\n uiMessage: UIMessage,\n): Array<ModelMessage> {\n const messageList: Array<ModelMessage> = []\n\n // Skip system messages - they're handled via systemPrompts, not ModelMessages\n if (uiMessage.role === 'system') {\n return messageList\n }\n\n // Separate parts by type\n // Note: thinking parts are UI-only and not included in ModelMessages\n const textParts: Array<TextPart> = []\n const toolCallParts: Array<ToolCallPart> = []\n const toolResultParts: Array<ToolResultPart> = []\n\n for (const part of uiMessage.parts) {\n if (part.type === 'text') {\n textParts.push(part)\n } else if (part.type === 'tool-call') {\n toolCallParts.push(part)\n } else if (part.type === 'tool-result') {\n toolResultParts.push(part)\n }\n // thinking parts are skipped - they're UI-only\n }\n\n // Build the main message (user or assistant)\n const content = textParts.map((p) => p.content).join('') || null\n const toolCalls =\n toolCallParts.length > 0\n ? toolCallParts\n .filter(\n (p) =>\n p.state === 'input-complete' ||\n p.state === 'approval-responded' ||\n p.output !== undefined, // Include if has output (client tool result)\n )\n .map((p) => ({\n id: p.id,\n type: 'function' as const,\n function: {\n name: p.name,\n arguments: p.arguments,\n },\n }))\n : undefined\n\n // Create the main message\n if (uiMessage.role !== 'assistant' || content || !toolCalls) {\n messageList.push({\n role: uiMessage.role,\n content,\n ...(toolCalls && toolCalls.length > 0 && { toolCalls }),\n })\n } else if (toolCalls.length > 0) {\n // Assistant message with only tool calls\n messageList.push({\n role: 'assistant',\n content,\n toolCalls,\n })\n }\n\n // Add tool result messages for completed tool calls\n // This includes:\n // 1. Explicit tool-result parts (from server tools)\n // 2. Client tool calls with output set\n // 3. Approval-responded tool calls (approval result)\n for (const toolResultPart of toolResultParts) {\n if (\n toolResultPart.state === 'complete' ||\n toolResultPart.state === 'error'\n ) {\n messageList.push({\n role: 'tool',\n content: toolResultPart.content,\n toolCallId: toolResultPart.toolCallId,\n })\n }\n }\n\n // Add tool result messages for client tool results (tools with output)\n // and approval responses (so iteration tracking works correctly)\n for (const toolCallPart of toolCallParts) {\n // Client tool with output - add as tool result\n if (toolCallPart.output !== undefined && !toolCallPart.approval) {\n messageList.push({\n role: 'tool',\n content: JSON.stringify(toolCallPart.output),\n toolCallId: toolCallPart.id,\n })\n }\n\n // Approval response - add as tool result for iteration tracking\n // For APPROVED: includes pendingExecution marker so the tool still executes\n // For DENIED: just marks the tool as complete (no execution needed)\n if (\n toolCallPart.state === 'approval-responded' &&\n toolCallPart.approval?.approved !== undefined\n ) {\n const approved = toolCallPart.approval.approved\n messageList.push({\n role: 'tool',\n content: JSON.stringify({\n approved,\n // Mark approved tools as pending execution - they still need to run\n ...(approved && { pendingExecution: true }),\n message: approved\n ? 'User approved this action'\n : 'User denied this action',\n }),\n toolCallId: toolCallPart.id,\n })\n }\n }\n\n return messageList\n}\n\n/**\n * Convert a ModelMessage to UIMessage\n *\n * This conversion creates a parts-based structure:\n * - content field → TextPart\n * - toolCalls array → ToolCallPart[]\n * - role=\"tool\" messages should be converted separately and merged\n *\n * @param modelMessage - The ModelMessage to convert\n * @param id - Optional ID for the UIMessage (generated if not provided)\n * @returns A UIMessage with parts\n */\nexport function modelMessageToUIMessage(\n modelMessage: ModelMessage,\n id?: string,\n): UIMessage {\n const parts: Array<MessagePart> = []\n\n // Handle content (convert multimodal content to text for UI)\n const textContent = getTextContent(modelMessage.content)\n if (textContent) {\n parts.push({\n type: 'text',\n content: textContent,\n })\n }\n\n // Handle tool calls\n if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) {\n for (const toolCall of modelMessage.toolCalls) {\n parts.push({\n type: 'tool-call',\n id: toolCall.id,\n name: toolCall.function.name,\n arguments: toolCall.function.arguments,\n state: 'input-complete', // Model messages have complete arguments\n })\n }\n }\n\n // Handle tool results (when role is \"tool\")\n if (modelMessage.role === 'tool' && modelMessage.toolCallId) {\n parts.push({\n type: 'tool-result',\n toolCallId: modelMessage.toolCallId,\n content: getTextContent(modelMessage.content),\n state: 'complete',\n })\n }\n\n return {\n id: id || generateMessageId(),\n role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,\n parts,\n }\n}\n\n/**\n * Convert an array of ModelMessages to UIMessages\n *\n * This handles merging tool result messages with their corresponding assistant messages\n *\n * @param modelMessages - Array of ModelMessages to convert\n * @returns Array of UIMessages\n */\nexport function modelMessagesToUIMessages(\n modelMessages: Array<ModelMessage>,\n): Array<UIMessage> {\n const uiMessages: Array<UIMessage> = []\n let currentAssistantMessage: UIMessage | null = null\n\n for (const msg of modelMessages) {\n if (msg.role === 'tool') {\n // Tool result - merge into the last assistant message if possible\n if (\n currentAssistantMessage &&\n currentAssistantMessage.role === 'assistant'\n ) {\n currentAssistantMessage.parts.push({\n type: 'tool-result',\n toolCallId: msg.toolCallId!,\n content: getTextContent(msg.content),\n state: 'complete',\n })\n } else {\n // No assistant message to merge into, create a standalone one\n const toolResultUIMessage = modelMessageToUIMessage(msg)\n uiMessages.push(toolResultUIMessage)\n }\n } else {\n // Regular message\n const uiMessage = modelMessageToUIMessage(msg)\n uiMessages.push(uiMessage)\n\n // Track assistant messages for potential tool result merging\n if (msg.role === 'assistant') {\n currentAssistantMessage = uiMessage\n } else {\n currentAssistantMessage = null\n }\n }\n }\n\n return uiMessages\n}\n\n/**\n * Normalize a message (UIMessage or ModelMessage) to a UIMessage\n * Ensures the message has an ID and createdAt timestamp\n *\n * @param message - Either a UIMessage or ModelMessage\n * @param generateId - Function to generate a message ID if needed\n * @returns A UIMessage with guaranteed id and createdAt\n */\nexport function normalizeToUIMessage(\n message: UIMessage | ModelMessage,\n generateId: () => string,\n): UIMessage {\n if ('parts' in message) {\n // Already a UIMessage\n return {\n ...message,\n id: message.id || generateId(),\n createdAt: message.createdAt || new Date(),\n }\n } else {\n // ModelMessage - convert to UIMessage\n return {\n ...modelMessageToUIMessage(message, generateId()),\n createdAt: new Date(),\n }\n }\n}\n\n/**\n * Generate a unique message ID\n */\nexport function generateMessageId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"messages.js","sources":["../../../../src/activities/chat/messages.ts"],"sourcesContent":["import type {\n AudioPart,\n ContentPart,\n DocumentPart,\n ImagePart,\n MessagePart,\n ModelMessage,\n TextPart,\n ToolCallPart,\n ToolResultPart,\n UIMessage,\n VideoPart,\n} from '../../types'\n// ===========================\n// Message Converters\n// ===========================\n\n/**\n * Helper to check if a part is a multimodal content part (image, audio, video, document)\n */\nfunction isMultimodalPart(\n part: MessagePart,\n): part is ImagePart | AudioPart | VideoPart | DocumentPart {\n return (\n part.type === 'image' ||\n part.type === 'audio' ||\n part.type === 'video' ||\n part.type === 'document'\n )\n}\n\n/**\n * Helper to extract text content from string or ContentPart array\n * For multimodal content, this extracts only the text parts\n */\nfunction getTextContent(content: string | null | Array<ContentPart>): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // Extract text from ContentPart array\n return content\n .filter((part) => part.type === 'text')\n .map((part) => part.content)\n .join('')\n}\n\n/**\n * Convert UIMessages or ModelMessages to ModelMessages\n */\nexport function convertMessagesToModelMessages(\n messages: Array<UIMessage | ModelMessage>,\n): Array<ModelMessage> {\n const modelMessages: Array<ModelMessage> = []\n for (const msg of messages) {\n if ('parts' in msg) {\n // UIMessage - convert to ModelMessages\n modelMessages.push(...uiMessageToModelMessages(msg))\n } else {\n // Already ModelMessage\n modelMessages.push(msg)\n }\n }\n return modelMessages\n}\n\n/**\n * Convert a UIMessage to ModelMessage(s)\n *\n * This conversion handles the parts-based structure:\n * - Text parts → content field (string or as part of ContentPart array)\n * - Multimodal parts (image, audio, video, document) → ContentPart array\n * - ToolCall parts → toolCalls array\n * - ToolResult parts → separate role=\"tool\" messages\n *\n * @param uiMessage - The UIMessage to convert\n * @returns An array of ModelMessages (may be multiple if tool results are present)\n */\nexport function uiMessageToModelMessages(\n uiMessage: UIMessage,\n): Array<ModelMessage> {\n const messageList: Array<ModelMessage> = []\n\n // Skip system messages - they're handled via systemPrompts, not ModelMessages\n if (uiMessage.role === 'system') {\n return messageList\n }\n\n // Separate parts by type\n // Note: thinking parts are UI-only and not included in ModelMessages\n const textParts: Array<TextPart> = []\n const multimodalParts: Array<\n ImagePart | AudioPart | VideoPart | DocumentPart\n > = []\n const toolCallParts: Array<ToolCallPart> = []\n const toolResultParts: Array<ToolResultPart> = []\n\n for (const part of uiMessage.parts) {\n if (part.type === 'text') {\n textParts.push(part)\n } else if (isMultimodalPart(part)) {\n multimodalParts.push(part)\n } else if (part.type === 'tool-call') {\n toolCallParts.push(part)\n } else if (part.type === 'tool-result') {\n toolResultParts.push(part)\n }\n // thinking parts are skipped - they're UI-only\n }\n\n // Build the content field\n // If we have multimodal parts, use ContentPart array format\n // Otherwise, use simple string format for backward compatibility\n let content: string | null | Array<ContentPart>\n if (multimodalParts.length > 0) {\n // Build ContentPart array preserving the order of text and multimodal parts\n const contentParts: Array<ContentPart> = []\n for (const part of uiMessage.parts) {\n if (part.type === 'text') {\n contentParts.push(part)\n } else if (isMultimodalPart(part)) {\n contentParts.push(part)\n }\n }\n content = contentParts\n } else {\n // Simple string content for text-only messages\n content = textParts.map((p) => p.content).join('') || null\n }\n\n const toolCalls =\n toolCallParts.length > 0\n ? toolCallParts\n .filter(\n (p) =>\n p.state === 'input-complete' ||\n p.state === 'approval-responded' ||\n p.output !== undefined, // Include if has output (client tool result)\n )\n .map((p) => ({\n id: p.id,\n type: 'function' as const,\n function: {\n name: p.name,\n arguments: p.arguments,\n },\n }))\n : undefined\n\n // Create the main message\n // For multimodal content, we always create a message even if content is an empty array\n const hasContent = Array.isArray(content) ? true : content !== null\n if (uiMessage.role !== 'assistant' || hasContent || !toolCalls) {\n messageList.push({\n role: uiMessage.role,\n content,\n ...(toolCalls && toolCalls.length > 0 && { toolCalls }),\n })\n } else if (toolCalls.length > 0) {\n // Assistant message with only tool calls\n messageList.push({\n role: 'assistant',\n content,\n toolCalls,\n })\n }\n\n // Add tool result messages for completed tool calls\n // This includes:\n // 1. Explicit tool-result parts (from server tools)\n // 2. Client tool calls with output set\n // 3. Approval-responded tool calls (approval result)\n for (const toolResultPart of toolResultParts) {\n if (\n toolResultPart.state === 'complete' ||\n toolResultPart.state === 'error'\n ) {\n messageList.push({\n role: 'tool',\n content: toolResultPart.content,\n toolCallId: toolResultPart.toolCallId,\n })\n }\n }\n\n // Add tool result messages for client tool results (tools with output)\n // and approval responses (so iteration tracking works correctly)\n for (const toolCallPart of toolCallParts) {\n // Client tool with output - add as tool result\n if (toolCallPart.output !== undefined && !toolCallPart.approval) {\n messageList.push({\n role: 'tool',\n content: JSON.stringify(toolCallPart.output),\n toolCallId: toolCallPart.id,\n })\n }\n\n // Approval response - add as tool result for iteration tracking\n // For APPROVED: includes pendingExecution marker so the tool still executes\n // For DENIED: just marks the tool as complete (no execution needed)\n if (\n toolCallPart.state === 'approval-responded' &&\n toolCallPart.approval?.approved !== undefined\n ) {\n const approved = toolCallPart.approval.approved\n messageList.push({\n role: 'tool',\n content: JSON.stringify({\n approved,\n // Mark approved tools as pending execution - they still need to run\n ...(approved && { pendingExecution: true }),\n message: approved\n ? 'User approved this action'\n : 'User denied this action',\n }),\n toolCallId: toolCallPart.id,\n })\n }\n }\n\n return messageList\n}\n\n/**\n * Convert a ModelMessage to UIMessage\n *\n * This conversion creates a parts-based structure:\n * - content field → TextPart\n * - toolCalls array → ToolCallPart[]\n * - role=\"tool\" messages should be converted separately and merged\n *\n * @param modelMessage - The ModelMessage to convert\n * @param id - Optional ID for the UIMessage (generated if not provided)\n * @returns A UIMessage with parts\n */\nexport function modelMessageToUIMessage(\n modelMessage: ModelMessage,\n id?: string,\n): UIMessage {\n const parts: Array<MessagePart> = []\n\n // Handle content (convert multimodal content to text for UI)\n const textContent = getTextContent(modelMessage.content)\n if (textContent) {\n parts.push({\n type: 'text',\n content: textContent,\n })\n }\n\n // Handle tool calls\n if (modelMessage.toolCalls && modelMessage.toolCalls.length > 0) {\n for (const toolCall of modelMessage.toolCalls) {\n parts.push({\n type: 'tool-call',\n id: toolCall.id,\n name: toolCall.function.name,\n arguments: toolCall.function.arguments,\n state: 'input-complete', // Model messages have complete arguments\n })\n }\n }\n\n // Handle tool results (when role is \"tool\")\n if (modelMessage.role === 'tool' && modelMessage.toolCallId) {\n parts.push({\n type: 'tool-result',\n toolCallId: modelMessage.toolCallId,\n content: getTextContent(modelMessage.content),\n state: 'complete',\n })\n }\n\n return {\n id: id || generateMessageId(),\n role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,\n parts,\n }\n}\n\n/**\n * Convert an array of ModelMessages to UIMessages\n *\n * This handles merging tool result messages with their corresponding assistant messages\n *\n * @param modelMessages - Array of ModelMessages to convert\n * @returns Array of UIMessages\n */\nexport function modelMessagesToUIMessages(\n modelMessages: Array<ModelMessage>,\n): Array<UIMessage> {\n const uiMessages: Array<UIMessage> = []\n let currentAssistantMessage: UIMessage | null = null\n\n for (const msg of modelMessages) {\n if (msg.role === 'tool') {\n // Tool result - merge into the last assistant message if possible\n if (\n currentAssistantMessage &&\n currentAssistantMessage.role === 'assistant'\n ) {\n currentAssistantMessage.parts.push({\n type: 'tool-result',\n toolCallId: msg.toolCallId!,\n content: getTextContent(msg.content),\n state: 'complete',\n })\n } else {\n // No assistant message to merge into, create a standalone one\n const toolResultUIMessage = modelMessageToUIMessage(msg)\n uiMessages.push(toolResultUIMessage)\n }\n } else {\n // Regular message\n const uiMessage = modelMessageToUIMessage(msg)\n uiMessages.push(uiMessage)\n\n // Track assistant messages for potential tool result merging\n if (msg.role === 'assistant') {\n currentAssistantMessage = uiMessage\n } else {\n currentAssistantMessage = null\n }\n }\n }\n\n return uiMessages\n}\n\n/**\n * Normalize a message (UIMessage or ModelMessage) to a UIMessage\n * Ensures the message has an ID and createdAt timestamp\n *\n * @param message - Either a UIMessage or ModelMessage\n * @param generateId - Function to generate a message ID if needed\n * @returns A UIMessage with guaranteed id and createdAt\n */\nexport function normalizeToUIMessage(\n message: UIMessage | ModelMessage,\n generateId: () => string,\n): UIMessage {\n if ('parts' in message) {\n // Already a UIMessage\n return {\n ...message,\n id: message.id || generateId(),\n createdAt: message.createdAt || new Date(),\n }\n } else {\n // ModelMessage - convert to UIMessage\n return {\n ...modelMessageToUIMessage(message, generateId()),\n createdAt: new Date(),\n }\n }\n}\n\n/**\n * Generate a unique message ID\n */\nexport function generateMessageId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ChunkRecording, ChunkStrategy, ProcessorResult, ProcessorState, ToolCallState } from './types.js';
2
- import { ModelMessage, StreamChunk, UIMessage } from '../../../types.js';
2
+ import { ContentPart, ModelMessage, StreamChunk, UIMessage } from '../../../types.js';
3
3
  /**
4
4
  * Events emitted by the StreamProcessor
5
5
  */
@@ -79,9 +79,29 @@ export declare class StreamProcessor {
79
79
  */
80
80
  setMessages(messages: Array<UIMessage>): void;
81
81
  /**
82
- * Add a user message to the conversation
83
- */
84
- addUserMessage(content: string): UIMessage;
82
+ * Add a user message to the conversation.
83
+ * Supports both simple string content and multimodal content arrays.
84
+ *
85
+ * @param content - The message content (string or array of content parts)
86
+ * @param id - Optional custom message ID (generated if not provided)
87
+ * @returns The created UIMessage
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * // Simple text message
92
+ * processor.addUserMessage('Hello!')
93
+ *
94
+ * // Multimodal message with image
95
+ * processor.addUserMessage([
96
+ * { type: 'text', content: 'What is in this image?' },
97
+ * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
98
+ * ])
99
+ *
100
+ * // With custom ID
101
+ * processor.addUserMessage('Hello!', 'custom-id-123')
102
+ * ```
103
+ */
104
+ addUserMessage(content: string | Array<ContentPart>, id?: string): UIMessage;
85
105
  /**
86
106
  * Start streaming a new assistant message
87
107
  * Returns the message ID
@@ -36,13 +36,36 @@ class StreamProcessor {
36
36
  this.emitMessagesChange();
37
37
  }
38
38
  /**
39
- * Add a user message to the conversation
40
- */
41
- addUserMessage(content) {
39
+ * Add a user message to the conversation.
40
+ * Supports both simple string content and multimodal content arrays.
41
+ *
42
+ * @param content - The message content (string or array of content parts)
43
+ * @param id - Optional custom message ID (generated if not provided)
44
+ * @returns The created UIMessage
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * // Simple text message
49
+ * processor.addUserMessage('Hello!')
50
+ *
51
+ * // Multimodal message with image
52
+ * processor.addUserMessage([
53
+ * { type: 'text', content: 'What is in this image?' },
54
+ * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
55
+ * ])
56
+ *
57
+ * // With custom ID
58
+ * processor.addUserMessage('Hello!', 'custom-id-123')
59
+ * ```
60
+ */
61
+ addUserMessage(content, id) {
62
+ const parts = typeof content === "string" ? [{ type: "text", content }] : content.map((part) => {
63
+ return part;
64
+ });
42
65
  const userMessage = {
43
- id: generateMessageId(),
66
+ id: id ?? generateMessageId(),
44
67
  role: "user",
45
- parts: [{ type: "text", content }],
68
+ parts,
46
69
  createdAt: /* @__PURE__ */ new Date()
47
70
  };
48
71
  this.messages = [...this.messages, userMessage];
@@ -1 +1 @@
1
- {"version":3,"file":"processor.js","sources":["../../../../../src/activities/chat/stream/processor.ts"],"sourcesContent":["/**\n * Unified Stream Processor\n *\n * Core stream processing engine that manages the full UIMessage[] conversation.\n * Single source of truth for message state.\n *\n * Handles:\n * - Full conversation management (UIMessage[])\n * - Text content accumulation with configurable chunking strategies\n * - Parallel tool calls with lifecycle state tracking\n * - Tool results and approval flows\n * - Thinking/reasoning content\n * - Recording/replay for testing\n * - Event-driven architecture for UI updates\n */\nimport { generateMessageId, uiMessageToModelMessages } from '../messages.js'\nimport { defaultJSONParser } from './json-parser'\nimport {\n updateTextPart,\n updateThinkingPart,\n updateToolCallApproval,\n updateToolCallApprovalResponse,\n updateToolCallPart,\n updateToolCallWithOutput,\n updateToolResultPart,\n} from './message-updaters'\nimport { ImmediateStrategy } from './strategies'\nimport type {\n ChunkRecording,\n ChunkStrategy,\n InternalToolCallState,\n ProcessorResult,\n ProcessorState,\n ToolCallState,\n ToolResultState,\n} from './types'\nimport type {\n ModelMessage,\n StreamChunk,\n ToolCall,\n ToolCallPart,\n ToolResultPart,\n UIMessage,\n} from '../../../types'\n\n/**\n * Events emitted by the StreamProcessor\n */\nexport interface StreamProcessorEvents {\n // State events - full array on any change\n onMessagesChange?: (messages: Array<UIMessage>) => void\n\n // Lifecycle events\n onStreamStart?: () => void\n onStreamEnd?: (message: UIMessage) => void\n onError?: (error: Error) => void\n\n // Interaction events - client must handle these\n onToolCall?: (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => void\n onApprovalRequest?: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => void\n\n // Granular events for UI optimization (character-by-character, state tracking)\n onTextUpdate?: (messageId: string, content: string) => void\n onToolCallStateChange?: (\n messageId: string,\n toolCallId: string,\n state: ToolCallState,\n args: string,\n ) => void\n onThinkingUpdate?: (messageId: string, content: string) => void\n}\n\n/**\n * Options for StreamProcessor\n */\nexport interface StreamProcessorOptions {\n chunkStrategy?: ChunkStrategy\n /** Event-driven handlers */\n events?: StreamProcessorEvents\n jsonParser?: {\n parse: (jsonString: string) => any\n }\n /** Enable recording for replay testing */\n recording?: boolean\n /** Initial messages to populate the processor */\n initialMessages?: Array<UIMessage>\n}\n\n/**\n * StreamProcessor - State machine for processing AI response streams\n *\n * Manages the full UIMessage[] conversation and emits events on changes.\n *\n * State tracking:\n * - Full message array\n * - Current assistant message being streamed\n * - Text content accumulation\n * - Multiple parallel tool calls\n * - Tool call completion detection\n *\n * Tool call completion is detected when:\n * 1. A new tool call starts at a different index\n * 2. Text content arrives\n * 3. Stream ends\n */\nexport class StreamProcessor {\n private chunkStrategy: ChunkStrategy\n private events: StreamProcessorEvents\n private jsonParser: { parse: (jsonString: string) => any }\n private recordingEnabled: boolean\n\n // Message state\n private messages: Array<UIMessage> = []\n private currentAssistantMessageId: string | null = null\n\n // Stream state for current assistant message\n // Total accumulated text across all segments (for the final result)\n private totalTextContent = ''\n // Current segment's text content (for onTextUpdate callbacks)\n private currentSegmentText = ''\n private lastEmittedText = ''\n private thinkingContent = ''\n private toolCalls: Map<string, InternalToolCallState> = new Map()\n private toolCallOrder: Array<string> = []\n private finishReason: string | null = null\n private isDone = false\n // Track if we've had tool calls since the last text segment started\n private hasToolCallsSinceTextStart = false\n\n // Recording\n private recording: ChunkRecording | null = null\n private recordingStartTime = 0\n\n constructor(options: StreamProcessorOptions = {}) {\n this.chunkStrategy = options.chunkStrategy || new ImmediateStrategy()\n this.events = options.events || {}\n this.jsonParser = options.jsonParser || defaultJSONParser\n this.recordingEnabled = options.recording ?? false\n\n // Initialize with provided messages\n if (options.initialMessages) {\n this.messages = [...options.initialMessages]\n }\n }\n\n // ============================================\n // Message Management Methods\n // ============================================\n\n /**\n * Set the messages array (e.g., from persisted state)\n */\n setMessages(messages: Array<UIMessage>): void {\n this.messages = [...messages]\n this.emitMessagesChange()\n }\n\n /**\n * Add a user message to the conversation\n */\n addUserMessage(content: string): UIMessage {\n const userMessage: UIMessage = {\n id: generateMessageId(),\n role: 'user',\n parts: [{ type: 'text', content }],\n createdAt: new Date(),\n }\n\n this.messages = [...this.messages, userMessage]\n this.emitMessagesChange()\n\n return userMessage\n }\n\n /**\n * Start streaming a new assistant message\n * Returns the message ID\n */\n startAssistantMessage(): string {\n // Reset stream state for new message\n this.resetStreamState()\n\n const assistantMessage: UIMessage = {\n id: generateMessageId(),\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n\n this.currentAssistantMessageId = assistantMessage.id\n this.messages = [...this.messages, assistantMessage]\n\n // Emit events\n this.events.onStreamStart?.()\n this.emitMessagesChange()\n\n return assistantMessage.id\n }\n\n /**\n * Add a tool result (called by client after handling onToolCall)\n */\n addToolResult(toolCallId: string, output: any, error?: string): void {\n // Find the message containing this tool call\n const messageWithToolCall = this.messages.find((msg) =>\n msg.parts.some(\n (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCallId,\n ),\n )\n\n if (!messageWithToolCall) {\n console.warn(\n `[StreamProcessor] Could not find message with tool call ${toolCallId}`,\n )\n return\n }\n\n // Step 1: Update the tool-call part's output field (for UI rendering)\n let updatedMessages = updateToolCallWithOutput(\n this.messages,\n toolCallId,\n output,\n error ? 'input-complete' : undefined,\n error,\n )\n\n // Step 2: Create a tool-result part (for LLM conversation history)\n const content = typeof output === 'string' ? output : JSON.stringify(output)\n const toolResultState: ToolResultState = error ? 'error' : 'complete'\n\n updatedMessages = updateToolResultPart(\n updatedMessages,\n messageWithToolCall.id,\n toolCallId,\n content,\n toolResultState,\n error,\n )\n\n this.messages = updatedMessages\n this.emitMessagesChange()\n }\n\n /**\n * Add an approval response (called by client after handling onApprovalRequest)\n */\n addToolApprovalResponse(approvalId: string, approved: boolean): void {\n this.messages = updateToolCallApprovalResponse(\n this.messages,\n approvalId,\n approved,\n )\n this.emitMessagesChange()\n }\n\n /**\n * Get the conversation as ModelMessages (for sending to LLM)\n */\n toModelMessages(): Array<ModelMessage> {\n const modelMessages: Array<ModelMessage> = []\n for (const msg of this.messages) {\n modelMessages.push(...uiMessageToModelMessages(msg))\n }\n return modelMessages\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.messages\n }\n\n /**\n * Check if all tool calls in the last assistant message are complete\n * Useful for auto-continue logic\n */\n areAllToolsComplete(): boolean {\n const lastAssistant = this.messages.findLast(\n (m: UIMessage) => m.role === 'assistant',\n )\n\n if (!lastAssistant) return true\n\n const toolParts = lastAssistant.parts.filter(\n (p): p is ToolCallPart => p.type === 'tool-call',\n )\n\n if (toolParts.length === 0) return true\n\n // Get tool result parts to check for server tool completion\n const toolResultIds = new Set(\n lastAssistant.parts\n .filter((p): p is ToolResultPart => p.type === 'tool-result')\n .map((p) => p.toolCallId),\n )\n\n // All tool calls must be in a terminal state\n // A tool call is complete if:\n // 1. It was approved/denied (approval-responded state)\n // 2. It has an output field set (client tool completed via addToolResult)\n // 3. It has a corresponding tool-result part (server tool completed)\n return toolParts.every(\n (part) =>\n part.state === 'approval-responded' ||\n (part.output !== undefined && !part.approval) ||\n toolResultIds.has(part.id),\n )\n }\n\n /**\n * Remove messages after a certain index (for reload/retry)\n */\n removeMessagesAfter(index: number): void {\n this.messages = this.messages.slice(0, index + 1)\n this.emitMessagesChange()\n }\n\n /**\n * Clear all messages\n */\n clearMessages(): void {\n this.messages = []\n this.currentAssistantMessageId = null\n this.emitMessagesChange()\n }\n\n // ============================================\n // Stream Processing Methods\n // ============================================\n\n /**\n * Process a stream and emit events through handlers\n */\n async process(stream: AsyncIterable<any>): Promise<ProcessorResult> {\n // Reset stream state (but keep messages)\n this.resetStreamState()\n\n // Start recording if enabled\n if (this.recordingEnabled) {\n this.startRecording()\n }\n\n // Process each chunk\n for await (const chunk of stream) {\n this.processChunk(chunk)\n }\n\n // Stream ended - finalize everything\n this.finalizeStream()\n\n // Finalize recording\n if (this.recording) {\n this.recording.result = this.getResult()\n }\n\n return this.getResult()\n }\n\n /**\n * Process a single chunk from the stream\n */\n processChunk(chunk: StreamChunk): void {\n // Record chunk if enabled\n if (this.recording) {\n this.recording.chunks.push({\n chunk,\n timestamp: Date.now(),\n index: this.recording.chunks.length,\n })\n }\n\n switch (chunk.type) {\n // AG-UI Events\n case 'TEXT_MESSAGE_CONTENT':\n this.handleTextMessageContentEvent(chunk)\n break\n\n case 'TOOL_CALL_START':\n this.handleToolCallStartEvent(chunk)\n break\n\n case 'TOOL_CALL_ARGS':\n this.handleToolCallArgsEvent(chunk)\n break\n\n case 'TOOL_CALL_END':\n this.handleToolCallEndEvent(chunk)\n break\n\n case 'RUN_FINISHED':\n this.handleRunFinishedEvent(chunk)\n break\n\n case 'RUN_ERROR':\n this.handleRunErrorEvent(chunk)\n break\n\n case 'STEP_FINISHED':\n this.handleStepFinishedEvent(chunk)\n break\n\n case 'CUSTOM':\n this.handleCustomEvent(chunk)\n break\n\n default:\n // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,\n // STATE_SNAPSHOT, STATE_DELTA - no special handling needed\n break\n }\n }\n\n /**\n * Handle TEXT_MESSAGE_CONTENT event\n */\n private handleTextMessageContentEvent(\n chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,\n ): void {\n // Content arriving means all current tool calls are complete\n this.completeAllToolCalls()\n\n const previousSegment = this.currentSegmentText\n\n // Detect if this is a NEW text segment (after tool calls) vs continuation\n const isNewSegment =\n this.hasToolCallsSinceTextStart &&\n previousSegment.length > 0 &&\n this.isNewTextSegment(chunk, previousSegment)\n\n if (isNewSegment) {\n // Emit any accumulated text before starting new segment\n if (previousSegment !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n // Reset SEGMENT text accumulation for the new text segment after tool calls\n this.currentSegmentText = ''\n this.lastEmittedText = ''\n this.hasToolCallsSinceTextStart = false\n }\n\n const currentText = this.currentSegmentText\n let nextText = currentText\n\n // Prefer delta over content - delta is the incremental change\n if (chunk.delta !== '') {\n nextText = currentText + chunk.delta\n } else if (chunk.content && chunk.content !== '') {\n // Fallback: use content if delta is not provided\n if (chunk.content.startsWith(currentText)) {\n nextText = chunk.content\n } else if (currentText.startsWith(chunk.content)) {\n nextText = currentText\n } else {\n nextText = currentText + chunk.content\n }\n }\n\n // Calculate the delta for totalTextContent\n const textDelta = nextText.slice(currentText.length)\n this.currentSegmentText = nextText\n this.totalTextContent += textDelta\n\n // Use delta for chunk strategy if available\n const chunkPortion = chunk.delta || chunk.content || ''\n const shouldEmit = this.chunkStrategy.shouldEmit(\n chunkPortion,\n this.currentSegmentText,\n )\n if (shouldEmit && this.currentSegmentText !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n }\n\n /**\n * Handle TOOL_CALL_START event\n */\n private handleToolCallStartEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_START' }>,\n ): void {\n // Mark that we've seen tool calls since the last text segment\n this.hasToolCallsSinceTextStart = true\n\n const toolCallId = chunk.toolCallId\n const existingToolCall = this.toolCalls.get(toolCallId)\n\n if (!existingToolCall) {\n // New tool call starting\n const initialState: ToolCallState = 'awaiting-input'\n\n const newToolCall: InternalToolCallState = {\n id: chunk.toolCallId,\n name: chunk.toolName,\n arguments: '',\n state: initialState,\n parsedArguments: undefined,\n index: chunk.index ?? this.toolCalls.size,\n }\n\n this.toolCalls.set(toolCallId, newToolCall)\n this.toolCallOrder.push(toolCallId)\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: chunk.toolCallId,\n name: chunk.toolName,\n arguments: '',\n state: initialState,\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n chunk.toolCallId,\n initialState,\n '',\n )\n }\n }\n }\n\n /**\n * Handle TOOL_CALL_ARGS event\n */\n private handleToolCallArgsEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_ARGS' }>,\n ): void {\n const toolCallId = chunk.toolCallId\n const existingToolCall = this.toolCalls.get(toolCallId)\n\n if (existingToolCall) {\n const wasAwaitingInput = existingToolCall.state === 'awaiting-input'\n\n // Accumulate arguments from delta\n existingToolCall.arguments += chunk.delta || ''\n\n // Update state\n if (wasAwaitingInput && chunk.delta) {\n existingToolCall.state = 'input-streaming'\n }\n\n // Try to parse the updated arguments\n existingToolCall.parsedArguments = this.jsonParser.parse(\n existingToolCall.arguments,\n )\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: existingToolCall.id,\n name: existingToolCall.name,\n arguments: existingToolCall.arguments,\n state: existingToolCall.state,\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n existingToolCall.id,\n existingToolCall.state,\n existingToolCall.arguments,\n )\n }\n }\n }\n\n /**\n * Handle TOOL_CALL_END event\n */\n private handleToolCallEndEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_END' }>,\n ): void {\n const state: ToolResultState = 'complete'\n\n // Update UIMessage if we have a current assistant message\n if (this.currentAssistantMessageId && chunk.result) {\n this.messages = updateToolResultPart(\n this.messages,\n this.currentAssistantMessageId,\n chunk.toolCallId,\n chunk.result,\n state,\n )\n this.emitMessagesChange()\n }\n }\n\n /**\n * Handle RUN_FINISHED event\n */\n private handleRunFinishedEvent(\n chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,\n ): void {\n this.finishReason = chunk.finishReason\n this.isDone = true\n this.completeAllToolCalls()\n }\n\n /**\n * Handle RUN_ERROR event\n */\n private handleRunErrorEvent(\n chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,\n ): void {\n // Emit error event\n this.events.onError?.(new Error(chunk.error.message || 'An error occurred'))\n }\n\n /**\n * Handle STEP_FINISHED event (for thinking/reasoning content)\n */\n private handleStepFinishedEvent(\n chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,\n ): void {\n const previous = this.thinkingContent\n let nextThinking = previous\n\n // Prefer delta over content\n if (chunk.delta && chunk.delta !== '') {\n nextThinking = previous + chunk.delta\n } else if (chunk.content && chunk.content !== '') {\n if (chunk.content.startsWith(previous)) {\n nextThinking = chunk.content\n } else if (previous.startsWith(chunk.content)) {\n nextThinking = previous\n } else {\n nextThinking = previous + chunk.content\n }\n }\n\n this.thinkingContent = nextThinking\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateThinkingPart(\n this.messages,\n this.currentAssistantMessageId,\n this.thinkingContent,\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onThinkingUpdate?.(\n this.currentAssistantMessageId,\n this.thinkingContent,\n )\n }\n }\n\n /**\n * Handle CUSTOM event\n * Handles special custom events like 'tool-input-available' for client-side tool execution\n * and 'approval-requested' for tool approval flows\n */\n private handleCustomEvent(\n chunk: Extract<StreamChunk, { type: 'CUSTOM' }>,\n ): void {\n // Handle client tool input availability - trigger client-side execution\n if (chunk.name === 'tool-input-available' && chunk.data) {\n const { toolCallId, toolName, input } = chunk.data as {\n toolCallId: string\n toolName: string\n input: any\n }\n\n // Emit onToolCall event for the client to execute the tool\n this.events.onToolCall?.({\n toolCallId,\n toolName,\n input,\n })\n }\n\n // Handle approval requests\n if (chunk.name === 'approval-requested' && chunk.data) {\n const { toolCallId, toolName, input, approval } = chunk.data as {\n toolCallId: string\n toolName: string\n input: any\n approval: { id: string; needsApproval: boolean }\n }\n\n // Update the tool call part with approval state\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallApproval(\n this.messages,\n this.currentAssistantMessageId,\n toolCallId,\n approval.id,\n )\n this.emitMessagesChange()\n }\n\n // Emit approval request event\n this.events.onApprovalRequest?.({\n toolCallId,\n toolName,\n input,\n approvalId: approval.id,\n })\n }\n }\n\n /**\n * Detect if an incoming content chunk represents a NEW text segment\n */\n private isNewTextSegment(\n chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,\n previous: string,\n ): boolean {\n // Check if content is present (delta is always defined but may be empty string)\n if (chunk.content !== undefined) {\n if (chunk.content.length < previous.length) {\n return true\n }\n if (\n !chunk.content.startsWith(previous) &&\n !previous.startsWith(chunk.content)\n ) {\n return true\n }\n }\n return false\n }\n\n /**\n * Complete all tool calls\n */\n private completeAllToolCalls(): void {\n this.toolCalls.forEach((toolCall, id) => {\n if (toolCall.state !== 'input-complete') {\n const index = this.toolCallOrder.indexOf(id)\n this.completeToolCall(index, toolCall)\n }\n })\n }\n\n /**\n * Mark a tool call as complete and emit event\n */\n private completeToolCall(\n _index: number,\n toolCall: InternalToolCallState,\n ): void {\n toolCall.state = 'input-complete'\n\n // Try final parse\n toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments)\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: toolCall.id,\n name: toolCall.name,\n arguments: toolCall.arguments,\n state: 'input-complete',\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n toolCall.id,\n 'input-complete',\n toolCall.arguments,\n )\n }\n }\n\n /**\n * Emit pending text update\n */\n private emitTextUpdate(): void {\n this.lastEmittedText = this.currentSegmentText\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateTextPart(\n this.messages,\n this.currentAssistantMessageId,\n this.currentSegmentText,\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onTextUpdate?.(\n this.currentAssistantMessageId,\n this.currentSegmentText,\n )\n }\n }\n\n /**\n * Emit messages change event\n */\n private emitMessagesChange(): void {\n this.events.onMessagesChange?.([...this.messages])\n }\n\n /**\n * Finalize the stream - complete all pending operations\n */\n finalizeStream(): void {\n // Complete any remaining tool calls\n this.completeAllToolCalls()\n\n // Emit any pending text if not already emitted\n if (this.currentSegmentText !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n\n // Emit stream end event\n if (this.currentAssistantMessageId) {\n const assistantMessage = this.messages.find(\n (m) => m.id === this.currentAssistantMessageId,\n )\n if (assistantMessage) {\n this.events.onStreamEnd?.(assistantMessage)\n }\n }\n }\n\n /**\n * Get completed tool calls in API format\n */\n private getCompletedToolCalls(): Array<ToolCall> {\n return Array.from(this.toolCalls.values())\n .filter((tc) => tc.state === 'input-complete')\n .map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n }))\n }\n\n /**\n * Get current result\n */\n private getResult(): ProcessorResult {\n const toolCalls = this.getCompletedToolCalls()\n return {\n content: this.totalTextContent,\n thinking: this.thinkingContent || undefined,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n finishReason: this.finishReason,\n }\n }\n\n /**\n * Get current processor state\n */\n getState(): ProcessorState {\n return {\n content: this.totalTextContent,\n thinking: this.thinkingContent,\n toolCalls: new Map(this.toolCalls),\n toolCallOrder: [...this.toolCallOrder],\n finishReason: this.finishReason,\n done: this.isDone,\n }\n }\n\n /**\n * Start recording chunks\n */\n startRecording(): void {\n this.recordingEnabled = true\n this.recordingStartTime = Date.now()\n this.recording = {\n version: '1.0',\n timestamp: this.recordingStartTime,\n chunks: [],\n }\n }\n\n /**\n * Get the current recording\n */\n getRecording(): ChunkRecording | null {\n return this.recording\n }\n\n /**\n * Reset stream state (but keep messages)\n */\n private resetStreamState(): void {\n this.totalTextContent = ''\n this.currentSegmentText = ''\n this.lastEmittedText = ''\n this.thinkingContent = ''\n this.toolCalls.clear()\n this.toolCallOrder = []\n this.finishReason = null\n this.isDone = false\n this.hasToolCallsSinceTextStart = false\n this.chunkStrategy.reset?.()\n }\n\n /**\n * Full reset (including messages)\n */\n reset(): void {\n this.resetStreamState()\n this.messages = []\n this.currentAssistantMessageId = null\n }\n\n /**\n * Replay a recording through the processor\n */\n static async replay(\n recording: ChunkRecording,\n options?: StreamProcessorOptions,\n ): Promise<ProcessorResult> {\n const processor = new StreamProcessor(options)\n return processor.process(createReplayStream(recording))\n }\n}\n\n/**\n * Create an async iterable from a recording\n */\nexport function createReplayStream(\n recording: ChunkRecording,\n): AsyncIterable<StreamChunk> {\n return {\n // eslint-disable-next-line @typescript-eslint/require-await\n async *[Symbol.asyncIterator]() {\n for (const { chunk } of recording.chunks) {\n yield chunk\n }\n },\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"processor.js","sources":["../../../../../src/activities/chat/stream/processor.ts"],"sourcesContent":["/**\n * Unified Stream Processor\n *\n * Core stream processing engine that manages the full UIMessage[] conversation.\n * Single source of truth for message state.\n *\n * Handles:\n * - Full conversation management (UIMessage[])\n * - Text content accumulation with configurable chunking strategies\n * - Parallel tool calls with lifecycle state tracking\n * - Tool results and approval flows\n * - Thinking/reasoning content\n * - Recording/replay for testing\n * - Event-driven architecture for UI updates\n */\nimport { generateMessageId, uiMessageToModelMessages } from '../messages.js'\nimport { defaultJSONParser } from './json-parser'\nimport {\n updateTextPart,\n updateThinkingPart,\n updateToolCallApproval,\n updateToolCallApprovalResponse,\n updateToolCallPart,\n updateToolCallWithOutput,\n updateToolResultPart,\n} from './message-updaters'\nimport { ImmediateStrategy } from './strategies'\nimport type {\n ChunkRecording,\n ChunkStrategy,\n InternalToolCallState,\n ProcessorResult,\n ProcessorState,\n ToolCallState,\n ToolResultState,\n} from './types'\nimport type {\n ContentPart,\n MessagePart,\n ModelMessage,\n StreamChunk,\n ToolCall,\n ToolCallPart,\n ToolResultPart,\n UIMessage,\n} from '../../../types'\n\n/**\n * Events emitted by the StreamProcessor\n */\nexport interface StreamProcessorEvents {\n // State events - full array on any change\n onMessagesChange?: (messages: Array<UIMessage>) => void\n\n // Lifecycle events\n onStreamStart?: () => void\n onStreamEnd?: (message: UIMessage) => void\n onError?: (error: Error) => void\n\n // Interaction events - client must handle these\n onToolCall?: (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => void\n onApprovalRequest?: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => void\n\n // Granular events for UI optimization (character-by-character, state tracking)\n onTextUpdate?: (messageId: string, content: string) => void\n onToolCallStateChange?: (\n messageId: string,\n toolCallId: string,\n state: ToolCallState,\n args: string,\n ) => void\n onThinkingUpdate?: (messageId: string, content: string) => void\n}\n\n/**\n * Options for StreamProcessor\n */\nexport interface StreamProcessorOptions {\n chunkStrategy?: ChunkStrategy\n /** Event-driven handlers */\n events?: StreamProcessorEvents\n jsonParser?: {\n parse: (jsonString: string) => any\n }\n /** Enable recording for replay testing */\n recording?: boolean\n /** Initial messages to populate the processor */\n initialMessages?: Array<UIMessage>\n}\n\n/**\n * StreamProcessor - State machine for processing AI response streams\n *\n * Manages the full UIMessage[] conversation and emits events on changes.\n *\n * State tracking:\n * - Full message array\n * - Current assistant message being streamed\n * - Text content accumulation\n * - Multiple parallel tool calls\n * - Tool call completion detection\n *\n * Tool call completion is detected when:\n * 1. A new tool call starts at a different index\n * 2. Text content arrives\n * 3. Stream ends\n */\nexport class StreamProcessor {\n private chunkStrategy: ChunkStrategy\n private events: StreamProcessorEvents\n private jsonParser: { parse: (jsonString: string) => any }\n private recordingEnabled: boolean\n\n // Message state\n private messages: Array<UIMessage> = []\n private currentAssistantMessageId: string | null = null\n\n // Stream state for current assistant message\n // Total accumulated text across all segments (for the final result)\n private totalTextContent = ''\n // Current segment's text content (for onTextUpdate callbacks)\n private currentSegmentText = ''\n private lastEmittedText = ''\n private thinkingContent = ''\n private toolCalls: Map<string, InternalToolCallState> = new Map()\n private toolCallOrder: Array<string> = []\n private finishReason: string | null = null\n private isDone = false\n // Track if we've had tool calls since the last text segment started\n private hasToolCallsSinceTextStart = false\n\n // Recording\n private recording: ChunkRecording | null = null\n private recordingStartTime = 0\n\n constructor(options: StreamProcessorOptions = {}) {\n this.chunkStrategy = options.chunkStrategy || new ImmediateStrategy()\n this.events = options.events || {}\n this.jsonParser = options.jsonParser || defaultJSONParser\n this.recordingEnabled = options.recording ?? false\n\n // Initialize with provided messages\n if (options.initialMessages) {\n this.messages = [...options.initialMessages]\n }\n }\n\n // ============================================\n // Message Management Methods\n // ============================================\n\n /**\n * Set the messages array (e.g., from persisted state)\n */\n setMessages(messages: Array<UIMessage>): void {\n this.messages = [...messages]\n this.emitMessagesChange()\n }\n\n /**\n * Add a user message to the conversation.\n * Supports both simple string content and multimodal content arrays.\n *\n * @param content - The message content (string or array of content parts)\n * @param id - Optional custom message ID (generated if not provided)\n * @returns The created UIMessage\n *\n * @example\n * ```ts\n * // Simple text message\n * processor.addUserMessage('Hello!')\n *\n * // Multimodal message with image\n * processor.addUserMessage([\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ])\n *\n * // With custom ID\n * processor.addUserMessage('Hello!', 'custom-id-123')\n * ```\n */\n addUserMessage(content: string | Array<ContentPart>, id?: string): UIMessage {\n // Convert content to message parts\n const parts: Array<MessagePart> =\n typeof content === 'string'\n ? [{ type: 'text', content }]\n : content.map((part) => {\n // ContentPart types (text, image, audio, video, document) are compatible with MessagePart\n return part as MessagePart\n })\n\n const userMessage: UIMessage = {\n id: id ?? generateMessageId(),\n role: 'user',\n parts,\n createdAt: new Date(),\n }\n\n this.messages = [...this.messages, userMessage]\n this.emitMessagesChange()\n\n return userMessage\n }\n\n /**\n * Start streaming a new assistant message\n * Returns the message ID\n */\n startAssistantMessage(): string {\n // Reset stream state for new message\n this.resetStreamState()\n\n const assistantMessage: UIMessage = {\n id: generateMessageId(),\n role: 'assistant',\n parts: [],\n createdAt: new Date(),\n }\n\n this.currentAssistantMessageId = assistantMessage.id\n this.messages = [...this.messages, assistantMessage]\n\n // Emit events\n this.events.onStreamStart?.()\n this.emitMessagesChange()\n\n return assistantMessage.id\n }\n\n /**\n * Add a tool result (called by client after handling onToolCall)\n */\n addToolResult(toolCallId: string, output: any, error?: string): void {\n // Find the message containing this tool call\n const messageWithToolCall = this.messages.find((msg) =>\n msg.parts.some(\n (p): p is ToolCallPart => p.type === 'tool-call' && p.id === toolCallId,\n ),\n )\n\n if (!messageWithToolCall) {\n console.warn(\n `[StreamProcessor] Could not find message with tool call ${toolCallId}`,\n )\n return\n }\n\n // Step 1: Update the tool-call part's output field (for UI rendering)\n let updatedMessages = updateToolCallWithOutput(\n this.messages,\n toolCallId,\n output,\n error ? 'input-complete' : undefined,\n error,\n )\n\n // Step 2: Create a tool-result part (for LLM conversation history)\n const content = typeof output === 'string' ? output : JSON.stringify(output)\n const toolResultState: ToolResultState = error ? 'error' : 'complete'\n\n updatedMessages = updateToolResultPart(\n updatedMessages,\n messageWithToolCall.id,\n toolCallId,\n content,\n toolResultState,\n error,\n )\n\n this.messages = updatedMessages\n this.emitMessagesChange()\n }\n\n /**\n * Add an approval response (called by client after handling onApprovalRequest)\n */\n addToolApprovalResponse(approvalId: string, approved: boolean): void {\n this.messages = updateToolCallApprovalResponse(\n this.messages,\n approvalId,\n approved,\n )\n this.emitMessagesChange()\n }\n\n /**\n * Get the conversation as ModelMessages (for sending to LLM)\n */\n toModelMessages(): Array<ModelMessage> {\n const modelMessages: Array<ModelMessage> = []\n for (const msg of this.messages) {\n modelMessages.push(...uiMessageToModelMessages(msg))\n }\n return modelMessages\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.messages\n }\n\n /**\n * Check if all tool calls in the last assistant message are complete\n * Useful for auto-continue logic\n */\n areAllToolsComplete(): boolean {\n const lastAssistant = this.messages.findLast(\n (m: UIMessage) => m.role === 'assistant',\n )\n\n if (!lastAssistant) return true\n\n const toolParts = lastAssistant.parts.filter(\n (p): p is ToolCallPart => p.type === 'tool-call',\n )\n\n if (toolParts.length === 0) return true\n\n // Get tool result parts to check for server tool completion\n const toolResultIds = new Set(\n lastAssistant.parts\n .filter((p): p is ToolResultPart => p.type === 'tool-result')\n .map((p) => p.toolCallId),\n )\n\n // All tool calls must be in a terminal state\n // A tool call is complete if:\n // 1. It was approved/denied (approval-responded state)\n // 2. It has an output field set (client tool completed via addToolResult)\n // 3. It has a corresponding tool-result part (server tool completed)\n return toolParts.every(\n (part) =>\n part.state === 'approval-responded' ||\n (part.output !== undefined && !part.approval) ||\n toolResultIds.has(part.id),\n )\n }\n\n /**\n * Remove messages after a certain index (for reload/retry)\n */\n removeMessagesAfter(index: number): void {\n this.messages = this.messages.slice(0, index + 1)\n this.emitMessagesChange()\n }\n\n /**\n * Clear all messages\n */\n clearMessages(): void {\n this.messages = []\n this.currentAssistantMessageId = null\n this.emitMessagesChange()\n }\n\n // ============================================\n // Stream Processing Methods\n // ============================================\n\n /**\n * Process a stream and emit events through handlers\n */\n async process(stream: AsyncIterable<any>): Promise<ProcessorResult> {\n // Reset stream state (but keep messages)\n this.resetStreamState()\n\n // Start recording if enabled\n if (this.recordingEnabled) {\n this.startRecording()\n }\n\n // Process each chunk\n for await (const chunk of stream) {\n this.processChunk(chunk)\n }\n\n // Stream ended - finalize everything\n this.finalizeStream()\n\n // Finalize recording\n if (this.recording) {\n this.recording.result = this.getResult()\n }\n\n return this.getResult()\n }\n\n /**\n * Process a single chunk from the stream\n */\n processChunk(chunk: StreamChunk): void {\n // Record chunk if enabled\n if (this.recording) {\n this.recording.chunks.push({\n chunk,\n timestamp: Date.now(),\n index: this.recording.chunks.length,\n })\n }\n\n switch (chunk.type) {\n // AG-UI Events\n case 'TEXT_MESSAGE_CONTENT':\n this.handleTextMessageContentEvent(chunk)\n break\n\n case 'TOOL_CALL_START':\n this.handleToolCallStartEvent(chunk)\n break\n\n case 'TOOL_CALL_ARGS':\n this.handleToolCallArgsEvent(chunk)\n break\n\n case 'TOOL_CALL_END':\n this.handleToolCallEndEvent(chunk)\n break\n\n case 'RUN_FINISHED':\n this.handleRunFinishedEvent(chunk)\n break\n\n case 'RUN_ERROR':\n this.handleRunErrorEvent(chunk)\n break\n\n case 'STEP_FINISHED':\n this.handleStepFinishedEvent(chunk)\n break\n\n case 'CUSTOM':\n this.handleCustomEvent(chunk)\n break\n\n default:\n // RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_END, STEP_STARTED,\n // STATE_SNAPSHOT, STATE_DELTA - no special handling needed\n break\n }\n }\n\n /**\n * Handle TEXT_MESSAGE_CONTENT event\n */\n private handleTextMessageContentEvent(\n chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,\n ): void {\n // Content arriving means all current tool calls are complete\n this.completeAllToolCalls()\n\n const previousSegment = this.currentSegmentText\n\n // Detect if this is a NEW text segment (after tool calls) vs continuation\n const isNewSegment =\n this.hasToolCallsSinceTextStart &&\n previousSegment.length > 0 &&\n this.isNewTextSegment(chunk, previousSegment)\n\n if (isNewSegment) {\n // Emit any accumulated text before starting new segment\n if (previousSegment !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n // Reset SEGMENT text accumulation for the new text segment after tool calls\n this.currentSegmentText = ''\n this.lastEmittedText = ''\n this.hasToolCallsSinceTextStart = false\n }\n\n const currentText = this.currentSegmentText\n let nextText = currentText\n\n // Prefer delta over content - delta is the incremental change\n if (chunk.delta !== '') {\n nextText = currentText + chunk.delta\n } else if (chunk.content && chunk.content !== '') {\n // Fallback: use content if delta is not provided\n if (chunk.content.startsWith(currentText)) {\n nextText = chunk.content\n } else if (currentText.startsWith(chunk.content)) {\n nextText = currentText\n } else {\n nextText = currentText + chunk.content\n }\n }\n\n // Calculate the delta for totalTextContent\n const textDelta = nextText.slice(currentText.length)\n this.currentSegmentText = nextText\n this.totalTextContent += textDelta\n\n // Use delta for chunk strategy if available\n const chunkPortion = chunk.delta || chunk.content || ''\n const shouldEmit = this.chunkStrategy.shouldEmit(\n chunkPortion,\n this.currentSegmentText,\n )\n if (shouldEmit && this.currentSegmentText !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n }\n\n /**\n * Handle TOOL_CALL_START event\n */\n private handleToolCallStartEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_START' }>,\n ): void {\n // Mark that we've seen tool calls since the last text segment\n this.hasToolCallsSinceTextStart = true\n\n const toolCallId = chunk.toolCallId\n const existingToolCall = this.toolCalls.get(toolCallId)\n\n if (!existingToolCall) {\n // New tool call starting\n const initialState: ToolCallState = 'awaiting-input'\n\n const newToolCall: InternalToolCallState = {\n id: chunk.toolCallId,\n name: chunk.toolName,\n arguments: '',\n state: initialState,\n parsedArguments: undefined,\n index: chunk.index ?? this.toolCalls.size,\n }\n\n this.toolCalls.set(toolCallId, newToolCall)\n this.toolCallOrder.push(toolCallId)\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: chunk.toolCallId,\n name: chunk.toolName,\n arguments: '',\n state: initialState,\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n chunk.toolCallId,\n initialState,\n '',\n )\n }\n }\n }\n\n /**\n * Handle TOOL_CALL_ARGS event\n */\n private handleToolCallArgsEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_ARGS' }>,\n ): void {\n const toolCallId = chunk.toolCallId\n const existingToolCall = this.toolCalls.get(toolCallId)\n\n if (existingToolCall) {\n const wasAwaitingInput = existingToolCall.state === 'awaiting-input'\n\n // Accumulate arguments from delta\n existingToolCall.arguments += chunk.delta || ''\n\n // Update state\n if (wasAwaitingInput && chunk.delta) {\n existingToolCall.state = 'input-streaming'\n }\n\n // Try to parse the updated arguments\n existingToolCall.parsedArguments = this.jsonParser.parse(\n existingToolCall.arguments,\n )\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: existingToolCall.id,\n name: existingToolCall.name,\n arguments: existingToolCall.arguments,\n state: existingToolCall.state,\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n existingToolCall.id,\n existingToolCall.state,\n existingToolCall.arguments,\n )\n }\n }\n }\n\n /**\n * Handle TOOL_CALL_END event\n */\n private handleToolCallEndEvent(\n chunk: Extract<StreamChunk, { type: 'TOOL_CALL_END' }>,\n ): void {\n const state: ToolResultState = 'complete'\n\n // Update UIMessage if we have a current assistant message\n if (this.currentAssistantMessageId && chunk.result) {\n this.messages = updateToolResultPart(\n this.messages,\n this.currentAssistantMessageId,\n chunk.toolCallId,\n chunk.result,\n state,\n )\n this.emitMessagesChange()\n }\n }\n\n /**\n * Handle RUN_FINISHED event\n */\n private handleRunFinishedEvent(\n chunk: Extract<StreamChunk, { type: 'RUN_FINISHED' }>,\n ): void {\n this.finishReason = chunk.finishReason\n this.isDone = true\n this.completeAllToolCalls()\n }\n\n /**\n * Handle RUN_ERROR event\n */\n private handleRunErrorEvent(\n chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,\n ): void {\n // Emit error event\n this.events.onError?.(new Error(chunk.error.message || 'An error occurred'))\n }\n\n /**\n * Handle STEP_FINISHED event (for thinking/reasoning content)\n */\n private handleStepFinishedEvent(\n chunk: Extract<StreamChunk, { type: 'STEP_FINISHED' }>,\n ): void {\n const previous = this.thinkingContent\n let nextThinking = previous\n\n // Prefer delta over content\n if (chunk.delta && chunk.delta !== '') {\n nextThinking = previous + chunk.delta\n } else if (chunk.content && chunk.content !== '') {\n if (chunk.content.startsWith(previous)) {\n nextThinking = chunk.content\n } else if (previous.startsWith(chunk.content)) {\n nextThinking = previous\n } else {\n nextThinking = previous + chunk.content\n }\n }\n\n this.thinkingContent = nextThinking\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateThinkingPart(\n this.messages,\n this.currentAssistantMessageId,\n this.thinkingContent,\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onThinkingUpdate?.(\n this.currentAssistantMessageId,\n this.thinkingContent,\n )\n }\n }\n\n /**\n * Handle CUSTOM event\n * Handles special custom events like 'tool-input-available' for client-side tool execution\n * and 'approval-requested' for tool approval flows\n */\n private handleCustomEvent(\n chunk: Extract<StreamChunk, { type: 'CUSTOM' }>,\n ): void {\n // Handle client tool input availability - trigger client-side execution\n if (chunk.name === 'tool-input-available' && chunk.data) {\n const { toolCallId, toolName, input } = chunk.data as {\n toolCallId: string\n toolName: string\n input: any\n }\n\n // Emit onToolCall event for the client to execute the tool\n this.events.onToolCall?.({\n toolCallId,\n toolName,\n input,\n })\n }\n\n // Handle approval requests\n if (chunk.name === 'approval-requested' && chunk.data) {\n const { toolCallId, toolName, input, approval } = chunk.data as {\n toolCallId: string\n toolName: string\n input: any\n approval: { id: string; needsApproval: boolean }\n }\n\n // Update the tool call part with approval state\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallApproval(\n this.messages,\n this.currentAssistantMessageId,\n toolCallId,\n approval.id,\n )\n this.emitMessagesChange()\n }\n\n // Emit approval request event\n this.events.onApprovalRequest?.({\n toolCallId,\n toolName,\n input,\n approvalId: approval.id,\n })\n }\n }\n\n /**\n * Detect if an incoming content chunk represents a NEW text segment\n */\n private isNewTextSegment(\n chunk: Extract<StreamChunk, { type: 'TEXT_MESSAGE_CONTENT' }>,\n previous: string,\n ): boolean {\n // Check if content is present (delta is always defined but may be empty string)\n if (chunk.content !== undefined) {\n if (chunk.content.length < previous.length) {\n return true\n }\n if (\n !chunk.content.startsWith(previous) &&\n !previous.startsWith(chunk.content)\n ) {\n return true\n }\n }\n return false\n }\n\n /**\n * Complete all tool calls\n */\n private completeAllToolCalls(): void {\n this.toolCalls.forEach((toolCall, id) => {\n if (toolCall.state !== 'input-complete') {\n const index = this.toolCallOrder.indexOf(id)\n this.completeToolCall(index, toolCall)\n }\n })\n }\n\n /**\n * Mark a tool call as complete and emit event\n */\n private completeToolCall(\n _index: number,\n toolCall: InternalToolCallState,\n ): void {\n toolCall.state = 'input-complete'\n\n // Try final parse\n toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments)\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateToolCallPart(\n this.messages,\n this.currentAssistantMessageId,\n {\n id: toolCall.id,\n name: toolCall.name,\n arguments: toolCall.arguments,\n state: 'input-complete',\n },\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onToolCallStateChange?.(\n this.currentAssistantMessageId,\n toolCall.id,\n 'input-complete',\n toolCall.arguments,\n )\n }\n }\n\n /**\n * Emit pending text update\n */\n private emitTextUpdate(): void {\n this.lastEmittedText = this.currentSegmentText\n\n // Update UIMessage\n if (this.currentAssistantMessageId) {\n this.messages = updateTextPart(\n this.messages,\n this.currentAssistantMessageId,\n this.currentSegmentText,\n )\n this.emitMessagesChange()\n\n // Emit granular event\n this.events.onTextUpdate?.(\n this.currentAssistantMessageId,\n this.currentSegmentText,\n )\n }\n }\n\n /**\n * Emit messages change event\n */\n private emitMessagesChange(): void {\n this.events.onMessagesChange?.([...this.messages])\n }\n\n /**\n * Finalize the stream - complete all pending operations\n */\n finalizeStream(): void {\n // Complete any remaining tool calls\n this.completeAllToolCalls()\n\n // Emit any pending text if not already emitted\n if (this.currentSegmentText !== this.lastEmittedText) {\n this.emitTextUpdate()\n }\n\n // Emit stream end event\n if (this.currentAssistantMessageId) {\n const assistantMessage = this.messages.find(\n (m) => m.id === this.currentAssistantMessageId,\n )\n if (assistantMessage) {\n this.events.onStreamEnd?.(assistantMessage)\n }\n }\n }\n\n /**\n * Get completed tool calls in API format\n */\n private getCompletedToolCalls(): Array<ToolCall> {\n return Array.from(this.toolCalls.values())\n .filter((tc) => tc.state === 'input-complete')\n .map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n }))\n }\n\n /**\n * Get current result\n */\n private getResult(): ProcessorResult {\n const toolCalls = this.getCompletedToolCalls()\n return {\n content: this.totalTextContent,\n thinking: this.thinkingContent || undefined,\n toolCalls: toolCalls.length > 0 ? toolCalls : undefined,\n finishReason: this.finishReason,\n }\n }\n\n /**\n * Get current processor state\n */\n getState(): ProcessorState {\n return {\n content: this.totalTextContent,\n thinking: this.thinkingContent,\n toolCalls: new Map(this.toolCalls),\n toolCallOrder: [...this.toolCallOrder],\n finishReason: this.finishReason,\n done: this.isDone,\n }\n }\n\n /**\n * Start recording chunks\n */\n startRecording(): void {\n this.recordingEnabled = true\n this.recordingStartTime = Date.now()\n this.recording = {\n version: '1.0',\n timestamp: this.recordingStartTime,\n chunks: [],\n }\n }\n\n /**\n * Get the current recording\n */\n getRecording(): ChunkRecording | null {\n return this.recording\n }\n\n /**\n * Reset stream state (but keep messages)\n */\n private resetStreamState(): void {\n this.totalTextContent = ''\n this.currentSegmentText = ''\n this.lastEmittedText = ''\n this.thinkingContent = ''\n this.toolCalls.clear()\n this.toolCallOrder = []\n this.finishReason = null\n this.isDone = false\n this.hasToolCallsSinceTextStart = false\n this.chunkStrategy.reset?.()\n }\n\n /**\n * Full reset (including messages)\n */\n reset(): void {\n this.resetStreamState()\n this.messages = []\n this.currentAssistantMessageId = null\n }\n\n /**\n * Replay a recording through the processor\n */\n static async replay(\n recording: ChunkRecording,\n options?: StreamProcessorOptions,\n ): Promise<ProcessorResult> {\n const processor = new StreamProcessor(options)\n return processor.process(createReplayStream(recording))\n }\n}\n\n/**\n * Create an async iterable from a recording\n */\nexport function createReplayStream(\n recording: ChunkRecording,\n): AsyncIterable<StreamChunk> {\n return {\n // eslint-disable-next-line @typescript-eslint/require-await\n async *[Symbol.asyncIterator]() {\n for (const { chunk } of recording.chunks) {\n yield chunk\n }\n },\n }\n}\n"],"names":[],"mappings":"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"}
@@ -72,7 +72,8 @@ class ToolCallManager {
72
72
  try {
73
73
  let args;
74
74
  try {
75
- args = JSON.parse(toolCall.function.arguments);
75
+ const argsString = toolCall.function.arguments.trim() || "{}";
76
+ args = JSON.parse(argsString === "null" ? "{}" : argsString);
76
77
  } catch (parseError) {
77
78
  throw new Error(
78
79
  `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`
@@ -151,8 +152,9 @@ async function executeToolCalls(toolCalls, tools, approvals = /* @__PURE__ */ ne
151
152
  continue;
152
153
  }
153
154
  let input = {};
154
- const argsStr = toolCall.function.arguments.trim() || "{}";
155
- {
155
+ let argsStr = toolCall.function.arguments.trim() || "{}";
156
+ if (argsStr === "null") argsStr = "{}";
157
+ if (argsStr) {
156
158
  try {
157
159
  input = JSON.parse(argsStr);
158
160
  } catch (parseError) {
@@ -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\n let args: unknown\n try {\n args = JSON.parse(toolCall.function.arguments)\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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"}
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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"}
@@ -12,6 +12,7 @@ export { streamToText, toServerSentEventsStream, toServerSentEventsResponse, toH
12
12
  export { ToolCallManager } from './activities/chat/tools/tool-calls.js';
13
13
  export { maxIterations, untilFinishReason, combineStrategies, } from './activities/chat/agent-loop-strategies.js';
14
14
  export * from './types.js';
15
+ export { detectImageMimeType } from './utils.js';
15
16
  export * from './event-client.js';
16
17
  export { convertMessagesToModelMessages, generateMessageId, uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, } from './activities/chat/messages.js';
17
18
  export { StreamProcessor, createReplayStream, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, PartialJSONParser, defaultJSONParser, parsePartialJSON, } from './activities/chat/stream/index.js';
package/dist/esm/index.js CHANGED
@@ -9,6 +9,7 @@ import { convertSchemaToJsonSchema } from "./activities/chat/tools/schema-conver
9
9
  import { streamToText, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream } from "./stream-to-response.js";
10
10
  import { ToolCallManager } from "./activities/chat/tools/tool-calls.js";
11
11
  import { combineStrategies, maxIterations, untilFinishReason } from "./activities/chat/agent-loop-strategies.js";
12
+ import { detectImageMimeType } from "./utils.js";
12
13
  import { aiEventClient } from "./event-client.js";
13
14
  import { convertMessagesToModelMessages, generateMessageId, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeToUIMessage, uiMessageToModelMessages } from "./activities/chat/messages.js";
14
15
  import { StreamProcessor, createReplayStream } from "./activities/chat/stream/processor.js";
@@ -36,6 +37,7 @@ export {
36
37
  createTranscriptionOptions,
37
38
  createVideoOptions,
38
39
  defaultJSONParser,
40
+ detectImageMimeType,
39
41
  generateImage,
40
42
  generateMessageId,
41
43
  generateSpeech,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
@@ -86,23 +86,49 @@ export interface ToolCall {
86
86
  */
87
87
  export type Modality = 'text' | 'image' | 'audio' | 'video' | 'document';
88
88
  /**
89
- * Source specification for multimodal content.
90
- * Supports both inline data (base64) and URL-based content.
89
+ * Source specification for inline data content (base64).
90
+ * Requires a mimeType to ensure providers receive proper content type information.
91
+ */
92
+ export interface ContentPartDataSource {
93
+ /**
94
+ * Indicates this is inline data content.
95
+ */
96
+ type: 'data';
97
+ /**
98
+ * The base64-encoded content value.
99
+ */
100
+ value: string;
101
+ /**
102
+ * The MIME type of the content (e.g., 'image/png', 'audio/wav').
103
+ * Required for data sources to ensure proper handling by providers.
104
+ */
105
+ mimeType: string;
106
+ }
107
+ /**
108
+ * Source specification for URL-based content.
109
+ * mimeType is optional as it can often be inferred from the URL or response headers.
91
110
  */
92
- export interface ContentPartSource {
111
+ export interface ContentPartUrlSource {
93
112
  /**
94
- * The type of source:
95
- * - 'data': Inline data (typically base64 encoded)
96
- * - 'url': URL reference to the content
113
+ * Indicates this is URL-referenced content.
97
114
  */
98
- type: 'data' | 'url';
115
+ type: 'url';
99
116
  /**
100
- * The actual content value:
101
- * - For 'data': base64-encoded string
102
- * - For 'url': HTTP(S) URL or data URI
117
+ * HTTP(S) URL or data URI pointing to the content.
103
118
  */
104
119
  value: string;
120
+ /**
121
+ * Optional MIME type hint for cases where providers can't infer it from the URL.
122
+ */
123
+ mimeType?: string;
105
124
  }
125
+ /**
126
+ * Source specification for multimodal content.
127
+ * Discriminated union supporting both inline data (base64) and URL-based content.
128
+ * - For 'data' sources: mimeType is required
129
+ * - For 'url' sources: mimeType is optional
130
+ */
131
+ export type ContentPartSource = ContentPartDataSource | ContentPartUrlSource;
106
132
  /**
107
133
  * Image content part for multimodal messages.
108
134
  * @template TMetadata - Provider-specific metadata type (e.g., OpenAI's detail level)
@@ -213,7 +239,7 @@ export interface ThinkingPart {
213
239
  type: 'thinking';
214
240
  content: string;
215
241
  }
216
- export type MessagePart = TextPart | ToolCallPart | ToolResultPart | ThinkingPart;
242
+ export type MessagePart = TextPart | ImagePart | AudioPart | VideoPart | DocumentPart | ToolCallPart | ToolResultPart | ThinkingPart;
217
243
  /**
218
244
  * UIMessage - Domain-specific message format optimized for building chat UIs
219
245
  * Contains parts that can be text, tool calls, or tool results
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Detect image mime type from base64 data using magic bytes.
3
+ * Returns undefined if the format cannot be detected.
4
+ *
5
+ * This function analyzes the first few bytes of base64-encoded image data
6
+ * to determine the image format based on file signature (magic bytes).
7
+ *
8
+ * @param base64Data - The base64-encoded image data
9
+ * @returns The detected mime type, or undefined if unrecognized
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const mimeType = detectImageMimeType(imageBase64)
14
+ * // Returns 'image/jpeg', 'image/png', 'image/gif', 'image/webp', or undefined
15
+ * ```
16
+ */
17
+ export declare function detectImageMimeType(base64Data: string): 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | undefined;
@@ -0,0 +1,20 @@
1
+ function detectImageMimeType(base64Data) {
2
+ const prefix = base64Data.substring(0, 20);
3
+ if (prefix.startsWith("/9j/")) {
4
+ return "image/jpeg";
5
+ }
6
+ if (prefix.startsWith("iVBORw0KGgo")) {
7
+ return "image/png";
8
+ }
9
+ if (prefix.startsWith("R0lGOD")) {
10
+ return "image/gif";
11
+ }
12
+ if (prefix.startsWith("UklGR")) {
13
+ return "image/webp";
14
+ }
15
+ return void 0;
16
+ }
17
+ export {
18
+ detectImageMimeType
19
+ };
20
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Detect image mime type from base64 data using magic bytes.\n * Returns undefined if the format cannot be detected.\n *\n * This function analyzes the first few bytes of base64-encoded image data\n * to determine the image format based on file signature (magic bytes).\n *\n * @param base64Data - The base64-encoded image data\n * @returns The detected mime type, or undefined if unrecognized\n *\n * @example\n * ```ts\n * const mimeType = detectImageMimeType(imageBase64)\n * // Returns 'image/jpeg', 'image/png', 'image/gif', 'image/webp', or undefined\n * ```\n */\nexport function detectImageMimeType(\n base64Data: string,\n): 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | undefined {\n // Get first few bytes (base64 encoded)\n const prefix = base64Data.substring(0, 20)\n\n // JPEG: starts with /9j/ (FFD8FF in base64)\n if (prefix.startsWith('/9j/')) {\n return 'image/jpeg'\n }\n // PNG: starts with iVBORw0KGgo (89504E47 in base64)\n if (prefix.startsWith('iVBORw0KGgo')) {\n return 'image/png'\n }\n // GIF: starts with R0lGOD (474946 in base64)\n if (prefix.startsWith('R0lGOD')) {\n return 'image/gif'\n }\n // WebP: starts with UklGR (52494646 in base64, followed by WEBP)\n if (prefix.startsWith('UklGR')) {\n return 'image/webp'\n }\n\n return undefined\n}\n"],"names":[],"mappings":"AAgBO,SAAS,oBACd,YACqE;AAErE,QAAM,SAAS,WAAW,UAAU,GAAG,EAAE;AAGzC,MAAI,OAAO,WAAW,MAAM,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,WAAW,aAAa,GAAG;AACpC,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,WAAW,QAAQ,GAAG;AAC/B,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,WAAW,OAAO,GAAG;AAC9B,WAAO;AAAA,EACT;AAEA,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Core TanStack AI library - Open source AI SDK",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -1,16 +1,34 @@
1
1
  import type {
2
+ AudioPart,
2
3
  ContentPart,
4
+ DocumentPart,
5
+ ImagePart,
3
6
  MessagePart,
4
7
  ModelMessage,
5
8
  TextPart,
6
9
  ToolCallPart,
7
10
  ToolResultPart,
8
11
  UIMessage,
12
+ VideoPart,
9
13
  } from '../../types'
10
14
  // ===========================
11
15
  // Message Converters
12
16
  // ===========================
13
17
 
18
+ /**
19
+ * Helper to check if a part is a multimodal content part (image, audio, video, document)
20
+ */
21
+ function isMultimodalPart(
22
+ part: MessagePart,
23
+ ): part is ImagePart | AudioPart | VideoPart | DocumentPart {
24
+ return (
25
+ part.type === 'image' ||
26
+ part.type === 'audio' ||
27
+ part.type === 'video' ||
28
+ part.type === 'document'
29
+ )
30
+ }
31
+
14
32
  /**
15
33
  * Helper to extract text content from string or ContentPart array
16
34
  * For multimodal content, this extracts only the text parts
@@ -52,7 +70,8 @@ export function convertMessagesToModelMessages(
52
70
  * Convert a UIMessage to ModelMessage(s)
53
71
  *
54
72
  * This conversion handles the parts-based structure:
55
- * - Text parts → content field
73
+ * - Text parts → content field (string or as part of ContentPart array)
74
+ * - Multimodal parts (image, audio, video, document) → ContentPart array
56
75
  * - ToolCall parts → toolCalls array
57
76
  * - ToolResult parts → separate role="tool" messages
58
77
  *
@@ -72,12 +91,17 @@ export function uiMessageToModelMessages(
72
91
  // Separate parts by type
73
92
  // Note: thinking parts are UI-only and not included in ModelMessages
74
93
  const textParts: Array<TextPart> = []
94
+ const multimodalParts: Array<
95
+ ImagePart | AudioPart | VideoPart | DocumentPart
96
+ > = []
75
97
  const toolCallParts: Array<ToolCallPart> = []
76
98
  const toolResultParts: Array<ToolResultPart> = []
77
99
 
78
100
  for (const part of uiMessage.parts) {
79
101
  if (part.type === 'text') {
80
102
  textParts.push(part)
103
+ } else if (isMultimodalPart(part)) {
104
+ multimodalParts.push(part)
81
105
  } else if (part.type === 'tool-call') {
82
106
  toolCallParts.push(part)
83
107
  } else if (part.type === 'tool-result') {
@@ -86,8 +110,26 @@ export function uiMessageToModelMessages(
86
110
  // thinking parts are skipped - they're UI-only
87
111
  }
88
112
 
89
- // Build the main message (user or assistant)
90
- const content = textParts.map((p) => p.content).join('') || null
113
+ // Build the content field
114
+ // If we have multimodal parts, use ContentPart array format
115
+ // Otherwise, use simple string format for backward compatibility
116
+ let content: string | null | Array<ContentPart>
117
+ if (multimodalParts.length > 0) {
118
+ // Build ContentPart array preserving the order of text and multimodal parts
119
+ const contentParts: Array<ContentPart> = []
120
+ for (const part of uiMessage.parts) {
121
+ if (part.type === 'text') {
122
+ contentParts.push(part)
123
+ } else if (isMultimodalPart(part)) {
124
+ contentParts.push(part)
125
+ }
126
+ }
127
+ content = contentParts
128
+ } else {
129
+ // Simple string content for text-only messages
130
+ content = textParts.map((p) => p.content).join('') || null
131
+ }
132
+
91
133
  const toolCalls =
92
134
  toolCallParts.length > 0
93
135
  ? toolCallParts
@@ -108,7 +150,9 @@ export function uiMessageToModelMessages(
108
150
  : undefined
109
151
 
110
152
  // Create the main message
111
- if (uiMessage.role !== 'assistant' || content || !toolCalls) {
153
+ // For multimodal content, we always create a message even if content is an empty array
154
+ const hasContent = Array.isArray(content) ? true : content !== null
155
+ if (uiMessage.role !== 'assistant' || hasContent || !toolCalls) {
112
156
  messageList.push({
113
157
  role: uiMessage.role,
114
158
  content,
@@ -35,6 +35,8 @@ import type {
35
35
  ToolResultState,
36
36
  } from './types'
37
37
  import type {
38
+ ContentPart,
39
+ MessagePart,
38
40
  ModelMessage,
39
41
  StreamChunk,
40
42
  ToolCall,
@@ -165,13 +167,42 @@ export class StreamProcessor {
165
167
  }
166
168
 
167
169
  /**
168
- * Add a user message to the conversation
169
- */
170
- addUserMessage(content: string): UIMessage {
170
+ * Add a user message to the conversation.
171
+ * Supports both simple string content and multimodal content arrays.
172
+ *
173
+ * @param content - The message content (string or array of content parts)
174
+ * @param id - Optional custom message ID (generated if not provided)
175
+ * @returns The created UIMessage
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * // Simple text message
180
+ * processor.addUserMessage('Hello!')
181
+ *
182
+ * // Multimodal message with image
183
+ * processor.addUserMessage([
184
+ * { type: 'text', content: 'What is in this image?' },
185
+ * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
186
+ * ])
187
+ *
188
+ * // With custom ID
189
+ * processor.addUserMessage('Hello!', 'custom-id-123')
190
+ * ```
191
+ */
192
+ addUserMessage(content: string | Array<ContentPart>, id?: string): UIMessage {
193
+ // Convert content to message parts
194
+ const parts: Array<MessagePart> =
195
+ typeof content === 'string'
196
+ ? [{ type: 'text', content }]
197
+ : content.map((part) => {
198
+ // ContentPart types (text, image, audio, video, document) are compatible with MessagePart
199
+ return part as MessagePart
200
+ })
201
+
171
202
  const userMessage: UIMessage = {
172
- id: generateMessageId(),
203
+ id: id ?? generateMessageId(),
173
204
  role: 'user',
174
- parts: [{ type: 'text', content }],
205
+ parts,
175
206
  createdAt: new Date(),
176
207
  }
177
208
 
@@ -127,10 +127,11 @@ export class ToolCallManager {
127
127
  let toolResultContent: string
128
128
  if (tool?.execute) {
129
129
  try {
130
- // Parse arguments
130
+ // Parse arguments (normalize "null" to "{}" for empty tool_use blocks)
131
131
  let args: unknown
132
132
  try {
133
- args = JSON.parse(toolCall.function.arguments)
133
+ const argsString = toolCall.function.arguments.trim() || '{}'
134
+ args = JSON.parse(argsString === 'null' ? '{}' : argsString)
134
135
  } catch (parseError) {
135
136
  throw new Error(
136
137
  `Failed to parse tool arguments as JSON: ${toolCall.function.arguments}`,
@@ -294,7 +295,10 @@ export async function executeToolCalls(
294
295
 
295
296
  // Parse arguments, throwing error if invalid JSON
296
297
  let input: unknown = {}
297
- const argsStr = toolCall.function.arguments.trim() || '{}'
298
+ let argsStr = toolCall.function.arguments.trim() || '{}'
299
+ // Normalize "null" to "{}" — can occur when the model streams a tool_use
300
+ // block with no input_json_delta events (Anthropic adapter edge case)
301
+ if (argsStr === 'null') argsStr = '{}'
298
302
  if (argsStr) {
299
303
  try {
300
304
  input = JSON.parse(argsStr)
package/src/index.ts CHANGED
@@ -73,6 +73,9 @@ export {
73
73
  // All types
74
74
  export * from './types'
75
75
 
76
+ // Utility functions
77
+ export { detectImageMimeType } from './utils'
78
+
76
79
  // Event client + event types
77
80
  export * from './event-client'
78
81
 
package/src/types.ts CHANGED
@@ -108,24 +108,52 @@ export interface ToolCall {
108
108
  export type Modality = 'text' | 'image' | 'audio' | 'video' | 'document'
109
109
 
110
110
  /**
111
- * Source specification for multimodal content.
112
- * Supports both inline data (base64) and URL-based content.
111
+ * Source specification for inline data content (base64).
112
+ * Requires a mimeType to ensure providers receive proper content type information.
113
+ */
114
+ export interface ContentPartDataSource {
115
+ /**
116
+ * Indicates this is inline data content.
117
+ */
118
+ type: 'data'
119
+ /**
120
+ * The base64-encoded content value.
121
+ */
122
+ value: string
123
+ /**
124
+ * The MIME type of the content (e.g., 'image/png', 'audio/wav').
125
+ * Required for data sources to ensure proper handling by providers.
126
+ */
127
+ mimeType: string
128
+ }
129
+
130
+ /**
131
+ * Source specification for URL-based content.
132
+ * mimeType is optional as it can often be inferred from the URL or response headers.
113
133
  */
114
- export interface ContentPartSource {
134
+ export interface ContentPartUrlSource {
115
135
  /**
116
- * The type of source:
117
- * - 'data': Inline data (typically base64 encoded)
118
- * - 'url': URL reference to the content
136
+ * Indicates this is URL-referenced content.
119
137
  */
120
- type: 'data' | 'url'
138
+ type: 'url'
121
139
  /**
122
- * The actual content value:
123
- * - For 'data': base64-encoded string
124
- * - For 'url': HTTP(S) URL or data URI
140
+ * HTTP(S) URL or data URI pointing to the content.
125
141
  */
126
142
  value: string
143
+ /**
144
+ * Optional MIME type hint for cases where providers can't infer it from the URL.
145
+ */
146
+ mimeType?: string
127
147
  }
128
148
 
149
+ /**
150
+ * Source specification for multimodal content.
151
+ * Discriminated union supporting both inline data (base64) and URL-based content.
152
+ * - For 'data' sources: mimeType is required
153
+ * - For 'url' sources: mimeType is optional
154
+ */
155
+ export type ContentPartSource = ContentPartDataSource | ContentPartUrlSource
156
+
129
157
  /**
130
158
  * Image content part for multimodal messages.
131
159
  * @template TMetadata - Provider-specific metadata type (e.g., OpenAI's detail level)
@@ -282,6 +310,10 @@ export interface ThinkingPart {
282
310
 
283
311
  export type MessagePart =
284
312
  | TextPart
313
+ | ImagePart
314
+ | AudioPart
315
+ | VideoPart
316
+ | DocumentPart
285
317
  | ToolCallPart
286
318
  | ToolResultPart
287
319
  | ThinkingPart
package/src/utils.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Detect image mime type from base64 data using magic bytes.
3
+ * Returns undefined if the format cannot be detected.
4
+ *
5
+ * This function analyzes the first few bytes of base64-encoded image data
6
+ * to determine the image format based on file signature (magic bytes).
7
+ *
8
+ * @param base64Data - The base64-encoded image data
9
+ * @returns The detected mime type, or undefined if unrecognized
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const mimeType = detectImageMimeType(imageBase64)
14
+ * // Returns 'image/jpeg', 'image/png', 'image/gif', 'image/webp', or undefined
15
+ * ```
16
+ */
17
+ export function detectImageMimeType(
18
+ base64Data: string,
19
+ ): 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | undefined {
20
+ // Get first few bytes (base64 encoded)
21
+ const prefix = base64Data.substring(0, 20)
22
+
23
+ // JPEG: starts with /9j/ (FFD8FF in base64)
24
+ if (prefix.startsWith('/9j/')) {
25
+ return 'image/jpeg'
26
+ }
27
+ // PNG: starts with iVBORw0KGgo (89504E47 in base64)
28
+ if (prefix.startsWith('iVBORw0KGgo')) {
29
+ return 'image/png'
30
+ }
31
+ // GIF: starts with R0lGOD (474946 in base64)
32
+ if (prefix.startsWith('R0lGOD')) {
33
+ return 'image/gif'
34
+ }
35
+ // WebP: starts with UklGR (52494646 in base64, followed by WEBP)
36
+ if (prefix.startsWith('UklGR')) {
37
+ return 'image/webp'
38
+ }
39
+
40
+ return undefined
41
+ }