fix: allow partial args through parseToolCall finalization for write_to_file and other tools

When the model response gets truncated (e.g., due to output token limits),
required parameters like `content` for write_to_file arrive as `undefined`.
The finalization path in parseToolCall() required ALL params via AND conditions,
causing nativeArgs to stay undefined and the tool call to be silently dropped
or throw a generic error -- bypassing the tool's own user-friendly retry logic.

The streaming/partial path already uses OR conditions to allow partial args
through. This change aligns the finalization path with the same pattern,
so that partially-constructed args flow through to the tool's execute() method,
which can produce specific "missing parameter X, retrying..." messages that
get sent back to the model.

Fixes #12079
This commit is contained in:
Roo Code 2026-04-08 15:54:36 +00:00
parent eafed9705c
commit cb70c75b5b
2 changed files with 100 additions and 15 deletions

View file

@ -794,7 +794,7 @@ export class NativeToolCallParser {
break
case "apply_diff":
if (args.path !== undefined && args.diff !== undefined) {
if (args.path !== undefined || args.diff !== undefined) {
nativeArgs = {
path: args.path,
diff: args.diff,
@ -805,8 +805,8 @@ export class NativeToolCallParser {
case "edit":
case "search_and_replace":
if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.file_path !== undefined ||
args.old_string !== undefined ||
args.new_string !== undefined
) {
nativeArgs = {
@ -819,7 +819,7 @@ export class NativeToolCallParser {
break
case "ask_followup_question":
if (args.question !== undefined && args.follow_up !== undefined) {
if (args.question !== undefined || args.follow_up !== undefined) {
nativeArgs = {
question: args.question,
follow_up: args.follow_up,
@ -837,7 +837,7 @@ export class NativeToolCallParser {
break
case "generate_image":
if (args.prompt !== undefined && args.path !== undefined) {
if (args.prompt !== undefined || args.path !== undefined) {
nativeArgs = {
prompt: args.prompt,
path: args.path,
@ -865,7 +865,7 @@ export class NativeToolCallParser {
break
case "search_files":
if (args.path !== undefined && args.regex !== undefined) {
if (args.path !== undefined || args.regex !== undefined) {
nativeArgs = {
path: args.path,
regex: args.regex,
@ -875,7 +875,7 @@ export class NativeToolCallParser {
break
case "switch_mode":
if (args.mode_slug !== undefined && args.reason !== undefined) {
if (args.mode_slug !== undefined || args.reason !== undefined) {
nativeArgs = {
mode_slug: args.mode_slug,
reason: args.reason,
@ -903,7 +903,7 @@ export class NativeToolCallParser {
break
case "write_to_file":
if (args.path !== undefined && args.content !== undefined) {
if (args.path !== undefined || args.content !== undefined) {
nativeArgs = {
path: args.path,
content: args.content,
@ -912,7 +912,7 @@ export class NativeToolCallParser {
break
case "use_mcp_tool":
if (args.server_name !== undefined && args.tool_name !== undefined) {
if (args.server_name !== undefined || args.tool_name !== undefined) {
nativeArgs = {
server_name: args.server_name,
tool_name: args.tool_name,
@ -922,7 +922,7 @@ export class NativeToolCallParser {
break
case "access_mcp_resource":
if (args.server_name !== undefined && args.uri !== undefined) {
if (args.server_name !== undefined || args.uri !== undefined) {
nativeArgs = {
server_name: args.server_name,
uri: args.uri,
@ -940,8 +940,8 @@ export class NativeToolCallParser {
case "search_replace":
if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.file_path !== undefined ||
args.old_string !== undefined ||
args.new_string !== undefined
) {
nativeArgs = {
@ -954,8 +954,8 @@ export class NativeToolCallParser {
case "edit_file":
if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.file_path !== undefined ||
args.old_string !== undefined ||
args.new_string !== undefined
) {
nativeArgs = {
@ -977,7 +977,7 @@ export class NativeToolCallParser {
break
case "new_task":
if (args.mode !== undefined && args.message !== undefined) {
if (args.mode !== undefined || args.message !== undefined) {
nativeArgs = {
mode: args.mode,
message: args.message,

View file

@ -313,6 +313,91 @@ describe("NativeToolCallParser", () => {
})
})
describe("write_to_file tool", () => {
it("should parse write_to_file with both path and content", () => {
const toolCall = {
id: "toolu_wtf_1",
name: "write_to_file" as const,
arguments: JSON.stringify({
path: "src/test.ts",
content: "console.log('hello')",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
const nativeArgs = result.nativeArgs as { path: string; content: string }
expect(nativeArgs.path).toBe("src/test.ts")
expect(nativeArgs.content).toBe("console.log('hello')")
}
})
it("should parse write_to_file with missing content (truncated response)", () => {
const toolCall = {
id: "toolu_wtf_2",
name: "write_to_file" as const,
arguments: JSON.stringify({
path: "src/test.ts",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
const nativeArgs = result.nativeArgs as { path: string; content?: string }
expect(nativeArgs.path).toBe("src/test.ts")
expect(nativeArgs.content).toBeUndefined()
}
})
it("should parse write_to_file with missing path", () => {
const toolCall = {
id: "toolu_wtf_3",
name: "write_to_file" as const,
arguments: JSON.stringify({
content: "console.log('hello')",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
const nativeArgs = result.nativeArgs as { path?: string; content: string }
expect(nativeArgs.path).toBeUndefined()
expect(nativeArgs.content).toBe("console.log('hello')")
}
})
})
describe("apply_diff tool with missing params", () => {
it("should parse apply_diff with missing diff (truncated response)", () => {
const toolCall = {
id: "toolu_ad_1",
name: "apply_diff" as const,
arguments: JSON.stringify({
path: "src/test.ts",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result?.type).toBe("tool_use")
if (result?.type === "tool_use") {
const nativeArgs = result.nativeArgs as { path: string; diff?: string }
expect(nativeArgs.path).toBe("src/test.ts")
expect(nativeArgs.diff).toBeUndefined()
}
})
})
describe("finalizeStreamingToolCall", () => {
describe("read_file tool", () => {
it("should parse read_file args on finalize", () => {