This commit is contained in:
roomote-v0[bot] 2026-04-10 16:18:26 +00:00 committed by GitHub
commit 0a16cbf02c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 3 deletions

View file

@ -465,7 +465,7 @@ export class NativeToolCallParser {
break
case "write_to_file":
if (partialArgs.path || partialArgs.content) {
if (partialArgs.path !== undefined && partialArgs.content !== undefined) {
nativeArgs = {
path: partialArgs.path,
content: partialArgs.content,

View file

@ -313,6 +313,107 @@ describe("NativeToolCallParser", () => {
})
})
describe("write_to_file partial streaming", () => {
it("should not set nativeArgs when only path is present (content missing)", () => {
const id = "toolu_wtf_partial_1"
NativeToolCallParser.startStreamingToolCall(id, "write_to_file")
// Simulate partial chunk with only path, no content yet
const result = NativeToolCallParser.processStreamingChunk(id, JSON.stringify({ path: "test.txt" }))
// nativeArgs should NOT be set when content is missing
expect(result).not.toBeNull()
if (result) {
expect(result.nativeArgs).toBeUndefined()
// params should still have path for UI display
expect(result.params.path).toBe("test.txt")
}
})
it("should set nativeArgs when both path and content are present", () => {
const id = "toolu_wtf_partial_2"
NativeToolCallParser.startStreamingToolCall(id, "write_to_file")
const result = NativeToolCallParser.processStreamingChunk(
id,
JSON.stringify({ path: "test.txt", content: "hello world" }),
)
expect(result).not.toBeNull()
if (result) {
expect(result.nativeArgs).toBeDefined()
const nativeArgs = result.nativeArgs as { path: string; content: string }
expect(nativeArgs.path).toBe("test.txt")
expect(nativeArgs.content).toBe("hello world")
}
})
})
describe("write_to_file parseToolCall (complete)", () => {
it("should return null when content is missing from complete tool call", () => {
const toolCall = {
id: "toolu_wtf_complete_1",
name: "write_to_file" as const,
arguments: JSON.stringify({ path: "test.txt" }),
}
// parseToolCall throws internally and returns null for invalid args
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).toBeNull()
})
it("should parse correctly when both path and content are present", () => {
const toolCall = {
id: "toolu_wtf_complete_2",
name: "write_to_file" as const,
arguments: JSON.stringify({ path: "test.txt", content: "hello world" }),
}
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 { path: string; content: string }
expect(nativeArgs.path).toBe("test.txt")
expect(nativeArgs.content).toBe("hello world")
}
})
})
describe("write_to_file finalization fallback", () => {
it("should return null when finalized with missing content", () => {
const id = "toolu_wtf_finalize_1"
NativeToolCallParser.startStreamingToolCall(id, "write_to_file")
// Stream only path, no content
NativeToolCallParser.processStreamingChunk(id, JSON.stringify({ path: "test.txt" }))
// Finalization should fail (return null) because content is missing
const result = NativeToolCallParser.finalizeStreamingToolCall(id)
expect(result).toBeNull()
})
it("should finalize successfully when both path and content are present", () => {
const id = "toolu_wtf_finalize_2"
NativeToolCallParser.startStreamingToolCall(id, "write_to_file")
NativeToolCallParser.processStreamingChunk(
id,
JSON.stringify({ path: "test.txt", content: "file content" }),
)
const result = NativeToolCallParser.finalizeStreamingToolCall(id)
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("test.txt")
expect(nativeArgs.content).toBe("file content")
}
})
})
describe("finalizeStreamingToolCall", () => {
describe("read_file tool", () => {
it("should parse read_file args on finalize", () => {

View file

@ -2967,6 +2967,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const existingToolUse = this.assistantMessageContent[toolUseIndex]
if (existingToolUse && existingToolUse.type === "tool_use") {
existingToolUse.partial = false
// Clear stale nativeArgs from partial parsing so the safety check
// in presentAssistantMessage (!block.nativeArgs) properly catches
// this as an invalid tool call. Without this, incomplete partial
// nativeArgs (e.g., path set but content undefined) would bypass
// the check and cause tools to execute with missing parameters.
existingToolUse.nativeArgs = undefined
// Ensure it has the ID for native protocol
;(existingToolUse as any).id = event.id
}
@ -3350,11 +3356,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
presentAssistantMessage(this)
} else if (toolUseIndex !== undefined) {
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
// We still need to mark the tool as non-partial so it gets executed
// The tool's validation will catch any missing required parameters
// We still need to mark the tool as non-partial so it gets presented.
// The presentAssistantMessage safety check will catch missing nativeArgs.
const existingToolUse = this.assistantMessageContent[toolUseIndex]
if (existingToolUse && existingToolUse.type === "tool_use") {
existingToolUse.partial = false
// Clear stale nativeArgs from partial parsing so the safety check
// in presentAssistantMessage (!block.nativeArgs) properly catches
// this as an invalid tool call. Without this, incomplete partial
// nativeArgs (e.g., path set but content undefined) would bypass
// the check and cause tools to execute with missing parameters.
existingToolUse.nativeArgs = undefined
// Ensure it has the ID for native protocol
;(existingToolUse as any).id = event.id
}