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