From 1f936e47c88910330377dbef20399beeb82839a1 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 28 Feb 2026 11:34:30 +0000 Subject: [PATCH] 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 --- src/core/prompts/responses.ts | 15 +++++ src/core/tools/WriteToFileTool.ts | 8 ++- .../tools/__tests__/writeToFileTool.spec.ts | 61 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts index 60b5b4123ac..7a678de56ea 100644 --- a/src/core/prompts/responses.ts +++ b/src/core/prompts/responses.ts @@ -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", diff --git a/src/core/tools/WriteToFileTool.ts b/src/core/tools/WriteToFileTool.ts index c8455ef3d97..4c7fa333a09 100644 --- a/src/core/tools/WriteToFileTool.ts +++ b/src/core/tools/WriteToFileTool.ts @@ -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 } diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 6c63387ee10..78c5f024bbb 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -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"))