import { type ToolName, toolNames } from "@roo-code/types" import { TextContent, ToolUse, McpToolUse, ToolParamName, toolParamNames } from "../../shared/tools" export type AssistantMessageContent = TextContent | ToolUse | McpToolUse export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] { let contentBlocks: AssistantMessageContent[] = [] let currentTextContent: TextContent | undefined = undefined let currentTextContentStartIndex = 0 let currentToolUse: ToolUse | undefined = undefined let currentToolUseStartIndex = 0 let currentParamName: ToolParamName | undefined = undefined let currentParamValueStartIndex = 0 let accumulator = "" for (let i = 0; i < assistantMessage.length; i++) { const char = assistantMessage[i] accumulator += char // There should not be a param without a tool use. if (currentToolUse && currentParamName) { const currentParamValue = accumulator.slice(currentParamValueStartIndex) const paramClosingTag = `` if (currentParamValue.endsWith(paramClosingTag)) { // End of param value. // Don't trim content parameters to preserve newlines, but strip first and last newline only const paramValue = currentParamValue.slice(0, -paramClosingTag.length) currentToolUse.params[currentParamName] = currentParamName === "content" ? paramValue.replace(/^\n/, "").replace(/\n$/, "") : paramValue.trim() currentParamName = undefined continue } else { // Partial param value is accumulating. continue } } // No currentParamName. if (currentToolUse) { const currentToolValue = accumulator.slice(currentToolUseStartIndex) const toolUseClosingTag = `` if (currentToolValue.endsWith(toolUseClosingTag)) { // End of a tool use. currentToolUse.partial = false contentBlocks.push(currentToolUse) currentToolUse = undefined continue } else { const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`) for (const paramOpeningTag of possibleParamOpeningTags) { if (accumulator.endsWith(paramOpeningTag)) { // Start of a new parameter. currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName currentParamValueStartIndex = accumulator.length break } } // There's no current param, and not starting a new param. // Special case for write_to_file where file contents could // contain the closing tag, in which case the param would have // closed and we end up with the rest of the file contents here. // To work around this, we get the string between the starting // content tag and the LAST content tag. const contentParamName: ToolParamName = "content" if (currentToolUse.name === "write_to_file" && accumulator.endsWith(``)) { const toolContent = accumulator.slice(currentToolUseStartIndex) const contentStartTag = `<${contentParamName}>` const contentEndTag = `` const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length const contentEndIndex = toolContent.lastIndexOf(contentEndTag) if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) { // Don't trim content to preserve newlines, but strip first and last newline only currentToolUse.params[contentParamName] = toolContent .slice(contentStartIndex, contentEndIndex) .replace(/^\n/, "") .replace(/\n$/, "") } } // Partial tool value is accumulating. continue } } // No currentToolUse. let didStartToolUse = false const possibleToolUseOpeningTags = toolNames.map((name) => `<${name}>`) for (const toolUseOpeningTag of possibleToolUseOpeningTags) { if (accumulator.endsWith(toolUseOpeningTag)) { // Start of a new tool use. currentToolUse = { type: "tool_use", name: toolUseOpeningTag.slice(1, -1) as ToolName, params: {}, partial: true, } currentToolUseStartIndex = accumulator.length // This also indicates the end of the current text content. if (currentTextContent) { currentTextContent.partial = false // Remove the partially accumulated tool use tag from the // end of text (