Repository navigation
fix(responses): compile the apply_patch body recognition accepted #5051
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,33 +9,43 @@ function isPlainObject(value: unknown): value is Record<string, unknown> { | |
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| function unwrapPatchInput(value: string): string { | ||
| try { | ||
| const parsed: unknown = JSON.parse(value); | ||
| if (isPlainObject(parsed)) { | ||
| if (typeof parsed.input === "string") return parsed.input; | ||
| if (typeof parsed.patch === "string") return parsed.patch; | ||
| } | ||
| } catch { | ||
| // Native custom calls carry the patch body directly. | ||
| } | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Convert a nested Code Mode helper call into unified-exec JavaScript. | ||
| * | ||
| * Parsed values are serialized as data, never interpolated as source, so command and patch text | ||
| * cannot escape the generated call. Invalid structured helper payloads are also passed as data so | ||
| * nested-tool validation can reject them without evaluating provider text as JavaScript. | ||
| * | ||
| * `wireToolName` is the name the body actually arrived under, which is not always the helper: | ||
| * a provider that got the NAME right and the BODY wrong sends `exec`, and the apply-patch | ||
| * helper is inferred from the payload. Recognition and compilation must read ONE canonical | ||
| * body, so both unwrap under that same name — see the apply-patch branch below. It defaults to | ||
| * the helper name, which is correct for the name-based path where the wire name IS the helper. | ||
| */ | ||
| export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string { | ||
| export function compileCodeModeHelperInput( | ||
| argumentsText: unknown, | ||
| toolName: string, | ||
| wireToolName?: string, | ||
| ): string { | ||
| if (typeof argumentsText !== "string") return ""; | ||
| const helperName = toolName.startsWith("default.") | ||
| ? toolName.slice("default.".length) | ||
| : toolName; | ||
| if (helperName === "apply_patch") { | ||
| const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText)); | ||
| // `resolveCodeModeHelperName` decides this IS an apply-patch call by reading | ||
| // `unwrapFreeformToolInput(argumentsText, wireToolName)`, which strips an outer Markdown | ||
| // fence and accepts that name's fallback fields (#4983). Compiling from a narrower unwrap | ||
| // meant a body accepted through a fence or a fallback field reached `tools.apply_patch` | ||
| // still wrapped, so the host rejected the JSON or the fence instead of applying the patch | ||
| // (#5046). One unwrap, one body, one decision. | ||
| // | ||
| // The vocabulary is the WIRE name rather than the helper name on purpose. `{"patch": ...}` | ||
| // is an apply_patch wrapper and is not an `exec` fallback field, and the recognizer already | ||
| // declines it under `exec`; reading it here would compile a body that recognition rejected, | ||
| // which is exactly the drift a second, looser unwrap introduces. | ||
| const patch = normalizeApplyPatchDelimiters( | ||
| unwrapFreeformToolInput(argumentsText, wireToolName ?? helperName), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a code-mode provider emits the supported Useful? React with 👍 / 👎. |
||
| ); | ||
| return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`; | ||
| } | ||
| let parsed: unknown = argumentsText; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new test is absent from both AGENTS.md reference: AGENTS.md:L23-L27 Useful? React with 👍 / 👎. |
||
| import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../../src/responses/code-mode-helper-compat"; | ||
| import { restoreRoutedCustomCallsInJson } from "../../src/responses/custom-tool-compat"; | ||
|
|
||
| /** | ||
| * Recognition and compilation must read ONE canonical body (#5046). | ||
| * | ||
| * #4983 widened what counts as an apply-patch call under a code-mode `exec` catalog: an | ||
| * outer Markdown fence, and any single fallback field of that tool name. Recognition used | ||
| * the widened unwrap; compilation kept a narrower one that saw only `input` and `patch`. So | ||
| * a body accepted through a fence or through `content` reached `tools.apply_patch` still | ||
| * wrapped, and the host rejected the JSON text or the fence instead of applying the patch. | ||
| * | ||
| * These cases are written against the pair, not against either half, because the defect was | ||
| * that the two halves disagreed while each looked correct alone. | ||
| */ | ||
| const PATCH = "*** Begin Patch\n*** Update File: README.md\n@@\n-old\n+new\n*** End Patch"; | ||
| const EXPECTED = `const result = await tools.apply_patch(${JSON.stringify(PATCH)});\ntext(result);`; | ||
| const CODE_MODE = new Set(["exec"]); | ||
|
|
||
| /** Exactly what every bridge call site does: recognize, then compile under the wire name. */ | ||
| function compileAsBridge(body: string, wireName = "exec", declared = CODE_MODE): string | undefined { | ||
| const helper = resolveCodeModeHelperName(undefined, wireName, body, undefined, declared); | ||
| return helper ? compileCodeModeHelperInput(body, helper, wireName) : undefined; | ||
| } | ||
|
|
||
| describe("code-mode apply_patch compiles the body recognition accepted", () => { | ||
| test("every accepted exec fallback field compiles to the same raw patch", () => { | ||
| // The list is `FREEFORM_FALLBACK_KEYS.exec` plus the `input` wrapper. If a key is added | ||
| // there and not here, the two lists have drifted and the next reader should be told. | ||
| for (const key of ["input", "code", "script", "js", "javascript", "command", "cmd", "content"]) { | ||
| const body = JSON.stringify({ [key]: PATCH }); | ||
| expect({ key, source: compileAsBridge(body) }).toEqual({ key, source: EXPECTED }); | ||
| } | ||
| }); | ||
|
|
||
| test("fenced and unfenced forms compile identically", () => { | ||
| const fenced = "```\n" + PATCH + "\n```"; | ||
| expect(compileAsBridge(PATCH)).toBe(EXPECTED); | ||
| expect(compileAsBridge(fenced)).toBe(EXPECTED); | ||
| expect(compileAsBridge(JSON.stringify({ input: fenced }))).toBe(EXPECTED); | ||
| expect(compileAsBridge("```diff\n" + PATCH + "\n```")).toBe(EXPECTED); | ||
| }); | ||
|
|
||
| test("a native apply_patch call keeps its own vocabulary", () => { | ||
| // The name-based path arrives under `apply_patch`, whose fallback keys are `patch` and | ||
| // `content`. `{"patch": ...}` is meaningful there and is NOT an exec fallback field, so | ||
| // the two names deliberately answer differently — which is why the wire name, not the | ||
| // helper name, decides. | ||
| expect(compileCodeModeHelperInput(JSON.stringify({ patch: PATCH }), "apply_patch")).toBe(EXPECTED); | ||
| expect(compileCodeModeHelperInput(PATCH, "apply_patch")).toBe(EXPECTED); | ||
| expect(compileAsBridge(JSON.stringify({ patch: PATCH }))).toBeUndefined(); | ||
| }); | ||
|
Comment on lines
+21
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Add production-bridge coverage for fallback and fenced inputs. The tests in Add representative non- 🤖 Prompt for AI Agents |
||
|
|
||
| test("a normal code-mode JavaScript body is left alone", () => { | ||
| for (const body of [ | ||
| 'const result = await tools.exec_command({ cmd: "ls" });\ntext(result);', | ||
| 'await tools.apply_patch("*** Begin Patch\\n*** Add File: a.txt\\n+hi\\n*** End Patch");', | ||
| JSON.stringify({ input: 'const a = 1;\ntext(a);' }), | ||
| ]) { | ||
| expect(resolveCodeModeHelperName(undefined, "exec", body, undefined, CODE_MODE)).toBeUndefined(); | ||
| } | ||
| }); | ||
|
|
||
| test("a caller-defined exec outside a code-mode catalog is never reinterpreted", () => { | ||
| // Without a genuine code-mode catalog an `exec` that takes patch text is a legitimate | ||
| // caller tool, and handing it generated JavaScript would be the mis-route this repair | ||
| // exists to avoid. The widened unwrap must not change that. | ||
| for (const body of [PATCH, JSON.stringify({ content: PATCH }), "```\n" + PATCH + "\n```"]) { | ||
| expect(compileAsBridge(body, "exec", new Set(["shell"]))).toBeUndefined(); | ||
| // A catalog that also declares a legacy shell bridge is not code mode either, and no | ||
| // declared set at all is the plainest case of the same rule. | ||
| expect(compileAsBridge(body, "exec", new Set(["exec", "shell_command"]))).toBeUndefined(); | ||
| expect(resolveCodeModeHelperName(undefined, "exec", body, undefined, undefined)).toBeUndefined(); | ||
| } | ||
| }); | ||
|
|
||
| test("the compiled helper survives the bridge, not only the recognizer", () => { | ||
| // The unit above proves the pair agrees. This proves the agreement reaches the item a | ||
| // client actually receives: before the fix this restored | ||
| // `tools.apply_patch("{\"content\":\"*** Begin Patch...\"}")`. | ||
| for (const key of ["content", "code", "input"]) { | ||
| const upstream = JSON.stringify({ | ||
| id: "resp_patch", | ||
| output: [{ | ||
| type: "function_call", | ||
| id: "fc_patch", | ||
| call_id: "call_patch", | ||
| name: "exec", | ||
| arguments: JSON.stringify({ [key]: PATCH }), | ||
| status: "completed", | ||
| }], | ||
| }); | ||
| const restored = JSON.parse(restoreRoutedCustomCallsInJson( | ||
| upstream, | ||
| new Set(["exec"]), | ||
| new Set(), | ||
| CODE_MODE, | ||
| )) as { output: Array<Record<string, unknown>> }; | ||
| expect({ key, item: restored.output[0] }).toMatchObject({ | ||
| key, | ||
| item: { type: "custom_tool_call", name: "exec", input: EXPECTED }, | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes shared Responses tool-call compilation across the mapped
src/responses/andsrc/server/areas without updating anystructure/document. Update the applicable documents listed for those source areas instructure/INDEX.md, particularly the runtime and Responses transport contracts, to describe the canonical-body compilation behavior.AGENTS.md reference: src/AGENTS.md:L10-L11
Useful? React with 👍 / 👎.