From a2ec01e12fb9088e5ee60bc7e3eb5070c034f508 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sat, 18 Apr 2026 19:29:23 +0300 Subject: [PATCH] fix(coding-agent): coerce stringified JSON edits in edit tool (#3370) Some models (Opus 4.6, GLM-5.1) send the edits parameter as a JSON string instead of a parsed array. This fails AJV validation with 'must be array' and models fall back to sed/python. Parse stringified edits in prepareEditArguments before validation. --- packages/coding-agent/src/core/tools/edit.ts | 22 +++++++++++----- .../test/edit-tool-legacy-input.test.ts | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index aac8cbbe9..e4bba04de 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -92,14 +92,24 @@ function prepareEditArguments(input: unknown): EditToolInput { return input as EditToolInput; } - const args = input as LegacyEditToolInput; - if (typeof args.oldText !== "string" || typeof args.newText !== "string") { - return input as EditToolInput; + const args = input as Record; + + // Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array + if (typeof args.edits === "string") { + try { + const parsed = JSON.parse(args.edits); + if (Array.isArray(parsed)) args.edits = parsed; + } catch {} } - const edits = Array.isArray(args.edits) ? [...args.edits] : []; - edits.push({ oldText: args.oldText, newText: args.newText }); - const { oldText: _oldText, newText: _newText, ...rest } = args; + const legacy = args as LegacyEditToolInput; + if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") { + return args as EditToolInput; + } + + const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : []; + edits.push({ oldText: legacy.oldText, newText: legacy.newText }); + const { oldText: _oldText, newText: _newText, ...rest } = legacy; return { ...rest, edits } as EditToolInput; } diff --git a/packages/coding-agent/test/edit-tool-legacy-input.test.ts b/packages/coding-agent/test/edit-tool-legacy-input.test.ts index d78acc655..13a8bbeb5 100644 --- a/packages/coding-agent/test/edit-tool-legacy-input.test.ts +++ b/packages/coding-agent/test/edit-tool-legacy-input.test.ts @@ -88,3 +88,29 @@ describe("edit tool prepareArguments", () => { expect(await readFile(filePath, "utf8")).toBe("after\n"); }); }); + +describe("edit tool stringified edits", () => { + it("parses edits from a JSON string", () => { + const definition = createEditToolDefinition(process.cwd()); + const prepared = definition.prepareArguments!({ + path: "file.txt", + edits: JSON.stringify([{ oldText: "a", newText: "b" }]), + }); + expect(prepared).toEqual({ + path: "file.txt", + edits: [{ oldText: "a", newText: "b" }], + }); + }); + + it("leaves edits alone when the string is not valid JSON", () => { + const definition = createEditToolDefinition(process.cwd()); + const prepared = definition.prepareArguments!({ + path: "file.txt", + edits: "not json", + }); + expect(prepared).toEqual({ + path: "file.txt", + edits: "not json", + }); + }); +});