diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e0277350c2..31a386d00f 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -50,6 +50,7 @@ import { MIN_CHECKPOINT_TIMEOUT_SECONDS, TOOL_PROTOCOL, ConsecutiveMistakeError, + toolNames, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService, BridgeOrchestrator } from "@roo-code/cloud" @@ -3109,6 +3110,50 @@ export class Task extends EventEmitter implements TaskLike { this.assistantMessageContent = parsedBlocks } + // FALLBACK: For native protocol, check if model output XML tool tags instead of using native tool calls. + // Some models/providers may fall back to XML-style tool output even when configured for native protocol. + // This can happen with certain providers (e.g., Requesty, OpenRouter) where the model doesn't properly + // use the native tool calling format. When detected, parse the XML as a fallback to recover tool calls. + // See: https://github.com/RooCodeInc/Roo-Code/issues/10106 + if (!shouldUseXmlParser && assistantMessage.length > 0) { + // Check if we received any native tool calls during streaming + const hasNativeToolCalls = this.assistantMessageContent.some( + (block) => block.type === "tool_use" || block.type === "mcp_tool_use", + ) + + if (!hasNativeToolCalls) { + // Check if the assistant message contains XML tool tags + // We look for patterns like where tool_name is a known tool + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + const containsXmlToolTags = xmlToolTagPattern.test(assistantMessage) + + if (containsXmlToolTags) { + console.log( + `[Task#${this.taskId}.${this.instanceId}] Native protocol received XML tool tags - applying fallback XML parsing`, + ) + + // Use AssistantMessageParser to parse the XML tool calls from the text + const fallbackParser = new AssistantMessageParser() + fallbackParser.processChunk(assistantMessage) + fallbackParser.finalizeContentBlocks() + const parsedBlocks = fallbackParser.getContentBlocks() + + // Check if we successfully parsed any tool_use blocks + const parsedToolUses = parsedBlocks.filter((block) => block.type === "tool_use") + + if (parsedToolUses.length > 0) { + console.log( + `[Task#${this.taskId}.${this.instanceId}] Fallback XML parsing recovered ${parsedToolUses.length} tool call(s)`, + ) + // Replace content with parsed blocks that include the tool calls + this.assistantMessageContent = parsedBlocks + // Mark that we have new content to process + this.userMessageContentReady = false + } + } + } + } + // Present any partial blocks that were just completed // For XML protocol: includes both text and tool_use blocks parsed from the text stream // For native protocol: tool_use blocks were already presented during streaming via diff --git a/src/core/task/__tests__/fallback-xml-parsing.spec.ts b/src/core/task/__tests__/fallback-xml-parsing.spec.ts new file mode 100644 index 0000000000..9e743e3406 --- /dev/null +++ b/src/core/task/__tests__/fallback-xml-parsing.spec.ts @@ -0,0 +1,325 @@ +// npx vitest run src/core/task/__tests__/fallback-xml-parsing.spec.ts + +import { describe, it, expect } from "vitest" +import { AssistantMessageParser } from "../../assistant-message/AssistantMessageParser" +import { toolNames } from "@roo-code/types" +import { ToolUse, TextContent } from "../../../shared/tools" + +/** + * Tests for the fallback XML parsing mechanism that handles cases where + * a model configured for native tool calling protocol outputs XML-style + * tool tags instead of using native tool calls. + * + * This addresses Issue #10106: "Unknown API Error" / "Model Response Incomplete" + * errors caused by protocol mismatch. + * + * @see https://github.com/RooCodeInc/Roo-Code/issues/10106 + */ +describe("Fallback XML Parsing for Native Protocol", () => { + describe("XML Tool Tag Detection", () => { + it("should detect XML tool tags using the toolNames array", () => { + // This pattern matches the one used in Task.ts for fallback detection + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + + // Test various tool names from the toolNames array + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + expect(xmlToolTagPattern.test("")).toBe(true) + }) + + it("should not match non-tool tags", () => { + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + + expect(xmlToolTagPattern.test("
")).toBe(false) + expect(xmlToolTagPattern.test("")).toBe(false) + expect(xmlToolTagPattern.test("")).toBe(false) + expect(xmlToolTagPattern.test("")).toBe(false) + expect(xmlToolTagPattern.test("")).toBe(false) + }) + + it("should detect XML tool tags in assistant message with surrounding text", () => { + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + + const messageWithToolTag = `I'll read the file now. + + +src/index.ts + + +Let me analyze the content.` + + expect(xmlToolTagPattern.test(messageWithToolTag)).toBe(true) + }) + + it("should detect multiple tool tags in a single message", () => { + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + + const messageWithMultipleTools = ` + +1Completed + + + + +Task completed successfully. +` + + expect(xmlToolTagPattern.test(messageWithMultipleTools)).toBe(true) + }) + }) + + describe("AssistantMessageParser Fallback Parsing", () => { + it("should parse a single XML tool call from assistant message", () => { + const parser = new AssistantMessageParser() + const assistantMessage = `I'll read the file for you. + + +src/index.ts +` + + parser.processChunk(assistantMessage) + parser.finalizeContentBlocks() + const blocks = parser.getContentBlocks() + + // Should have text + tool_use blocks + const toolUses = blocks.filter((b) => b.type === "tool_use") + expect(toolUses.length).toBe(1) + + const toolUse = toolUses[0] as ToolUse + expect(toolUse.name).toBe("read_file") + expect(toolUse.params.path).toBe("src/index.ts") + }) + + it("should parse multiple XML tool calls from assistant message", () => { + const parser = new AssistantMessageParser() + + // This is the exact pattern from the bug report in Issue #10106 + const assistantMessage = `I'll update the todo list and complete the task properly. + + + + +10 +Create integration test plan +Completed + + +11 +Document the POC implementation +Completed + + + + + + +POC implementation completed successfully. + +` + + parser.processChunk(assistantMessage) + parser.finalizeContentBlocks() + const blocks = parser.getContentBlocks() + + const toolUses = blocks.filter((b) => b.type === "tool_use") + expect(toolUses.length).toBe(2) + + const updateTodo = toolUses[0] as ToolUse + expect(updateTodo.name).toBe("update_todo_list") + // The updates param is parsed as raw string content + expect(Object.keys(updateTodo.params).length).toBeGreaterThan(0) + + const attemptCompletion = toolUses[1] as ToolUse + expect(attemptCompletion.name).toBe("attempt_completion") + expect(attemptCompletion.params.result).toBeDefined() + }) + + it("should parse write_to_file tool with multiline content", () => { + const parser = new AssistantMessageParser() + const assistantMessage = ` +src/example.ts + +export function hello() { + console.log("Hello, world!"); +} + +` + + parser.processChunk(assistantMessage) + parser.finalizeContentBlocks() + const blocks = parser.getContentBlocks() + + const toolUses = blocks.filter((b) => b.type === "tool_use") + expect(toolUses.length).toBe(1) + + const writeFile = toolUses[0] as ToolUse + expect(writeFile.name).toBe("write_to_file") + expect(writeFile.params.path).toBe("src/example.ts") + expect(writeFile.params.content).toContain("export function hello()") + expect(writeFile.params.content).toContain('console.log("Hello, world!")') + }) + + it("should parse execute_command tool", () => { + const parser = new AssistantMessageParser() + const assistantMessage = `Let me run the tests. + + +npm test +/path/to/project +` + + parser.processChunk(assistantMessage) + parser.finalizeContentBlocks() + const blocks = parser.getContentBlocks() + + const toolUses = blocks.filter((b) => b.type === "tool_use") + expect(toolUses.length).toBe(1) + + const executeCmd = toolUses[0] as ToolUse + expect(executeCmd.name).toBe("execute_command") + expect(executeCmd.params.command).toBe("npm test") + expect(executeCmd.params.cwd).toBe("/path/to/project") + }) + + it("should preserve text content alongside parsed tool uses", () => { + const parser = new AssistantMessageParser() + const assistantMessage = `I'll analyze the code. + + +src/main.ts + + +After reviewing, I'll make some changes.` + + parser.processChunk(assistantMessage) + parser.finalizeContentBlocks() + const blocks = parser.getContentBlocks() + + const textBlocks = blocks.filter((b) => b.type === "text") + const toolUses = blocks.filter((b) => b.type === "tool_use") + + expect(textBlocks.length).toBeGreaterThan(0) + expect(toolUses.length).toBe(1) + + // Verify text content is preserved + const textContents = textBlocks.map((b) => (b as TextContent).content).join("") + expect(textContents).toContain("I'll analyze the code") + }) + }) + + describe("Fallback Trigger Conditions", () => { + it("should identify when fallback is needed (no native tool calls, has XML tags)", () => { + const assistantMessageContent: any[] = [{ type: "text", content: "Some text response with XML tool tags" }] + + const assistantMessage = ` +test.ts +` + + // Check conditions for fallback (logic from Task.ts) + const hasNativeToolCalls = assistantMessageContent.some( + (block) => block.type === "tool_use" || block.type === "mcp_tool_use", + ) + + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + const containsXmlToolTags = xmlToolTagPattern.test(assistantMessage) + + expect(hasNativeToolCalls).toBe(false) + expect(containsXmlToolTags).toBe(true) + // Fallback should be triggered + expect(!hasNativeToolCalls && containsXmlToolTags).toBe(true) + }) + + it("should not trigger fallback when native tool calls are present", () => { + const assistantMessageContent: any[] = [ + { type: "text", content: "Some text" }, + { + type: "tool_use", + id: "toolu_123", + name: "read_file", + params: { path: "test.ts" }, + }, + ] + + const hasNativeToolCalls = assistantMessageContent.some( + (block) => block.type === "tool_use" || block.type === "mcp_tool_use", + ) + + expect(hasNativeToolCalls).toBe(true) + // Fallback should NOT be triggered + }) + + it("should not trigger fallback when message has no XML tool tags", () => { + const assistantMessage = "This is just a regular text response without any tool calls." + + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + const containsXmlToolTags = xmlToolTagPattern.test(assistantMessage) + + expect(containsXmlToolTags).toBe(false) + // Fallback should NOT be triggered + }) + }) + + describe("Real-world Bug Scenarios from Issue #10106", () => { + it("should handle the exact error case from the bug report", () => { + // This is the exact message format that was causing "Model Response Incomplete" errors + const bugReportMessage = `I'll update the todo list and complete the task properly. + + + + +10 +Create integration test plan +Completed + + +11 +Document the POC implementation +Completed + + + + + + +POC implementation completed successfully with the following components: +1. Core infrastructure +2. Integration tests +3. Documentation + +` + + // Simulate the fallback detection + const assistantMessageContent: any[] = [] // Empty - no native tool calls + const hasNativeToolCalls = assistantMessageContent.some( + (block) => block.type === "tool_use" || block.type === "mcp_tool_use", + ) + + const xmlToolTagPattern = new RegExp(`<(${toolNames.join("|")})>`, "i") + const containsXmlToolTags = xmlToolTagPattern.test(bugReportMessage) + + // Verify fallback would be triggered + expect(hasNativeToolCalls).toBe(false) + expect(containsXmlToolTags).toBe(true) + + // Now verify the parser can extract the tools + const parser = new AssistantMessageParser() + parser.processChunk(bugReportMessage) + parser.finalizeContentBlocks() + const parsedBlocks = parser.getContentBlocks() + + const parsedToolUses = parsedBlocks.filter((block) => block.type === "tool_use") + expect(parsedToolUses.length).toBe(2) + + // Verify the tools are correctly identified + expect((parsedToolUses[0] as ToolUse).name).toBe("update_todo_list") + expect((parsedToolUses[1] as ToolUse).name).toBe("attempt_completion") + }) + }) +})