Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/core/prompts/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 7 additions & 1 deletion src/core/tools/WriteToFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
61 changes: 61 additions & 0 deletions src/core/tools/__tests__/writeToFileTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
),
},
}))

Expand Down Expand Up @@ -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"))
Expand Down
Loading