fix: improve tool call failure handling and error recovery

- Enhanced error handling in presentAssistantMessage.ts with better tool error tracking
- Improved ToolRepetitionDetector with contextual failure history and intelligent error messages
- Added enhanced logging and debugging information for tool failures
- Updated error messages to be more helpful and actionable with specific suggestions
- Added telemetry tracking for tool validation and execution failures
- Improved consecutive mistake handling with better user guidance

Fixes #5927
This commit is contained in:
Roo Code 2025-07-18 21:24:40 +00:00
parent 90148401e9
commit 038aeecd5b
4 changed files with 146 additions and 5 deletions

View file

@ -2,6 +2,7 @@ import cloneDeep from "clone-deep"
import { serializeError } from "serialize-error"
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import { TelemetryEventName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
@ -307,6 +308,19 @@ export async function presentAssistantMessage(cline: Task) {
const handleError = async (action: string, error: Error) => {
const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
// Record the tool error for tracking and telemetry
cline.recordToolError(block.name as ToolName, error.message)
// Capture detailed telemetry for tool failures using existing event
TelemetryService.instance.captureEvent(TelemetryEventName.DIFF_APPLICATION_ERROR, {
taskId: cline.taskId,
toolName: block.name,
action,
errorMessage: error.message,
errorType: error.constructor.name,
consecutiveMistakes: cline.consecutiveMistakeCount,
})
await cline.say(
"error",
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
@ -364,7 +378,22 @@ export async function presentAssistantMessage(cline: Task) {
)
} catch (error) {
cline.consecutiveMistakeCount++
pushToolResult(formatResponse.toolError(error.message))
// Record the validation error for tracking
cline.recordToolError(block.name as ToolName, `Validation failed: ${error.message}`)
// Capture telemetry for validation failures using existing event
TelemetryService.instance.captureEvent(TelemetryEventName.SCHEMA_VALIDATION_ERROR, {
taskId: cline.taskId,
toolName: block.name,
errorMessage: error.message,
consecutiveMistakes: cline.consecutiveMistakeCount,
mode: mode ?? defaultModeSlug,
})
// Provide more helpful error message for tool validation failures
const enhancedErrorMessage = `Tool validation failed for ${block.name}: ${error.message}\n\nThis may indicate:\n- Invalid parameters provided to the tool\n- Tool not allowed in current mode\n- Missing required parameters\n\nPlease review the tool usage and try again with correct parameters.`
pushToolResult(formatResponse.toolError(enhancedErrorMessage))
break
}
@ -399,6 +428,9 @@ export async function presentAssistantMessage(cline: Task) {
TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
}
// Capture telemetry for tool repetition using existing event
TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
// Return tool result message about the repetition
pushToolResult(
formatResponse.toolError(

View file

@ -13,7 +13,31 @@ export const formatResponse = {
toolApprovedWithFeedback: (feedback?: string) =>
`The user approved this operation and provided the following context:\n<feedback>\n${feedback}\n</feedback>`,
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
toolError: (error?: string) => {
const baseMessage = `The tool execution failed with the following error:\n<error>\n${error}\n</error>`
// Add helpful suggestions based on common error patterns
let suggestions = ""
if (error) {
const lowerError = error.toLowerCase()
if (lowerError.includes("permission") || lowerError.includes("access denied")) {
suggestions =
"\n\nSuggestions:\n• Check file permissions\n• Ensure the file is not locked by another process\n• Try using a different file path"
} else if (lowerError.includes("not found") || lowerError.includes("no such file")) {
suggestions =
"\n\nSuggestions:\n• Verify the file path is correct\n• Use list_files to check available files\n• Create the file first if it doesn't exist"
} else if (lowerError.includes("validation failed")) {
suggestions =
"\n\nSuggestions:\n• Check that all required parameters are provided\n• Verify parameter formats match expectations\n• Review the tool documentation for correct usage"
} else if (lowerError.includes("syntax error") || lowerError.includes("invalid")) {
suggestions =
"\n\nSuggestions:\n• Check the syntax of your input\n• Verify all brackets, quotes, and tags are properly closed\n• Try simplifying the operation"
}
}
return baseMessage + suggestions
},
rooIgnoreError: (path: string) =>
`Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`,
@ -30,8 +54,17 @@ If you require additional information from the user, use the ask_followup_questi
Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task.
(This is an automated message, so do not respond to it conversationally.)`,
tooManyMistakes: (feedback?: string) =>
`You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`,
tooManyMistakes: (feedback?: string) => {
let message = `You seem to be having trouble proceeding. This may indicate:\n• Tool parameters are incorrect\n• The approach needs to be changed\n• The task should be broken into smaller steps\n• A different tool might be more appropriate`
if (feedback) {
message += `\n\nThe user has provided the following feedback to help guide you:\n<feedback>\n${feedback}\n</feedback>`
} else {
message += `\n\nConsider:\n• Re-reading files to understand current state\n• Using simpler, more targeted operations\n• Asking for clarification if the task is unclear`
}
return message
},
missingToolParameterError: (paramName: string) =>
`Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${toolUseInstructionsReminder}`,

View file

@ -1932,9 +1932,21 @@ export class Task extends EventEmitter<ClineEvents> {
this.toolUsage[toolName].failures++
// Record the failure in the repetition detector for better context
this.toolRepetitionDetector.recordToolFailure(toolName, error)
if (error) {
this.emit("taskToolFailed", this.taskId, toolName, error)
}
// Add enhanced logging for debugging tool failures
console.debug(`[Task ${this.taskId}] Tool failure recorded:`, {
toolName,
error,
totalFailures: this.toolUsage[toolName].failures,
totalAttempts: this.toolUsage[toolName].attempts,
consecutiveMistakes: this.consecutiveMistakeCount,
})
}
// Getters

View file

@ -9,6 +9,8 @@ export class ToolRepetitionDetector {
private previousToolCallJson: string | null = null
private consecutiveIdenticalToolCallCount: number = 0
private readonly consecutiveIdenticalToolCallLimit: number
private toolFailureHistory: Array<{ toolName: string; timestamp: number; reason?: string }> = []
private readonly maxHistorySize = 10
/**
* Creates a new ToolRepetitionDetector
@ -48,6 +50,10 @@ export class ToolRepetitionDetector {
this.consecutiveIdenticalToolCallLimit > 0 &&
this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit
) {
// Generate enhanced error message with context
const recentFailures = this.getRecentFailureContext(currentToolCallBlock.name)
const contextMessage = this.generateContextualErrorMessage(currentToolCallBlock.name, recentFailures)
// Reset counters to allow recovery if user guides the AI past this point
this.consecutiveIdenticalToolCallCount = 0
this.previousToolCallJson = null
@ -57,7 +63,7 @@ export class ToolRepetitionDetector {
allowExecution: false,
askUser: {
messageKey: "mistake_limit_reached",
messageDetail: t("tools:toolRepetitionLimitReached", { toolName: currentToolCallBlock.name }),
messageDetail: contextMessage,
},
}
}
@ -66,6 +72,64 @@ export class ToolRepetitionDetector {
return { allowExecution: true }
}
/**
* Records a tool failure for better context in future error messages
*/
public recordToolFailure(toolName: string, reason?: string): void {
this.toolFailureHistory.push({
toolName,
timestamp: Date.now(),
reason,
})
// Keep history size manageable
if (this.toolFailureHistory.length > this.maxHistorySize) {
this.toolFailureHistory.shift()
}
}
/**
* Gets recent failure context for a specific tool
*/
private getRecentFailureContext(toolName: string): Array<{ reason?: string; timestamp: number }> {
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000
return this.toolFailureHistory
.filter((failure) => failure.toolName === toolName && failure.timestamp > fiveMinutesAgo)
.map((failure) => ({ reason: failure.reason, timestamp: failure.timestamp }))
}
/**
* Generates a contextual error message based on recent failures
*/
private generateContextualErrorMessage(
toolName: string,
recentFailures: Array<{ reason?: string; timestamp: number }>,
): string {
let message = t("tools:toolRepetitionLimitReached", { toolName })
if (recentFailures.length > 0) {
message += "\n\nRecent issues with this tool:"
const uniqueReasons = [...new Set(recentFailures.map((f) => f.reason).filter(Boolean))]
if (uniqueReasons.length > 0) {
message += "\n" + uniqueReasons.map((reason) => `${reason}`).join("\n")
}
message += "\n\nSuggestions:"
message += "\n• Try a different approach or tool"
message += "\n• Check if the parameters are correct"
message += "\n• Consider breaking down the task into smaller steps"
if (toolName === "apply_diff" || toolName === "write_to_file") {
message += "\n• Use read_file first to understand the current file content"
} else if (toolName === "execute_command") {
message += "\n• Verify the command syntax and file paths"
}
}
return message
}
/**
* Serializes a ToolUse object into a canonical JSON string for comparison
*