This commit is contained in:
roomote-v0[bot] 2026-05-17 21:42:55 +00:00 committed by GitHub
commit 1e123e6734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 277 additions and 3 deletions

View file

@ -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

View file

@ -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

View 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")
})
})

View 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}`
}