fix: improve write_to_file missing content error with actionable recovery hints

When write_to_file is called without the content parameter (typically due to
output token truncation on large files), the error feedback sent back to the
model was generic and unhelpful. The model would simply retry the same failing
approach, hitting the same token limit each time.

This change replaces the generic "missing parameter" error with a specific
message that:
- Explains the likely cause (output token truncation)
- Suggests concrete alternatives (create smaller file first, use edit_file,
  split into multiple files)
- Explicitly tells the model NOT to retry the same approach

Closes #11795
This commit is contained in:
Roo Code 2026-02-28 11:34:30 +00:00
parent 193053110b
commit 1f936e47c8
3 changed files with 83 additions and 1 deletions

View file

@ -66,6 +66,21 @@ Otherwise, if you have not completed the task and do not need additional informa
return `Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${instructions}`
},
writeToFileMissingContentError: () => {
const instructions = getToolInstructionsReminder()
return `Missing value for required parameter 'content'. This most commonly happens when the file content is too large and your response was truncated before the 'content' parameter could be fully generated.
To recover, try one of these approaches:
1. **Create a smaller file first**: Write a minimal version of the file with write_to_file, then use edit_file or apply_diff to add the remaining content incrementally.
2. **Use edit_file instead**: If modifying an existing file, use edit_file with targeted changes rather than rewriting the entire file.
3. **Split into multiple files**: If the content is genuinely large, consider splitting it across multiple smaller files.
Do NOT simply retry write_to_file with the same large content it will likely fail again for the same reason.
${instructions}`
},
invalidMcpToolArgumentError: (serverName: string, toolName: string) =>
JSON.stringify({
status: "error",

View file

@ -42,7 +42,13 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
if (newContent === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("write_to_file")
pushToolResult(await task.sayAndCreateMissingParamError("write_to_file", "content"))
await task.say(
"error",
`Roo tried to use write_to_file${
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter 'content'. This is likely due to output token limits. Retrying...`,
)
pushToolResult(formatResponse.toolError(formatResponse.writeToFileMissingContentError()))
await task.diffViewProvider.reset()
return
}

View file

@ -36,6 +36,10 @@ vi.mock("../../prompts/responses", () => ({
toolError: vi.fn((msg) => `Error: ${msg}`),
rooIgnoreError: vi.fn((path) => `Access denied: ${path}`),
createPrettyPatch: vi.fn(() => "mock-diff"),
writeToFileMissingContentError: vi.fn(
() =>
"Missing value for required parameter 'content'. This most commonly happens when the file content is too large and your response was truncated.",
),
},
}))
@ -442,6 +446,63 @@ describe("writeToFileTool", () => {
})
})
describe("missing parameter handling", () => {
it("returns enhanced error when content is missing, suggesting alternatives to large file writes", async () => {
// Directly build a toolUse with nativeArgs.content = undefined to simulate truncation
const toolUse: ToolUse = {
type: "tool_use",
name: "write_to_file",
params: { path: testFilePath },
nativeArgs: { path: testFilePath, content: undefined } as any,
partial: false,
}
mockPushToolResult = vi.fn()
await writeToFileTool.handle(mockCline, toolUse as ToolUse<"write_to_file">, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
})
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.say).toHaveBeenCalledWith(
"error",
expect.stringContaining("without value for required parameter 'content'"),
)
expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("output token limits"))
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("truncated"))
expect(mockCline.diffViewProvider.reset).toHaveBeenCalled()
// Should NOT call the generic sayAndCreateMissingParamError
expect(mockCline.sayAndCreateMissingParamError).not.toHaveBeenCalled()
})
it("returns generic error when path is missing", async () => {
// Directly build a toolUse with nativeArgs.path = undefined
const toolUse: ToolUse = {
type: "tool_use",
name: "write_to_file",
params: {},
nativeArgs: { path: undefined, content: testContent } as any,
partial: false,
}
mockPushToolResult = vi.fn()
await writeToFileTool.handle(mockCline, toolUse as ToolUse<"write_to_file">, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
})
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file")
expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("write_to_file", "path")
expect(mockCline.diffViewProvider.reset).toHaveBeenCalled()
})
})
describe("error handling", () => {
it("handles general file operation errors", async () => {
mockCline.diffViewProvider.open.mockRejectedValue(new Error("General error"))