@tanstack/ai 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
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
+ {"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 UIMessage,\n} from '../../types'\n// ===========================\n// Message Converters\n// ===========================\n\n/**\n * Check if a MessagePart is a content part (text, image, audio, video, document)\n * that maps directly to a ModelMessage ContentPart.\n */\nfunction isContentPart(part: MessagePart): part is ContentPart {\n return (\n part.type === 'text' ||\n part.type === 'image' ||\n part.type === 'audio' ||\n part.type === 'video' ||\n part.type === 'document'\n )\n}\n\n/**\n * Collapse an array of ContentParts into the most compact ModelMessage content:\n * - Empty array → null\n * - All text parts → joined string (or null if empty)\n * - Mixed content → ContentPart array as-is\n */\nfunction collapseContentParts(\n parts: Array<ContentPart>,\n): string | null | Array<ContentPart> {\n if (parts.length === 0) return null\n\n const allText = parts.every((p) => p.type === 'text')\n if (allText) {\n const joined = parts.map((p) => p.content).join('')\n return joined || null\n }\n\n return parts\n}\n\n/**\n * Extract text content from ModelMessage content (string, null, or ContentPart array).\n * Used when only the text portion is needed (e.g., tool result content).\n */\nfunction getTextContent(content: string | null | Array<ContentPart>): string {\n if (content === null) return ''\n if (typeof content === 'string') return content\n return content\n .filter((part): part is TextPart => 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 * Walks the parts array IN ORDER to preserve the interleaving of text,\n * tool calls, and tool results. This is critical for multi-round tool\n * flows where the model generates text, calls a tool, gets the result,\n * then generates more text and calls another tool.\n *\n * The output preserves the sequential structure:\n * text1 → toolCall1 → toolResult1 → text2 → toolCall2 → toolResult2\n * becomes:\n * assistant: {content: \"text1\", toolCalls: [toolCall1]}\n * tool: toolResult1\n * assistant: {content: \"text2\", toolCalls: [toolCall2]}\n * tool: toolResult2\n *\n * @param uiMessage - The UIMessage to convert\n * @returns An array of ModelMessages preserving part ordering\n */\nexport function uiMessageToModelMessages(\n uiMessage: UIMessage,\n): Array<ModelMessage> {\n // Skip system messages - they're handled via systemPrompts, not ModelMessages\n if (uiMessage.role === 'system') {\n return []\n }\n\n // For non-assistant messages (user), use the simpler path since they\n // don't have tool calls or tool results to interleave\n if (uiMessage.role !== 'assistant') {\n return [buildUserOrToolMessage(uiMessage)]\n }\n\n // For assistant messages, walk parts in order to preserve interleaving\n return buildAssistantMessages(uiMessage)\n}\n\n/**\n * Build a single ModelMessage for user messages (simple path).\n * Preserves ordering of text and multimodal content parts.\n */\nfunction buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {\n const contentParts: Array<ContentPart> = []\n for (const part of uiMessage.parts) {\n if (isContentPart(part)) {\n contentParts.push(part)\n }\n }\n\n return {\n role: uiMessage.role as 'user' | 'assistant' | 'tool',\n content: collapseContentParts(contentParts),\n }\n}\n\n// Accumulator for building an assistant segment (content + tool calls)\ninterface AssistantSegment {\n contentParts: Array<ContentPart>\n toolCalls: Array<{\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n }>\n}\n\nfunction createSegment(): AssistantSegment {\n return { contentParts: [], toolCalls: [] }\n}\n\nfunction isToolCallIncluded(part: ToolCallPart): boolean {\n return (\n part.state === 'input-complete' ||\n part.state === 'approval-responded' ||\n part.output !== undefined\n )\n}\n\n/**\n * Build ModelMessages for an assistant UIMessage, preserving the\n * sequential interleaving of text, tool calls, and tool results.\n *\n * Walks parts in order. Text and tool-call parts accumulate into the\n * current \"segment\". When a tool-result part is encountered, the\n * current segment is flushed as an assistant message, then the tool\n * result is emitted as a tool message.\n */\nfunction buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {\n const messageList: Array<ModelMessage> = []\n let current = createSegment()\n\n // Track emitted tool result IDs to avoid duplicates.\n // A tool call can have BOTH an explicit tool-result part AND an output\n // field on the tool-call part. We only want one per tool call ID.\n const emittedToolResultIds = new Set<string>()\n\n function flushSegment(): void {\n const content = collapseContentParts(current.contentParts)\n const hasContent = content !== null\n const hasToolCalls = current.toolCalls.length > 0\n\n if (hasContent || hasToolCalls) {\n messageList.push({\n role: 'assistant',\n content,\n ...(hasToolCalls && { toolCalls: current.toolCalls }),\n })\n }\n current = createSegment()\n }\n\n for (const part of uiMessage.parts) {\n switch (part.type) {\n case 'text':\n case 'image':\n case 'audio':\n case 'video':\n case 'document':\n current.contentParts.push(part)\n break\n\n case 'tool-call':\n if (isToolCallIncluded(part)) {\n current.toolCalls.push({\n id: part.id,\n type: 'function' as const,\n function: {\n name: part.name,\n arguments: part.arguments,\n },\n })\n }\n break\n\n case 'tool-result':\n // Flush the current assistant segment before emitting the tool result\n flushSegment()\n\n // Emit the tool result\n if (\n (part.state === 'complete' || part.state === 'error') &&\n !emittedToolResultIds.has(part.toolCallId)\n ) {\n messageList.push({\n role: 'tool',\n content: part.content,\n toolCallId: part.toolCallId,\n })\n emittedToolResultIds.add(part.toolCallId)\n }\n break\n\n // thinking parts are skipped - they're UI-only\n default:\n break\n }\n }\n\n // Flush any remaining accumulated content\n flushSegment()\n\n // Emit tool results from client tool-call parts with output or approval,\n // but only if not already covered by an explicit tool-result part above.\n // These are appended at the end since they don't have explicit tool-result\n // parts in the parts array to trigger inline emission.\n for (const part of uiMessage.parts) {\n if (part.type !== 'tool-call') continue\n\n // Client tool with output - add as tool result (if not already emitted)\n if (\n part.output !== undefined &&\n !part.approval &&\n !emittedToolResultIds.has(part.id)\n ) {\n messageList.push({\n role: 'tool',\n content: JSON.stringify(part.output),\n toolCallId: part.id,\n })\n emittedToolResultIds.add(part.id)\n }\n\n // Approval response - add as tool result for iteration tracking\n if (\n part.state === 'approval-responded' &&\n part.approval?.approved !== undefined &&\n !emittedToolResultIds.has(part.id)\n ) {\n const approved = part.approval.approved\n messageList.push({\n role: 'tool',\n content: JSON.stringify({\n approved,\n ...(approved && { pendingExecution: true }),\n message: approved\n ? 'User approved this action'\n : 'User denied this action',\n }),\n toolCallId: part.id,\n })\n emittedToolResultIds.add(part.id)\n }\n }\n\n // If no messages were produced (e.g., empty parts), emit a minimal assistant message\n if (messageList.length === 0) {\n messageList.push({\n role: 'assistant',\n content: null,\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 tool results (when role is \"tool\") - only produce tool-result part,\n // not a text part (the content IS the tool result, not display text)\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 } else if (Array.isArray(modelMessage.content)) {\n // Multimodal content - preserve all content parts as MessageParts\n for (const part of modelMessage.content) {\n parts.push(part)\n }\n } else {\n // String or null content\n const textContent = getTextContent(modelMessage.content)\n if (textContent) {\n parts.push({\n type: 'text',\n content: textContent,\n })\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 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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"}
@@ -42,18 +42,18 @@ export interface StreamProcessorOptions {
42
42
  * StreamProcessor - State machine for processing AI response streams
43
43
  *
44
44
  * Manages the full UIMessage[] conversation and emits events on changes.
45
+ * Trusts the adapter contract: adapters emit clean AG-UI events in the
46
+ * correct order.
45
47
  *
46
48
  * State tracking:
47
49
  * - Full message array
48
50
  * - Current assistant message being streamed
49
- * - Text content accumulation
51
+ * - Text content accumulation (reset on TEXT_MESSAGE_START)
50
52
  * - Multiple parallel tool calls
51
- * - Tool call completion detection
53
+ * - Tool call completion via TOOL_CALL_END events
52
54
  *
53
- * Tool call completion is detected when:
54
- * 1. A new tool call starts at a different index
55
- * 2. Text content arrives
56
- * 3. Stream ends
55
+ * @see docs/chat-architecture.md#streamprocessor-internal-state — State field reference
56
+ * @see docs/chat-architecture.md#adapter-contract — What this class expects from adapters
57
57
  */
58
58
  export declare class StreamProcessor {
59
59
  private chunkStrategy;
@@ -69,8 +69,8 @@ export declare class StreamProcessor {
69
69
  private toolCalls;
70
70
  private toolCallOrder;
71
71
  private finishReason;
72
+ private hasError;
72
73
  private isDone;
73
- private hasToolCallsSinceTextStart;
74
74
  private recording;
75
75
  private recordingStartTime;
76
76
  constructor(options?: StreamProcessorOptions);
@@ -103,10 +103,35 @@ export declare class StreamProcessor {
103
103
  */
104
104
  addUserMessage(content: string | Array<ContentPart>, id?: string): UIMessage;
105
105
  /**
106
- * Start streaming a new assistant message
107
- * Returns the message ID
106
+ * Prepare for a new assistant message stream.
107
+ * Does NOT create the message immediately -- the message is created lazily
108
+ * when the first content-bearing chunk arrives via ensureAssistantMessage().
109
+ * This prevents empty assistant messages from flickering in the UI when
110
+ * auto-continuation produces no content.
111
+ */
112
+ prepareAssistantMessage(): void;
113
+ /**
114
+ * @deprecated Use prepareAssistantMessage() instead. This eagerly creates
115
+ * an assistant message which can cause empty message flicker.
108
116
  */
109
117
  startAssistantMessage(): string;
118
+ /**
119
+ * Get the current assistant message ID (if one has been created).
120
+ * Returns null if prepareAssistantMessage() was called but no content
121
+ * has arrived yet.
122
+ */
123
+ getCurrentAssistantMessageId(): string | null;
124
+ /**
125
+ * Lazily create the assistant message if it hasn't been created yet.
126
+ * Called by content handlers on the first content-bearing chunk.
127
+ * Returns the message ID.
128
+ *
129
+ * Content-bearing chunks that trigger this:
130
+ * TEXT_MESSAGE_CONTENT, TOOL_CALL_START, STEP_FINISHED, RUN_ERROR.
131
+ *
132
+ * @see docs/chat-architecture.md#streamprocessor-internal-state — Lazy creation pattern
133
+ */
134
+ private ensureAssistantMessage;
110
135
  /**
111
136
  * Add a tool result (called by client after handling onToolCall)
112
137
  */
@@ -141,27 +166,90 @@ export declare class StreamProcessor {
141
166
  */
142
167
  process(stream: AsyncIterable<any>): Promise<ProcessorResult>;
143
168
  /**
144
- * Process a single chunk from the stream
169
+ * Process a single chunk from the stream.
170
+ *
171
+ * Central dispatch for all AG-UI events. Each event type maps to a specific
172
+ * handler. Events not listed in the switch are intentionally ignored
173
+ * (RUN_STARTED, TEXT_MESSAGE_END, STEP_STARTED, STATE_SNAPSHOT, STATE_DELTA).
174
+ *
175
+ * @see docs/chat-architecture.md#adapter-contract — Expected event types and ordering
145
176
  */
146
177
  processChunk(chunk: StreamChunk): void;
147
178
  /**
148
- * Handle TEXT_MESSAGE_CONTENT event
179
+ * Handle TEXT_MESSAGE_START event — marks the beginning of a new text segment.
180
+ * Resets segment accumulation so text after tool calls starts fresh.
181
+ *
182
+ * This is the key mechanism for multi-segment text (text before and after tool
183
+ * calls becoming separate TextParts). Without this reset, all text would merge
184
+ * into a single TextPart and tool-call interleaving would be lost.
185
+ *
186
+ * @see docs/chat-architecture.md#single-shot-text-response — Step-by-step text processing
187
+ * @see docs/chat-architecture.md#text-then-tool-interleaving-single-shot — Multi-segment text
188
+ */
189
+ private handleTextMessageStartEvent;
190
+ /**
191
+ * Handle TEXT_MESSAGE_CONTENT event.
192
+ *
193
+ * Accumulates delta into both currentSegmentText (for UI emission) and
194
+ * totalTextContent (for ProcessorResult). Lazily creates the assistant
195
+ * UIMessage on first content. Uses updateTextPart() which replaces the
196
+ * last TextPart or creates a new one depending on part ordering.
197
+ *
198
+ * @see docs/chat-architecture.md#single-shot-text-response — Text accumulation step-by-step
199
+ * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
149
200
  */
150
201
  private handleTextMessageContentEvent;
151
202
  /**
152
- * Handle TOOL_CALL_START event
203
+ * Handle TOOL_CALL_START event.
204
+ *
205
+ * Creates a new InternalToolCallState entry in the toolCalls Map and appends
206
+ * a ToolCallPart to the UIMessage. Duplicate toolCallId is a no-op.
207
+ *
208
+ * CRITICAL: This MUST be received before any TOOL_CALL_ARGS for the same
209
+ * toolCallId. Args for unknown IDs are silently dropped.
210
+ *
211
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Tool call state transitions
212
+ * @see docs/chat-architecture.md#parallel-tool-calls-single-shot — Parallel tracking by ID
213
+ * @see docs/chat-architecture.md#adapter-contract — Ordering requirements
153
214
  */
154
215
  private handleToolCallStartEvent;
155
216
  /**
156
- * Handle TOOL_CALL_ARGS event
217
+ * Handle TOOL_CALL_ARGS event.
218
+ *
219
+ * Appends the delta to the tool call's accumulated arguments string.
220
+ * Transitions state from awaiting-input → input-streaming on first non-empty delta.
221
+ * Attempts partial JSON parse on each update for UI preview.
222
+ *
223
+ * If toolCallId is not found in the Map (no preceding TOOL_CALL_START),
224
+ * this event is silently dropped.
225
+ *
226
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Step-by-step tool call processing
157
227
  */
158
228
  private handleToolCallArgsEvent;
159
229
  /**
160
- * Handle TOOL_CALL_END event
230
+ * Handle TOOL_CALL_END event — authoritative signal that a tool call's input is finalized.
231
+ *
232
+ * This event has a DUAL ROLE:
233
+ * - Without `result`: Signals arguments are done (from adapter). Transitions to input-complete.
234
+ * - With `result`: Signals tool was executed and result is available (from TextEngine).
235
+ * Creates both output on the tool-call part AND a tool-result part.
236
+ *
237
+ * If `input` is provided, it overrides the accumulated string parse as the
238
+ * canonical parsed arguments.
239
+ *
240
+ * @see docs/chat-architecture.md#tool-results-and-the-tool_call_end-dual-role — Full explanation
241
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — End-to-end flow
161
242
  */
162
243
  private handleToolCallEndEvent;
163
244
  /**
164
- * Handle RUN_FINISHED event
245
+ * Handle RUN_FINISHED event.
246
+ *
247
+ * Records the finishReason and calls completeAllToolCalls() as a safety net
248
+ * to force-complete any tool calls that didn't receive an explicit TOOL_CALL_END.
249
+ * This handles cases like aborted streams or adapter bugs.
250
+ *
251
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — finishReason semantics
252
+ * @see docs/chat-architecture.md#adapter-contract — Why RUN_FINISHED is mandatory
165
253
  */
166
254
  private handleRunFinishedEvent;
167
255
  /**
@@ -169,21 +257,33 @@ export declare class StreamProcessor {
169
257
  */
170
258
  private handleRunErrorEvent;
171
259
  /**
172
- * Handle STEP_FINISHED event (for thinking/reasoning content)
260
+ * Handle STEP_FINISHED event (for thinking/reasoning content).
261
+ *
262
+ * Accumulates delta into thinkingContent and updates a single ThinkingPart
263
+ * in the UIMessage (replaced in-place, not appended).
264
+ *
265
+ * @see docs/chat-architecture.md#thinkingreasoning-content — Thinking flow
173
266
  */
174
267
  private handleStepFinishedEvent;
175
268
  /**
176
- * Handle CUSTOM event
177
- * Handles special custom events like 'tool-input-available' for client-side tool execution
178
- * and 'approval-requested' for tool approval flows
269
+ * Handle CUSTOM event.
270
+ *
271
+ * Handles special custom events emitted by the TextEngine (not adapters):
272
+ * - 'tool-input-available': Client tool needs execution. Fires onToolCall.
273
+ * - 'approval-requested': Tool needs user approval. Updates tool-call part
274
+ * state and fires onApprovalRequest.
275
+ *
276
+ * @see docs/chat-architecture.md#client-tools-and-approval-flows — Full flow details
179
277
  */
180
278
  private handleCustomEvent;
181
279
  /**
182
- * Detect if an incoming content chunk represents a NEW text segment
183
- */
184
- private isNewTextSegment;
185
- /**
186
- * Complete all tool calls
280
+ * Complete all tool calls — safety net for stream termination.
281
+ *
282
+ * Called by RUN_FINISHED and finalizeStream(). Force-transitions any tool call
283
+ * not yet in input-complete state. Handles cases where TOOL_CALL_END was
284
+ * missed (adapter bug, network error, aborted stream).
285
+ *
286
+ * @see docs/chat-architecture.md#single-shot-tool-call-response — Safety net behavior
187
287
  */
188
288
  private completeAllToolCalls;
189
289
  /**
@@ -191,7 +291,13 @@ export declare class StreamProcessor {
191
291
  */
192
292
  private completeToolCall;
193
293
  /**
194
- * Emit pending text update
294
+ * Emit pending text update.
295
+ *
296
+ * Calls updateTextPart() which has critical append-vs-replace logic:
297
+ * - If last UIMessage part is TextPart → replaces its content (same segment).
298
+ * - If last part is anything else → pushes new TextPart (new segment after tools).
299
+ *
300
+ * @see docs/chat-architecture.md#uimessage-part-ordering-invariants — Replace vs. push logic
195
301
  */
196
302
  private emitTextUpdate;
197
303
  /**
@@ -199,7 +305,13 @@ export declare class StreamProcessor {
199
305
  */
200
306
  private emitMessagesChange;
201
307
  /**
202
- * Finalize the stream - complete all pending operations
308
+ * Finalize the stream — complete all pending operations.
309
+ *
310
+ * Called when the async iterable ends (stream closed). Acts as the final
311
+ * safety net: completes any remaining tool calls, flushes un-emitted text,
312
+ * and fires onStreamEnd.
313
+ *
314
+ * @see docs/chat-architecture.md#single-shot-text-response — Finalization step
203
315
  */
204
316
  finalizeStream(): void;
205
317
  /**
@@ -230,6 +342,11 @@ export declare class StreamProcessor {
230
342
  * Full reset (including messages)
231
343
  */
232
344
  reset(): void;
345
+ /**
346
+ * Check if a message contains only whitespace text and no other meaningful parts
347
+ * (no tool calls, tool results, thinking, etc.)
348
+ */
349
+ private isWhitespaceOnlyMessage;
233
350
  /**
234
351
  * Replay a recording through the processor
235
352
  */