Tighten Claude partial tool input parsing

This commit is contained in:
Mohamed Boudra
2026-03-25 20:38:43 +07:00
parent 08f25f065a
commit 508cd524a8
3 changed files with 12 additions and 21 deletions
@@ -441,7 +441,7 @@ describe("ClaudeAgentSession redesign invariants", () => {
} }
}); });
test("routes input_json_delta through partial parsing before buffered JSON is complete", async () => { test("waits for complete JSON values before updating tool input from input_json_delta", async () => {
const session = await createSession(); const session = await createSession();
const internal = session as unknown as { const internal = session as unknown as {
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[]; mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
@@ -479,7 +479,7 @@ describe("ClaudeAgentSession redesign invariants", () => {
partial_json: '{"command":"echo ', partial_json: '{"command":"echo ',
}, },
}, },
expectedCommand: "echo ", expectedCommand: "echo seed",
}, },
{ {
event: { event: {
@@ -520,7 +520,7 @@ describe("ClaudeAgentSession redesign invariants", () => {
} }
}); });
test("surfaces canonical partial tool input from input_json_delta before JSON is complete", async () => { test("does not surface incomplete string values from input_json_delta", async () => {
const session = await createSession(); const session = await createSession();
const internal = session as unknown as { const internal = session as unknown as {
mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[]; mapPartialEvent: (event: Record<string, unknown>) => AgentTimelineItem[];
@@ -551,7 +551,6 @@ describe("ClaudeAgentSession redesign invariants", () => {
expect(internal.toolUseCache.get(toolUseId)?.input).toEqual({ expect(internal.toolUseCache.get(toolUseId)?.input).toEqual({
file_path: "src/message.tsx", file_path: "src/message.tsx",
old_string: "before",
}); });
} finally { } finally {
await session.close(); await session.close();
@@ -13,33 +13,25 @@ describe("parsePartialJsonObject", () => {
}); });
}); });
it("keeps partial string values without field-specific logic", () => { it("does not emit incomplete string values", () => {
expect(parsePartialJsonObject('{"command":"echo ')).toEqual({ expect(parsePartialJsonObject('{"command":"echo ')).toEqual({
value: { value: {},
command: "echo ",
},
complete: false, complete: false,
}); });
}); });
it("returns parsed prefix fields from incomplete objects", () => { it("returns only complete prefix fields from incomplete objects", () => {
expect(parsePartialJsonObject('{"file_path":"src/message.tsx","old_string":"before')).toEqual({ expect(parsePartialJsonObject('{"file_path":"src/message.tsx","old_string":"before')).toEqual({
value: { value: {
file_path: "src/message.tsx", file_path: "src/message.tsx",
old_string: "before",
}, },
complete: false, complete: false,
}); });
}); });
it("parses nested partial values generically", () => { it("does not emit incomplete nested values", () => {
expect(parsePartialJsonObject('{"payload":{"path":"src/index.ts","content":"hello')).toEqual({ expect(parsePartialJsonObject('{"payload":{"path":"src/index.ts","content":"hello')).toEqual({
value: { value: {},
payload: {
path: "src/index.ts",
content: "hello",
},
},
complete: false, complete: false,
}); });
}); });
@@ -271,17 +271,17 @@ function parsePartialObject(
}; };
} }
value[parsedKey.value] = parsedMemberValue.value;
currentIndex = skipWhitespace(input, parsedMemberValue.nextIndex);
if (!parsedMemberValue.complete) { if (!parsedMemberValue.complete) {
return { return {
value, value,
nextIndex: currentIndex, nextIndex: skipWhitespace(input, parsedMemberValue.nextIndex),
complete: false, complete: false,
}; };
} }
value[parsedKey.value] = parsedMemberValue.value;
currentIndex = skipWhitespace(input, parsedMemberValue.nextIndex);
const delimiter = input[currentIndex]; const delimiter = input[currentIndex];
if (delimiter === ",") { if (delimiter === ",") {
currentIndex += 1; currentIndex += 1;