From 9b3e7ba70a5faf02b54e99727b9115147d3d2b7c Mon Sep 17 00:00:00 2001 From: Ritwik Date: Thu, 19 Feb 2026 22:15:44 -0800 Subject: [PATCH] fix: accept empty string result in attempt_completion tool Fixes #11404 (partial) The attempt_completion tool's nativeArgs parsing used a truthy check (if (args.result)) which rejected empty strings as invalid. Changed to an existence check (!== undefined) in both streaming and finalize parse paths. Added regression tests for: - Empty string result in parseToolCall - Non-empty result in parseToolCall - Empty string result in streaming path --- .../assistant-message/NativeToolCallParser.ts | 4 +- .../__tests__/NativeToolCallParser.spec.ts | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index bda7c71eb8d..ef5824528b9 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -449,7 +449,7 @@ export class NativeToolCallParser { break case "attempt_completion": - if (partialArgs.result) { + if (partialArgs.result !== undefined) { nativeArgs = { result: partialArgs.result } } break @@ -778,7 +778,7 @@ export class NativeToolCallParser { break case "attempt_completion": - if (args.result) { + if (args.result !== undefined) { nativeArgs = { result: args.result } as NativeArgsFor } break diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 2c15e12069c..20706af5a97 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -343,4 +343,59 @@ describe("NativeToolCallParser", () => { }) }) }) + + describe("attempt_completion tool", () => { + it("should parse attempt_completion with empty string result", () => { + // Regression test for issue #11404: empty string result should be valid + const toolCall = { + id: "toolu_completion_empty", + name: "attempt_completion" as const, + arguments: JSON.stringify({ + result: "", + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + if (result?.type === "tool_use") { + expect(result.nativeArgs).toBeDefined() + const nativeArgs = result.nativeArgs as { result: string } + expect(nativeArgs.result).toBe("") + } + }) + + it("should parse attempt_completion with non-empty result", () => { + const toolCall = { + id: "toolu_completion_full", + name: "attempt_completion" as const, + arguments: JSON.stringify({ + result: "Task completed successfully", + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + if (result?.type === "tool_use") { + expect(result.nativeArgs).toBeDefined() + const nativeArgs = result.nativeArgs as { result: string } + expect(nativeArgs.result).toBe("Task completed successfully") + } + }) + + it("should handle streaming attempt_completion with empty string result", () => { + const id = "toolu_streaming_completion" + NativeToolCallParser.startStreamingToolCall(id, "attempt_completion") + + const result = NativeToolCallParser.processStreamingChunk(id, JSON.stringify({ result: "" })) + + expect(result).not.toBeNull() + expect(result?.nativeArgs).toBeDefined() + const nativeArgs = result?.nativeArgs as { result: string } + expect(nativeArgs.result).toBe("") + }) + }) })