From 2b2015ce694ad47c846b82f0693ce4046267e701 Mon Sep 17 00:00:00 2001 From: RGB-loop <91311486+RGB-loop@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:14:36 +0000 Subject: [PATCH 1/2] Accept native rendering of blank and padded Claude function results The pinned Claude harness replaces a function result without an image or visible text with its own placeholder (for example "Unknown error"), and it trims joined error text. The exact receipt check failed the Turn with execution_failed for those results. Blank results are now confirmed by their native outcome, and error text is compared after the same trim; other results keep exact matching. Co-Authored-By: Claude Opus 5.5 --- .../claude-sdk-adapter/src/function_bridge.ts | 18 ++++++-- .../tests/function_bridge.test.mjs | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/claude-sdk-adapter/src/function_bridge.ts b/packages/claude-sdk-adapter/src/function_bridge.ts index 777d9ea15..63ab91356 100644 --- a/packages/claude-sdk-adapter/src/function_bridge.ts +++ b/packages/claude-sdk-adapter/src/function_bridge.ts @@ -85,9 +85,12 @@ export class FunctionBridge { this.cancelUnanswered(block.tool_use_id); continue; } - // Native error results join MCP text blocks; retain the original parts in the host. - const matches = result.success ? matchesNativeContent(result.content, block.content) : - block.content === result.content.map(part => part.type === "input_text" ? part.text : "").join("\n"); + // Native error results join and trim MCP text blocks; retain the original parts in the host. + // The native harness replaces a result without an image or visible text with its own + // placeholder, so the outcome alone confirms a blank result. + const matches = isBlankResult(result.content) ? isNativeText(block.content) : + result.success ? matchesNativeContent(result.content, block.content) : + block.content === result.content.map(part => part.type === "input_text" ? part.text : "").join("\n").trim(); if (!!block.is_error !== !result.success || !matches) { throw new Error("Native function response differs from submitted result."); } @@ -137,3 +140,12 @@ function matchesNativeContent(content: InputContent[], actual: unknown): boolean } catch { return false; } }); } + +function isBlankResult(content: InputContent[]): boolean { + return content.every(part => part.type === "input_text" && !part.text.trim()); +} + +function isNativeText(actual: unknown): boolean { + return typeof actual === "string" || + Array.isArray(actual) && actual.every(part => part?.type === "text" && typeof part.text === "string"); +} diff --git a/packages/claude-sdk-adapter/tests/function_bridge.test.mjs b/packages/claude-sdk-adapter/tests/function_bridge.test.mjs index bd78b0e27..47b5538dc 100644 --- a/packages/claude-sdk-adapter/tests/function_bridge.test.mjs +++ b/packages/claude-sdk-adapter/tests/function_bridge.test.mjs @@ -140,3 +140,47 @@ test("native cancellation preserves a submitted result's missing receipt", async assert.throws(() => bridge.assertComplete(), /unconfirmed/); bridge.close(); }); + +test("blank results accept the native placeholder and error text is compared as natively trimmed", async () => { + const text = value => ({ type: "input_text", text: value }); + const blank = [ + ["empty", true, [], "(mcp__functions__lookup completed with no output)"], + ["empty-text", true, [text("")], "(mcp__functions__lookup completed with no output)"], + ["whitespace", true, [text(" \n")], "(mcp__functions__lookup completed with no output)"], + ["empty-error", false, [], "Unknown error"], + ["empty-text-error", false, [text("")], "Command failed with no output"], + ]; + const events = []; + const bridge = new FunctionBridge(async event => { events.push(event); }); + for (const [id, success, content, placeholder] of blank) { + const value = { ...result(id, success), content }; + const waiting = bridge.invoke(call(id), new AbortController().signal); + bridge.submit(JSON.stringify(value)); + assert.equal((await waiting).isError, !success); + await assert.rejects(bridge.consume(native({ ...value, success: !success }, placeholder), "native"), /differs/); + await bridge.consume(native(value, placeholder), "native"); + } + assert.deepEqual(events.filter(event => event.type === "function_applied").map(event => event.call_id), blank.map(([id]) => id)); + bridge.assertComplete(); + + for (const [id, content, received] of [["trailing-empty", [text("x"), text("")], "x"], + ["trailing-newline", [text("boom\n")], "boom"], ["empty-between", [text("x"), text(""), text("y")], "x\n\ny"]]) { + const value = { ...result(id, false), content }; + const waiting = bridge.invoke(call(id), new AbortController().signal); + bridge.submit(JSON.stringify(value)); + await waiting; + await bridge.consume(native(value, received), "native"); + } + assert.equal(events.at(-1).call_id, "empty-between"); + bridge.assertComplete(); + + for (const [id, success, received] of [["text", true, [{ type: "text", text: "43" }]], ["error", false, "43"]]) { + const value = { ...result(id, success), content: [text("42")] }; + const waiting = bridge.invoke(call(id), new AbortController().signal); + bridge.submit(JSON.stringify(value)); + await waiting; + await assert.rejects(bridge.consume(native(value, received), "native"), /differs/); + } + assert.throws(() => bridge.assertComplete(), /unconfirmed/); + bridge.close(); +}); From 66cc5f561653dfa7bc4f29eed73e68d745990799 Mon Sep 17 00:00:00 2001 From: RGB-loop <91311486+RGB-loop@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:45:59 +0000 Subject: [PATCH 2/2] Document the native rendering of blank and trimmed Claude function results The adapter README now states that a blank result is confirmed by its native outcome and that error text is compared after the native join and trim. The OpenAI compatibility ledger records the model-visible placeholder and trimmed error text, with the Chinese mirror and source hash updated. The unit test also covers whitespace-only errors and leading whitespace in error text. Co-Authored-By: Claude Opus 5.5 --- contracts/agents-api/index.md | 1 + contracts/agents-api/zh/index.md | 3 ++- packages/claude-sdk-adapter/README.md | 2 +- packages/claude-sdk-adapter/tests/function_bridge.test.mjs | 4 +++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index 67f23d1de..2d15e35f5 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -81,6 +81,7 @@ Each item is Core's deliberate or native behavior where the official service beh - The Items list includes in-progress and incomplete output Items, and keeps a failed function result's submitted `output`. - Error messages omit the call and executor IDs that official messages include. - An empty text part beside other text is accepted and stored. +- Claude Code shows the model its own placeholder text for a function result with no image and no non-blank text, and trims surrounding whitespace from function error text. - Empty input returns 400 `invalid_request` with a generic message and a null param; the official response is `invalid_request_error` with param `input`. - Session usage is available as soon as every root Turn has settled; official reads lag by seconds. diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index b45440123..a77b8bf71 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: 5032e5d52040d39196c2cfeb46daa004d268c766a440b155fe34129500207772 +source_hash: a18ebc428c0610870c2d231d8d51c187b4096e2980f1a4950b72545529917fbd --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -84,6 +84,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - Items 列表会包含进行中和未完成的输出 Items,并保留失败函数结果中已提交的 `output`。 - 错误消息会省略官方消息包含的 call 和 executor ID。 - 与其他文本并存的空文本部分可被接受并存储。 +- 对于既没有图像、也没有非空白文本的函数结果,Claude Code 会向模型显示其自身的占位文本;函数错误文本的首尾空白会被去除。 - 空输入会返回 400 `invalid_request`,附带通用消息和值为 null 的 param;官方响应为 `invalid_request_error`,param 为 `input`。 - 一旦所有根级 Turn 均已进入终态,Session 用量即可用;官方读取会滞后数秒。 diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index 4f9082e20..e5d34a8e6 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -80,7 +80,7 @@ The `none` profile accepts only text, explicit model/system instructions, manage The internal SDK function-server helper uses the maintained MCP server's public request handlers and standard Tool/CallToolResult types. It snapshots definitions and forwards JSON Schema without a JSON Schema-to-Zod conversion; supplied tools are always loaded. Native call identity comes from the pinned harness's `claudecode/toolUseId` MCP metadata, independently of request IDs, names or arrival order. Missing identities and undeclared tools fail before invoking the host. Return content/error fields unchanged over MCP and forward its per-request abort signal. The private Go factory connects declared functions through this helper and reuses the daemon function-call/result interface and neutral observations. The native function-server registry must contain exactly those functions. SDK allowlisting admits only these host callbacks; the host still owns result decisions and any business permission checks. It grants no runtime-token business authority. -Function results remain pending after stdin/MCP delivery. A matching live, root native user tool_result confirms application only when its Session/call identity, error flag and ordered content match the submission. Text matches exactly; each submitted image position must remain a valid native base64 image. Native resizing or re-encoding may change image bytes. This acknowledges incorporation into native history, not byte/pixel fidelity or completed provider consumption. Public Items retain the original caller content; real image-dependent model responses separately qualify usability. Ignore replayed, synthetic and subagent messages. Native error text joins the submitted text parts with newlines; neutral observations retain their original order and separate failure status. Missing/mismatched receipts fail the execution; do not replay unknown delivery. Result submission waits at most ten seconds for a receipt and cancels uncertain execution on timeout. Invalid or unsupported image results fail before consuming a pending call. Function state belongs to one live Run and ends with it; the router owns receipt retry/conflict handling. This does not establish crash recovery or exactly-once effects. +Function results remain pending after stdin/MCP delivery. A matching live, root native user tool_result confirms application only when its Session/call identity, error flag and ordered content match the submission. Text matches exactly, except as noted for blank results and error text below; each submitted image position must remain a valid native base64 image. Native resizing or re-encoding may change image bytes. This acknowledges incorporation into native history, not byte/pixel fidelity or completed provider consumption. Public Items retain the original caller content; real image-dependent model responses separately qualify usability. Ignore replayed, synthetic and subagent messages. A result with no image and no non-blank text is confirmed by any native text tool_result with the same error flag, because the native harness replaces it with its own placeholder. Native error text joins the submitted text parts with newlines and trims surrounding whitespace, so the receipt compares the trimmed joined text; neutral observations retain their original order and separate failure status. Missing/mismatched receipts fail the execution; do not replay unknown delivery. Result submission waits at most ten seconds for a receipt and cancels uncertain execution on timeout. Invalid or unsupported image results fail before consuming a pending call. Function state belongs to one live Run and ends with it; the router owns receipt retry/conflict handling. This does not establish crash recovery or exactly-once effects. ### Usage diff --git a/packages/claude-sdk-adapter/tests/function_bridge.test.mjs b/packages/claude-sdk-adapter/tests/function_bridge.test.mjs index 47b5538dc..e7637f714 100644 --- a/packages/claude-sdk-adapter/tests/function_bridge.test.mjs +++ b/packages/claude-sdk-adapter/tests/function_bridge.test.mjs @@ -149,6 +149,7 @@ test("blank results accept the native placeholder and error text is compared as ["whitespace", true, [text(" \n")], "(mcp__functions__lookup completed with no output)"], ["empty-error", false, [], "Unknown error"], ["empty-text-error", false, [text("")], "Command failed with no output"], + ["whitespace-error", false, [text(" \n")], "Command failed with no output"], ]; const events = []; const bridge = new FunctionBridge(async event => { events.push(event); }); @@ -164,7 +165,8 @@ test("blank results accept the native placeholder and error text is compared as bridge.assertComplete(); for (const [id, content, received] of [["trailing-empty", [text("x"), text("")], "x"], - ["trailing-newline", [text("boom\n")], "boom"], ["empty-between", [text("x"), text(""), text("y")], "x\n\ny"]]) { + ["trailing-newline", [text("boom\n")], "boom"], ["leading-space", [text(" boom")], "boom"], + ["leading-empty", [text(""), text("y")], "y"], ["empty-between", [text("x"), text(""), text("y")], "x\n\ny"]]) { const value = { ...result(id, false), content }; const waiting = bridge.invoke(call(id), new AbortController().signal); bridge.submit(JSON.stringify(value));