mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-22 00:31:46 +00:00
Merge 5468b76bbb into b867ec9145
This commit is contained in:
commit
1e123e6734
4 changed files with 277 additions and 3 deletions
|
|
@ -39,12 +39,16 @@ export const formatResponse = {
|
|||
suggestion: "Try to continue without this file, or ask the user to update the .rooignore file",
|
||||
}),
|
||||
|
||||
noToolsUsed: () => {
|
||||
noToolsUsed: (malformedToolCallInfo?: string) => {
|
||||
const instructions = getToolInstructionsReminder()
|
||||
|
||||
const malformedHint = malformedToolCallInfo
|
||||
? `\n\n# Malformed Tool Call Detected\n\nIt looks like you tried to call a tool using XML markup in your text response, but this is not supported. You must use the native/platform tool calling mechanism instead of writing XML tags.\n\nHere is what was detected in your response:\n${malformedToolCallInfo}\n\nPlease retry using the proper native tool calling mechanism with the correct tool name and parameters.`
|
||||
: ""
|
||||
|
||||
return `[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
|
||||
|
||||
${instructions}
|
||||
${instructions}${malformedHint}
|
||||
|
||||
# Next Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ import { getTaskDirectoryPath } from "../../utils/storage"
|
|||
import { formatResponse } from "../prompts/responses"
|
||||
import { SYSTEM_PROMPT } from "../prompts/system"
|
||||
import { buildNativeToolsArrayWithRestrictions } from "./build-tools"
|
||||
import { recoverMalformedToolCall, formatRecoveredToolCall } from "./malformed-tool-call-recovery"
|
||||
|
||||
// core modules
|
||||
import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
|
||||
|
|
@ -3493,10 +3494,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
this.consecutiveMistakeCount++
|
||||
}
|
||||
|
||||
// Attempt to detect malformed XML-style tool calls in the text output.
|
||||
// This helps open-weight models self-correct by providing specific feedback.
|
||||
let malformedToolCallInfo: string | undefined
|
||||
if (assistantMessage) {
|
||||
const recovered = recoverMalformedToolCall(assistantMessage)
|
||||
if (recovered) {
|
||||
malformedToolCallInfo = formatRecoveredToolCall(recovered)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the task's locked protocol for consistent behavior
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: formatResponse.noToolsUsed(),
|
||||
text: formatResponse.noToolsUsed(malformedToolCallInfo),
|
||||
})
|
||||
} else {
|
||||
// Reset counter when tools are used successfully
|
||||
|
|
|
|||
169
src/core/task/__tests__/malformed-tool-call-recovery.spec.ts
Normal file
169
src/core/task/__tests__/malformed-tool-call-recovery.spec.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { recoverMalformedToolCall, formatRecoveredToolCall } from "../malformed-tool-call-recovery"
|
||||
|
||||
describe("recoverMalformedToolCall", () => {
|
||||
describe("Pattern 1: <function=TOOL_NAME><parameter=PARAM_NAME>VALUE</parameter></function>", () => {
|
||||
it("should recover a basic function-style tool call", () => {
|
||||
const text = `<function=attempt_completion>
|
||||
<parameter=result>
|
||||
Task completed successfully.
|
||||
</parameter>
|
||||
</function>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("attempt_completion")
|
||||
expect(result!.parameters.result).toBe("Task completed successfully.")
|
||||
})
|
||||
|
||||
it("should recover a function-style tool call with trailing </tool_call>", () => {
|
||||
const text = `<function=attempt_completion>
|
||||
<parameter=result>
|
||||
LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISICING ELIT...
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("attempt_completion")
|
||||
expect(result!.parameters.result).toBe("LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISICING ELIT...")
|
||||
})
|
||||
|
||||
it("should recover a function-style tool call wrapped in <tool_call>", () => {
|
||||
const text = `<tool_call>
|
||||
<function=read_file>
|
||||
<parameter=path>/src/main.ts</parameter>
|
||||
</function>
|
||||
</tool_call>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("read_file")
|
||||
expect(result!.parameters.path).toBe("/src/main.ts")
|
||||
})
|
||||
|
||||
it("should recover multiple parameters", () => {
|
||||
const text = `<function=write_to_file>
|
||||
<parameter=path>/src/test.ts</parameter>
|
||||
<parameter=content>console.log("hello")</parameter>
|
||||
</function>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("write_to_file")
|
||||
expect(result!.parameters.path).toBe("/src/test.ts")
|
||||
expect(result!.parameters.content).toBe('console.log("hello")')
|
||||
})
|
||||
|
||||
it("should recover tool call with surrounding text/reasoning", () => {
|
||||
const text = `I will now complete the task.
|
||||
|
||||
<function=attempt_completion>
|
||||
<parameter=result>
|
||||
Done!
|
||||
</parameter>
|
||||
</function>
|
||||
|
||||
That should do it.`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("attempt_completion")
|
||||
expect(result!.parameters.result).toBe("Done!")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Pattern 2: XML-style <tool_name><param>value</param></tool_name>", () => {
|
||||
it("should recover an XML-style tool call", () => {
|
||||
const text = `<read_file>
|
||||
<path>/src/main.ts</path>
|
||||
</read_file>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("read_file")
|
||||
expect(result!.parameters.path).toBe("/src/main.ts")
|
||||
})
|
||||
|
||||
it("should recover XML-style tool call with multiple parameters", () => {
|
||||
const text = `<execute_command>
|
||||
<command>npm test</command>
|
||||
</execute_command>`
|
||||
|
||||
const result = recoverMalformedToolCall(text)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.toolName).toBe("execute_command")
|
||||
expect(result!.parameters.command).toBe("npm test")
|
||||
})
|
||||
})
|
||||
|
||||
describe("No match cases", () => {
|
||||
it("should return null for plain text without tool call patterns", () => {
|
||||
const text = "I need to think about this problem more carefully."
|
||||
const result = recoverMalformedToolCall(text)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should return null for empty string", () => {
|
||||
const result = recoverMalformedToolCall("")
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it("should return null for random XML that does not look like a tool call", () => {
|
||||
const text = "<div><span>Hello</span></div>"
|
||||
const result = recoverMalformedToolCall(text)
|
||||
// This might match pattern 2, but div/span don't have underscore names
|
||||
// The regex requires [a-z_]+ which matches div, but the inner must also match
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatRecoveredToolCall", () => {
|
||||
it("should format a simple recovered tool call", () => {
|
||||
const recovered = {
|
||||
toolName: "attempt_completion",
|
||||
parameters: { result: "Task done" },
|
||||
}
|
||||
|
||||
const formatted = formatRecoveredToolCall(recovered)
|
||||
|
||||
expect(formatted).toContain("Tool: attempt_completion")
|
||||
expect(formatted).toContain("result")
|
||||
expect(formatted).toContain("Task done")
|
||||
})
|
||||
|
||||
it("should truncate long parameter values", () => {
|
||||
const longValue = "x".repeat(200)
|
||||
const recovered = {
|
||||
toolName: "write_to_file",
|
||||
parameters: { content: longValue },
|
||||
}
|
||||
|
||||
const formatted = formatRecoveredToolCall(recovered)
|
||||
|
||||
expect(formatted).toContain("...")
|
||||
expect(formatted.length).toBeLessThan(longValue.length + 100)
|
||||
})
|
||||
|
||||
it("should format multiple parameters", () => {
|
||||
const recovered = {
|
||||
toolName: "write_to_file",
|
||||
parameters: { path: "/src/test.ts", content: "hello world" },
|
||||
}
|
||||
|
||||
const formatted = formatRecoveredToolCall(recovered)
|
||||
|
||||
expect(formatted).toContain("path")
|
||||
expect(formatted).toContain("content")
|
||||
expect(formatted).toContain("/src/test.ts")
|
||||
expect(formatted).toContain("hello world")
|
||||
})
|
||||
})
|
||||
90
src/core/task/malformed-tool-call-recovery.ts
Normal file
90
src/core/task/malformed-tool-call-recovery.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Malformed Tool Call Recovery
|
||||
*
|
||||
* Detects common XML-style tool call patterns in assistant text output when no
|
||||
* native tool calls were detected. This is especially common with open-weight
|
||||
* models (e.g., qwen3-coder) that sometimes emit tool calls as plain text
|
||||
* instead of using the native tool calling mechanism.
|
||||
*
|
||||
* IMPORTANT: Recovered information is NEVER auto-executed. It is only used to
|
||||
* provide a clearer retry message to the model so it can self-correct.
|
||||
*/
|
||||
|
||||
export interface RecoveredToolCall {
|
||||
toolName: string
|
||||
parameters: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to recover a malformed tool call from the assistant's text output.
|
||||
*
|
||||
* Supported patterns:
|
||||
* 1. `<function=TOOL_NAME><parameter=PARAM_NAME>VALUE</parameter></function>` (with optional `</tool_call>`)
|
||||
* 2. `<tool_call><function=TOOL_NAME>...</function></tool_call>`
|
||||
* 3. XML-style `<TOOL_NAME><PARAM_NAME>VALUE</PARAM_NAME></TOOL_NAME>`
|
||||
*
|
||||
* @param text - The assistant's text output to scan
|
||||
* @returns A RecoveredToolCall if a malformed tool call is detected, or null otherwise
|
||||
*/
|
||||
export function recoverMalformedToolCall(text: string): RecoveredToolCall | null {
|
||||
// Pattern 1: <function=TOOL_NAME><parameter=PARAM_NAME>VALUE</parameter></function>
|
||||
// Optionally wrapped in <tool_call>...</tool_call>
|
||||
const functionPattern = /<function=([a-z_]+)>\s*([\s\S]*?)<\/function>/i
|
||||
const functionMatch = text.match(functionPattern)
|
||||
|
||||
if (functionMatch) {
|
||||
const toolName = functionMatch[1]
|
||||
const body = functionMatch[2]
|
||||
|
||||
const parameters: Record<string, string> = {}
|
||||
const paramPattern = /<parameter=([a-z_]+)>([\s\S]*?)<\/parameter>/gi
|
||||
let paramMatch
|
||||
|
||||
while ((paramMatch = paramPattern.exec(body)) !== null) {
|
||||
parameters[paramMatch[1]] = paramMatch[2].trim()
|
||||
}
|
||||
|
||||
return { toolName, parameters }
|
||||
}
|
||||
|
||||
// Pattern 2: XML-style <tool_name><param_name>value</param_name></tool_name>
|
||||
// Common with some models that try to emulate XML tool calling.
|
||||
// Requires at least one underscore in the tool name to avoid matching regular HTML tags.
|
||||
const xmlToolPattern = /<([a-z]+_[a-z_]+)>\s*((?:<[a-z_]+>[\s\S]*?<\/[a-z_]+>\s*)+)<\/\1>/i
|
||||
const xmlMatch = text.match(xmlToolPattern)
|
||||
|
||||
if (xmlMatch) {
|
||||
const toolName = xmlMatch[1]
|
||||
const body = xmlMatch[2]
|
||||
|
||||
const parameters: Record<string, string> = {}
|
||||
const paramPattern = /<([a-z_]+)>([\s\S]*?)<\/\1>/gi
|
||||
let paramMatch
|
||||
|
||||
while ((paramMatch = paramPattern.exec(body)) !== null) {
|
||||
parameters[paramMatch[1]] = paramMatch[2].trim()
|
||||
}
|
||||
|
||||
// Only return if we found at least one parameter
|
||||
if (Object.keys(parameters).length > 0) {
|
||||
return { toolName, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a recovered tool call into a human-readable summary for the retry message.
|
||||
*/
|
||||
export function formatRecoveredToolCall(recovered: RecoveredToolCall): string {
|
||||
const paramSummary = Object.entries(recovered.parameters)
|
||||
.map(([key, value]) => {
|
||||
// Truncate long parameter values to keep the message concise
|
||||
const truncated = value.length > 100 ? value.substring(0, 100) + "..." : value
|
||||
return ` - ${key}: "${truncated}"`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
return `Tool: ${recovered.toolName}\nParameters:\n${paramSummary}`
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue