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 = `${currentParamName}>`
- // 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 = `${currentToolDirective.name}>`
-
- 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 = `${contentParamName}>`
- 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
-}