diff --git a/src/core/message-parsing/__tests__/parseAssistantMessage.test.ts b/src/core/message-parsing/__tests__/parseAssistantMessage.test.ts deleted file mode 100644 index 12bd04c3f9..0000000000 --- a/src/core/message-parsing/__tests__/parseAssistantMessage.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -// npx jest src/core/assistant-message/__tests__/parseAssistantMessage.test.ts - -import { TextDirective, ToolDirective } from "../directives" -import { AssistantMessageContent, parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage" -import { parseAssistantMessageV2 } from "../parseAssistantMessageV2" - -const isEmptyTextContent = (block: AssistantMessageContent) => - block.type === "text" && (block as TextDirective).content === "" - -;[parseAssistantMessageV1, parseAssistantMessageV2].forEach((parser, index) => { - describe(`parseAssistantMessageV${index + 1}`, () => { - describe("text content parsing", () => { - it("should parse a simple text message", () => { - const message = "This is a simple text message" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, // Text is always partial when it's the last content - }) - }) - - it("should parse a multi-line text message", () => { - const message = "This is a multi-line\ntext message\nwith several lines" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, // Text is always partial when it's the last content - }) - }) - - it("should mark text as partial when it's the last content in the message", () => { - const message = "This is a partial text" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0]).toEqual({ - type: "text", - content: message, - partial: true, - }) - }) - }) - - describe("tool use parsing", () => { - it("should parse a simple tool use", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(false) - }) - - it("should parse a tool use with multiple parameters", () => { - const message = - "src/file.ts1020" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.params.start_line).toBe("10") - expect(ToolDirective.params.end_line).toBe("20") - expect(ToolDirective.partial).toBe(false) - }) - - it("should mark tool use as partial when it's not closed", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(true) - }) - - it("should handle a partial parameter in a tool use", () => { - const message = "src/file.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(true) - }) - }) - - describe("mixed content parsing", () => { - it("should parse text followed by a tool use", () => { - const message = "Here's the file content: src/file.ts" - const result = parser(message) - - expect(result).toHaveLength(2) - - const textContent = result[0] as TextDirective - expect(textContent.type).toBe("text") - expect(textContent.content).toBe("Here's the file content:") - expect(textContent.partial).toBe(false) - - const ToolDirective = result[1] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(false) - }) - - it("should parse a tool use followed by text", () => { - const message = "src/file.tsHere's what I found in the file." - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(2) - - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(false) - - const textContent = result[1] as TextDirective - expect(textContent.type).toBe("text") - expect(textContent.content).toBe("Here's what I found in the file.") - expect(textContent.partial).toBe(true) - }) - - it("should parse multiple tool uses separated by text", () => { - const message = - "First file: src/file1.tsSecond file: src/file2.ts" - const result = parser(message) - - expect(result).toHaveLength(4) - - expect(result[0].type).toBe("text") - expect((result[0] as TextDirective).content).toBe("First file:") - - expect(result[1].type).toBe("tool_use") - expect((result[1] as ToolDirective).name).toBe("read_file") - expect((result[1] as ToolDirective).params.path).toBe("src/file1.ts") - - expect(result[2].type).toBe("text") - expect((result[2] as TextDirective).content).toBe("Second file:") - - expect(result[3].type).toBe("tool_use") - expect((result[3] as ToolDirective).name).toBe("read_file") - expect((result[3] as ToolDirective).params.path).toBe("src/file2.ts") - }) - }) - - describe("special cases", () => { - it("should handle the write_to_file tool with content that contains closing tags", () => { - const message = `src/file.ts - function example() { - // This has XML-like content: - return true; - } - 5` - - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("write_to_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.params.line_count).toBe("5") - expect(ToolDirective.params.content).toContain("function example()") - expect(ToolDirective.params.content).toContain("// This has XML-like content: ") - expect(ToolDirective.params.content).toContain("return true;") - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle empty messages", () => { - const message = "" - const result = parser(message) - - expect(result).toHaveLength(0) - }) - - it("should handle malformed tool use tags", () => { - const message = "This has a malformed tag" - const result = parser(message) - - expect(result).toHaveLength(1) - expect(result[0].type).toBe("text") - expect((result[0] as TextDirective).content).toBe(message) - }) - - it("should handle tool use with no parameters", () => { - const message = "" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("browser_action") - expect(Object.keys(ToolDirective.params).length).toBe(0) - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle nested tool tags that aren't actually nested", () => { - const message = - "echo 'test.txt'" - - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("execute_command") - expect(ToolDirective.params.command).toBe("echo 'test.txt'") - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle a tool use with a parameter containing XML-like content", () => { - const message = "
.*
src
" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("search_files") - expect(ToolDirective.params.regex).toBe("
.*
") - expect(ToolDirective.params.path).toBe("src") - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle consecutive tool uses without text in between", () => { - const message = - "file1.tsfile2.ts" - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(2) - - const ToolDirective1 = result[0] as ToolDirective - expect(ToolDirective1.type).toBe("tool_use") - expect(ToolDirective1.name).toBe("read_file") - expect(ToolDirective1.params.path).toBe("file1.ts") - expect(ToolDirective1.partial).toBe(false) - - const ToolDirective2 = result[1] as ToolDirective - expect(ToolDirective2.type).toBe("tool_use") - expect(ToolDirective2.name).toBe("read_file") - expect(ToolDirective2.params.path).toBe("file2.ts") - expect(ToolDirective2.partial).toBe(false) - }) - - it("should handle whitespace in parameters", () => { - const message = " src/file.ts " - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("read_file") - expect(ToolDirective.params.path).toBe("src/file.ts") - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle multi-line parameters", () => { - const message = `file.ts - line 1 - line 2 - line 3 - 3` - const result = parser(message).filter((block) => !isEmptyTextContent(block)) - - expect(result).toHaveLength(1) - const ToolDirective = result[0] as ToolDirective - expect(ToolDirective.type).toBe("tool_use") - expect(ToolDirective.name).toBe("write_to_file") - expect(ToolDirective.params.path).toBe("file.ts") - expect(ToolDirective.params.content).toContain("line 1") - expect(ToolDirective.params.content).toContain("line 2") - expect(ToolDirective.params.content).toContain("line 3") - expect(ToolDirective.params.line_count).toBe("3") - expect(ToolDirective.partial).toBe(false) - }) - - it("should handle a complex message with multiple content types", () => { - const message = `I'll help you with that task. - - src/index.ts - - Now let's modify the file: - - src/index.ts - // Updated content - console.log("Hello world"); - 2 - - Let's run the code: - - node src/index.ts` - - const result = parser(message) - - expect(result).toHaveLength(6) - - // First text block - expect(result[0].type).toBe("text") - expect((result[0] as TextDirective).content).toBe("I'll help you with that task.") - - // First tool use (read_file) - expect(result[1].type).toBe("tool_use") - expect((result[1] as ToolDirective).name).toBe("read_file") - - // Second text block - expect(result[2].type).toBe("text") - expect((result[2] as TextDirective).content).toContain("Now let's modify the file:") - - // Second tool use (write_to_file) - expect(result[3].type).toBe("tool_use") - expect((result[3] as ToolDirective).name).toBe("write_to_file") - - // Third text block - expect(result[4].type).toBe("text") - expect((result[4] as TextDirective).content).toContain("Let's run the code:") - - // Third tool use (execute_command) - expect(result[5].type).toBe("tool_use") - expect((result[5] as ToolDirective).name).toBe("execute_command") - }) - }) - }) -}) diff --git a/src/core/message-parsing/__tests__/parseAssistantMessageBenchmark.ts b/src/core/message-parsing/__tests__/parseAssistantMessageBenchmark.ts deleted file mode 100644 index d5450988c9..0000000000 --- a/src/core/message-parsing/__tests__/parseAssistantMessageBenchmark.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ - -// node --expose-gc --import tsx src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts - -import { performance } from "perf_hooks" -import { parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage" -import { parseAssistantMessageV2 } from "../parseAssistantMessageV2" - -const formatNumber = (num: number): string => { - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") -} - -const measureExecutionTime = (fn: Function, input: string, iterations: number = 1000): number => { - for (let i = 0; i < 10; i++) { - fn(input) - } - - const start = performance.now() - - for (let i = 0; i < iterations; i++) { - fn(input) - } - - const end = performance.now() - return (end - start) / iterations // Average time per iteration in ms. -} - -const measureMemoryUsage = ( - fn: Function, - input: string, - iterations: number = 100, -): { heapUsed: number; heapTotal: number } => { - if (global.gc) { - // Force garbage collection if available. - global.gc() - } else { - console.warn("No garbage collection hook! Run with --expose-gc for more accurate memory measurements.") - } - - const initialMemory = process.memoryUsage() - - for (let i = 0; i < iterations; i++) { - fn(input) - } - - const finalMemory = process.memoryUsage() - - return { - heapUsed: (finalMemory.heapUsed - initialMemory.heapUsed) / iterations, - heapTotal: (finalMemory.heapTotal - initialMemory.heapTotal) / iterations, - } -} - -const testCases = [ - { - name: "Simple text message", - input: "This is a simple text message without any tool uses.", - }, - { - name: "Message with a simple tool use", - input: "Let's read a file: src/file.ts", - }, - { - name: "Message with a complex tool use (write_to_file)", - input: "src/file.ts\nfunction example() {\n // This has XML-like content: \n return true;\n}\n5", - }, - { - name: "Message with multiple tool uses", - input: "First file: src/file1.ts\nSecond file: src/file2.ts\nLet's write a new file: src/file3.ts\nexport function newFunction() {\n return 'Hello world';\n}\n3", - }, - { - name: "Large message with repeated tool uses", - input: Array(50) - .fill( - 'src/file.ts\noutput.tsconsole.log("hello");1', - ) - .join("\n"), - }, -] - -const runBenchmark = () => { - const maxNameLength = testCases.reduce((max, testCase) => Math.max(max, testCase.name.length), 0) - const namePadding = maxNameLength + 2 - - console.log( - `| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1/V2 Ratio | V1 Heap (bytes) | V2 Heap (bytes) |`, - ) - console.log( - `| ${"-".repeat(namePadding)} | ------------ | ------------ | ----------- | ---------------- | ---------------- |`, - ) - - for (const testCase of testCases) { - const v1Time = measureExecutionTime(parseAssistantMessageV1, testCase.input) - const v2Time = measureExecutionTime(parseAssistantMessageV2, testCase.input) - const timeRatio = v1Time / v2Time - - const v1Memory = measureMemoryUsage(parseAssistantMessageV1, testCase.input) - const v2Memory = measureMemoryUsage(parseAssistantMessageV2, testCase.input) - - console.log( - `| ${testCase.name.padEnd(namePadding)} | ` + - `${v1Time.toFixed(4).padStart(12)} | ` + - `${v2Time.toFixed(4).padStart(12)} | ` + - `${timeRatio.toFixed(2).padStart(11)} | ` + - `${formatNumber(Math.round(v1Memory.heapUsed)).padStart(16)} | ` + - `${formatNumber(Math.round(v2Memory.heapUsed)).padStart(16)} |`, - ) - } -} - -runBenchmark() diff --git a/src/core/message-parsing/parseAssistantMessage.ts b/src/core/message-parsing/parseAssistantMessage.ts deleted file mode 100644 index e09b258934..0000000000 --- a/src/core/message-parsing/parseAssistantMessage.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DirectiveStreamingParser } from "./DirectiveStreamingParser" -import type { Directive } from "./directives" - -// Backward compatibility alias -export type AssistantMessageContent = Directive - -export function parseAssistantMessage(assistantMessage: string): Directive[] { - return DirectiveStreamingParser.parse(assistantMessage) -} diff --git a/src/core/message-parsing/parseAssistantMessageV2.ts b/src/core/message-parsing/parseAssistantMessageV2.ts deleted file mode 100644 index d54b776e62..0000000000 --- a/src/core/message-parsing/parseAssistantMessageV2.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { type ToolName, toolNames } from "@roo-code/types" -import { TextDirective, ToolDirective } from "./directives" -import { ToolParamName, toolParamNames } from "./directives" - -export type AssistantMessageContent = TextDirective | ToolDirective - -/** - * Parses an assistant message string potentially containing mixed text and tool - * usage blocks marked with XML-like tags into an array of structured content - * objects. - * - * This version aims for efficiency by avoiding the character-by-character - * accumulator of V1. It iterates through the string using an index `i`. At each - * position, it checks if the substring *ending* at `i` matches any known - * opening or closing tags for tools or parameters using `startsWith` with an - * offset. - * It uses pre-computed Maps (`ToolDirectiveOpenTags`, `toolParamOpenTags`) for quick - * tag lookups. - * State is managed using indices (`currentTextContentStart`, - * `currentToolDirectiveStart`, `currentParamValueStart`) pointing to the start of the - * current block within the original `assistantMessage` string. - * - * Slicing is used to extract content only when a block (text, parameter, or - * tool use) is completed. - * - * Special handling for `write_to_file` and `new_rule` content parameters is - * included, using `indexOf` and `lastIndexOf` on the relevant slice to handle - * potentially nested closing tags. - * - * If the input string ends mid-block, the last open block is added and marked - * as partial. - * - * @param assistantMessage The raw string output from the assistant. - * @returns An array of `AssistantMessageContent` objects, which can be - * `TextContent` or `ToolDirective`. Blocks that were not fully closed by the - * end of the input string will have their `partial` flag set to - * `true`. - */ - -export function parseAssistantMessageV2(assistantMessage: string): AssistantMessageContent[] { - const contentBlocks: AssistantMessageContent[] = [] - - let currentTextContentStart = 0 // Index where the current text block started. - let currentTextContent: TextDirective | undefined = undefined - let currentToolDirectiveStart = 0 // Index *after* the opening tag of the current tool use. - let currentToolDirective: ToolDirective | undefined = undefined - let currentParamValueStart = 0 // Index *after* the opening tag of the current param. - let currentParamName: ToolParamName | undefined = undefined - - // Precompute tags for faster lookups. - const ToolDirectiveOpenTags = new Map() - const toolParamOpenTags = new Map() - - for (const name of toolNames) { - ToolDirectiveOpenTags.set(`<${name}>`, name) - } - - for (const name of toolParamNames) { - toolParamOpenTags.set(`<${name}>`, name) - } - - const len = assistantMessage.length - - for (let i = 0; i < len; i++) { - const currentCharIndex = i - - // Parsing a tool parameter - if (currentToolDirective && currentParamName) { - const closeTag = `` - // Check if the string *ending* at index `i` matches the closing tag - if ( - currentCharIndex >= closeTag.length - 1 && - assistantMessage.startsWith( - closeTag, - currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag. - ) - ) { - // Found the closing tag for the parameter. - const value = assistantMessage - .slice( - currentParamValueStart, // Start after the opening tag. - currentCharIndex - closeTag.length + 1, // End before the closing tag. - ) - .trim() - currentToolDirective.params[currentParamName] = value - currentParamName = undefined // Go back to parsing tool content. - // We don't continue loop here, need to check for tool close or other params at index i. - } else { - continue // Still inside param value, move to next char. - } - } - - // Parsing a tool use (but not a specific parameter). - if (currentToolDirective && !currentParamName) { - // Ensure we are not inside a parameter already. - // Check if starting a new parameter. - let startedNewParam = false - - for (const [tag, paramName] of toolParamOpenTags.entries()) { - if ( - currentCharIndex >= tag.length - 1 && - assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1) - ) { - currentParamName = paramName - currentParamValueStart = currentCharIndex + 1 // Value starts after the tag. - startedNewParam = true - break - } - } - - if (startedNewParam) { - continue // Handled start of param, move to next char. - } - - // Check if closing the current tool use. - const toolCloseTag = `` - - if ( - currentCharIndex >= toolCloseTag.length - 1 && - assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1) - ) { - // End of the tool use found. - // Special handling for content params *before* finalizing the - // tool. - const toolContentSlice = assistantMessage.slice( - currentToolDirectiveStart, // From after the tool opening tag. - currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag. - ) - - // Check if content parameter needs special handling - // (write_to_file/new_rule). - // This check is important if the closing tag was - // missed by the parameter parsing logic (e.g., if content is - // empty or parsing logic prioritizes tool close). - const contentParamName: ToolParamName = "content" - if ( - currentToolDirective.name === "write_to_file" /* || currentToolDirective.name === "new_rule" */ && - // !(contentParamName in currentToolDirective.params) && // Only if not already parsed. - toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists. - ) { - const contentStartTag = `<${contentParamName}>` - const contentEndTag = `` - const contentStart = toolContentSlice.indexOf(contentStartTag) - - // Use `lastIndexOf` for robustness against nested tags. - const contentEnd = toolContentSlice.lastIndexOf(contentEndTag) - - if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) { - const contentValue = toolContentSlice - .slice(contentStart + contentStartTag.length, contentEnd) - .trim() - - currentToolDirective.params[contentParamName] = contentValue - } - } - - currentToolDirective.partial = false // Mark as complete. - contentBlocks.push(currentToolDirective) - currentToolDirective = undefined // Reset state. - currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag. - continue // Move to next char. - } - - // If not starting a param and not closing the tool, continue - // accumulating tool content implicitly. - continue - } - - // Parsing text / looking for tool start. - if (!currentToolDirective) { - // Check if starting a new tool use. - let startedNewTool = false - - for (const [tag, toolName] of ToolDirectiveOpenTags.entries()) { - if ( - currentCharIndex >= tag.length - 1 && - assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1) - ) { - // End current text block if one was active. - if (currentTextContent) { - currentTextContent.content = assistantMessage - .slice( - currentTextContentStart, // From where text started. - currentCharIndex - tag.length + 1, // To before the tool tag starts. - ) - .trim() - - currentTextContent.partial = false // Ended because tool started. - - if (currentTextContent.content.length > 0) { - contentBlocks.push(currentTextContent) - } - - currentTextContent = undefined - } else { - // Check for any text between the last block and this tag. - const potentialText = assistantMessage - .slice( - currentTextContentStart, // From where text *might* have started. - currentCharIndex - tag.length + 1, // To before the tool tag starts. - ) - .trim() - - if (potentialText.length > 0) { - contentBlocks.push({ - type: "text", - content: potentialText, - partial: false, - }) - } - } - - // Start the new tool use. - currentToolDirective = { - type: "tool_use", - name: toolName, - params: {}, - partial: true, // Assume partial until closing tag is found. - } - - currentToolDirectiveStart = currentCharIndex + 1 // Tool content starts after the opening tag. - startedNewTool = true - - break - } - } - - if (startedNewTool) { - continue // Handled start of tool, move to next char. - } - - // If not starting a tool, it must be text content. - if (!currentTextContent) { - // Start a new text block if we aren't already in one. - currentTextContentStart = currentCharIndex // Text starts at the current character. - - // Check if the current char is the start of potential text *immediately* after a tag. - // This needs the previous state - simpler to let slicing handle it later. - // Resetting start index accurately is key. - // It should be the index *after* the last processed tag. - // The logic managing currentTextContentStart after closing tags handles this. - currentTextContent = { - type: "text", - content: "", // Will be determined by slicing at the end or when a tool starts - partial: true, - } - } - // Continue accumulating text implicitly; content is extracted later. - } - } - - // Finalize any open parameter within an open tool use. - if (currentToolDirective && currentParamName) { - currentToolDirective.params[currentParamName] = assistantMessage - .slice(currentParamValueStart) // From param start to end of string. - .trim() - // Tool use remains partial. - } - - // Finalize any open tool use (which might contain the finalized partial param). - if (currentToolDirective) { - // Tool use is partial because the loop finished before its closing tag. - contentBlocks.push(currentToolDirective) - } - // Finalize any trailing text content. - // Only possible if a tool use wasn't open at the very end. - else if (currentTextContent) { - currentTextContent.content = assistantMessage - .slice(currentTextContentStart) // From text start to end of string. - .trim() - - // Text is partial because the loop finished. - if (currentTextContent.content.length > 0) { - contentBlocks.push(currentTextContent) - } - } - - return contentBlocks -}