@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.
- package/dist/esm/activities/chat/index.js +3 -4
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +13 -6
- package/dist/esm/activities/chat/messages.js +115 -86
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +143 -26
- package/dist/esm/activities/chat/stream/processor.js +205 -77
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.js +2 -3
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/types.d.ts +3 -3
- package/package.json +1 -1
- package/src/activities/chat/index.ts +12 -9
- package/src/activities/chat/messages.ts +211 -138
- package/src/activities/chat/stream/processor.ts +240 -115
- package/src/activities/chat/tools/tool-calls.ts +1 -4
- package/src/types.ts +3 -3
|
@@ -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":"AAgBA,SAAS,cAAc,MAAwC;AAC7D,SACE,KAAK,SAAS,UACd,KAAK,SAAS,WACd,KAAK,SAAS,WACd,KAAK,SAAS,WACd,KAAK,SAAS;AAElB;AAQA,SAAS,qBACP,OACoC;AACpC,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,UAAU,MAAM,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM;AACpD,MAAI,SAAS;AACX,UAAM,SAAS,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE;AAClD,WAAO,UAAU;AAAA,EACnB;AAEA,SAAO;AACT;AAMA,SAAS,eAAe,SAAqD;AAC3E,MAAI,YAAY,KAAM,QAAO;AAC7B,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,SAAO,QACJ,OAAO,CAAC,SAA2B,KAAK,SAAS,MAAM,EACvD,IAAI,CAAC,SAAS,KAAK,OAAO,EAC1B,KAAK,EAAE;AACZ;AAKO,SAAS,+BACd,UACqB;AACrB,QAAM,gBAAqC,CAAA;AAC3C,aAAW,OAAO,UAAU;AAC1B,QAAI,WAAW,KAAK;AAElB,oBAAc,KAAK,GAAG,yBAAyB,GAAG,CAAC;AAAA,IACrD,OAAO;AAEL,oBAAc,KAAK,GAAG;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;AAqBO,SAAS,yBACd,WACqB;AAErB,MAAI,UAAU,SAAS,UAAU;AAC/B,WAAO,CAAA;AAAA,EACT;AAIA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,CAAC,uBAAuB,SAAS,CAAC;AAAA,EAC3C;AAGA,SAAO,uBAAuB,SAAS;AACzC;AAMA,SAAS,uBAAuB,WAAoC;AAClE,QAAM,eAAmC,CAAA;AACzC,aAAW,QAAQ,UAAU,OAAO;AAClC,QAAI,cAAc,IAAI,GAAG;AACvB,mBAAa,KAAK,IAAI;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,UAAU;AAAA,IAChB,SAAS,qBAAqB,YAAY;AAAA,EAAA;AAE9C;AAYA,SAAS,gBAAkC;AACzC,SAAO,EAAE,cAAc,IAAI,WAAW,CAAA,EAAC;AACzC;AAEA,SAAS,mBAAmB,MAA6B;AACvD,SACE,KAAK,UAAU,oBACf,KAAK,UAAU,wBACf,KAAK,WAAW;AAEpB;AAWA,SAAS,uBAAuB,WAA2C;AACzE,QAAM,cAAmC,CAAA;AACzC,MAAI,UAAU,cAAA;AAKd,QAAM,2CAA2B,IAAA;AAEjC,WAAS,eAAqB;AAC5B,UAAM,UAAU,qBAAqB,QAAQ,YAAY;AACzD,UAAM,aAAa,YAAY;AAC/B,UAAM,eAAe,QAAQ,UAAU,SAAS;AAEhD,QAAI,cAAc,cAAc;AAC9B,kBAAY,KAAK;AAAA,QACf,MAAM;AAAA,QACN;AAAA,QACA,GAAI,gBAAgB,EAAE,WAAW,QAAQ,UAAA;AAAA,MAAU,CACpD;AAAA,IACH;AACA,cAAU,cAAA;AAAA,EACZ;AAEA,aAAW,QAAQ,UAAU,OAAO;AAClC,YAAQ,KAAK,MAAA;AAAA,MACX,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,gBAAQ,aAAa,KAAK,IAAI;AAC9B;AAAA,MAEF,KAAK;AACH,YAAI,mBAAmB,IAAI,GAAG;AAC5B,kBAAQ,UAAU,KAAK;AAAA,YACrB,IAAI,KAAK;AAAA,YACT,MAAM;AAAA,YACN,UAAU;AAAA,cACR,MAAM,KAAK;AAAA,cACX,WAAW,KAAK;AAAA,YAAA;AAAA,UAClB,CACD;AAAA,QACH;AACA;AAAA,MAEF,KAAK;AAEH,qBAAA;AAGA,aACG,KAAK,UAAU,cAAc,KAAK,UAAU,YAC7C,CAAC,qBAAqB,IAAI,KAAK,UAAU,GACzC;AACA,sBAAY,KAAK;AAAA,YACf,MAAM;AAAA,YACN,SAAS,KAAK;AAAA,YACd,YAAY,KAAK;AAAA,UAAA,CAClB;AACD,+BAAqB,IAAI,KAAK,UAAU;AAAA,QAC1C;AACA;AAAA,IAIA;AAAA,EAEN;AAGA,eAAA;AAMA,aAAW,QAAQ,UAAU,OAAO;AAClC,QAAI,KAAK,SAAS,YAAa;AAG/B,QACE,KAAK,WAAW,UAChB,CAAC,KAAK,YACN,CAAC,qBAAqB,IAAI,KAAK,EAAE,GACjC;AACA,kBAAY,KAAK;AAAA,QACf,MAAM;AAAA,QACN,SAAS,KAAK,UAAU,KAAK,MAAM;AAAA,QACnC,YAAY,KAAK;AAAA,MAAA,CAClB;AACD,2BAAqB,IAAI,KAAK,EAAE;AAAA,IAClC;AAGA,QACE,KAAK,UAAU,wBACf,KAAK,UAAU,aAAa,UAC5B,CAAC,qBAAqB,IAAI,KAAK,EAAE,GACjC;AACA,YAAM,WAAW,KAAK,SAAS;AAC/B,kBAAY,KAAK;AAAA,QACf,MAAM;AAAA,QACN,SAAS,KAAK,UAAU;AAAA,UACtB;AAAA,UACA,GAAI,YAAY,EAAE,kBAAkB,KAAA;AAAA,UACpC,SAAS,WACL,8BACA;AAAA,QAAA,CACL;AAAA,QACD,YAAY,KAAK;AAAA,MAAA,CAClB;AACD,2BAAqB,IAAI,KAAK,EAAE;AAAA,IAClC;AAAA,EACF;AAGA,MAAI,YAAY,WAAW,GAAG;AAC5B,gBAAY,KAAK;AAAA,MACf,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAAA,EACH;AAEA,SAAO;AACT;AAcO,SAAS,wBACd,cACA,IACW;AACX,QAAM,QAA4B,CAAA;AAIlC,MAAI,aAAa,SAAS,UAAU,aAAa,YAAY;AAC3D,UAAM,KAAK;AAAA,MACT,MAAM;AAAA,MACN,YAAY,aAAa;AAAA,MACzB,SAAS,eAAe,aAAa,OAAO;AAAA,MAC5C,OAAO;AAAA,IAAA,CACR;AAAA,EACH,WAAW,MAAM,QAAQ,aAAa,OAAO,GAAG;AAE9C,eAAW,QAAQ,aAAa,SAAS;AACvC,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF,OAAO;AAEL,UAAM,cAAc,eAAe,aAAa,OAAO;AACvD,QAAI,aAAa;AACf,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAAA,IACH;AAAA,EACF;AAGA,MAAI,aAAa,aAAa,aAAa,UAAU,SAAS,GAAG;AAC/D,eAAW,YAAY,aAAa,WAAW;AAC7C,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,IAAI,SAAS;AAAA,QACb,MAAM,SAAS,SAAS;AAAA,QACxB,WAAW,SAAS,SAAS;AAAA,QAC7B,OAAO;AAAA;AAAA,MAAA,CACR;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL,IAAI,MAAM,kBAAA;AAAA,IACV,MAAM,aAAa,SAAS,SAAS,cAAc,aAAa;AAAA,IAChE;AAAA,EAAA;AAEJ;AAUO,SAAS,0BACd,eACkB;AAClB,QAAM,aAA+B,CAAA;AACrC,MAAI,0BAA4C;AAEhD,aAAW,OAAO,eAAe;AAC/B,QAAI,IAAI,SAAS,QAAQ;AAEvB,UACE,2BACA,wBAAwB,SAAS,aACjC;AACA,gCAAwB,MAAM,KAAK;AAAA,UACjC,MAAM;AAAA,UACN,YAAY,IAAI;AAAA,UAChB,SAAS,eAAe,IAAI,OAAO;AAAA,UACnC,OAAO;AAAA,QAAA,CACR;AAAA,MACH,OAAO;AAEL,cAAM,sBAAsB,wBAAwB,GAAG;AACvD,mBAAW,KAAK,mBAAmB;AAAA,MACrC;AAAA,IACF,OAAO;AAEL,YAAM,YAAY,wBAAwB,GAAG;AAC7C,iBAAW,KAAK,SAAS;AAGzB,UAAI,IAAI,SAAS,aAAa;AAC5B,kCAA0B;AAAA,MAC5B,OAAO;AACL,kCAA0B;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAUO,SAAS,qBACd,SACA,YACW;AACX,MAAI,WAAW,SAAS;AAEtB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,IAAI,QAAQ,MAAM,WAAA;AAAA,MAClB,WAAW,QAAQ,aAAa,oBAAI,KAAA;AAAA,IAAK;AAAA,EAE7C,OAAO;AAEL,WAAO;AAAA,MACL,GAAG,wBAAwB,SAAS,YAAY;AAAA,MAChD,+BAAe,KAAA;AAAA,IAAK;AAAA,EAExB;AACF;AAKO,SAAS,oBAA4B;AAC1C,SAAO,OAAO,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AACrE;"}
|
|
@@ -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
|
|
53
|
+
* - Tool call completion via TOOL_CALL_END events
|
|
52
54
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
107
|
-
*
|
|
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
|
|
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
|
-
*
|
|
178
|
-
*
|
|
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
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
*
|
|
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
|
|
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
|
*/
|