From 93ab8a15a9d9b29c3433e91ae6caae95c0d8f9a8 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 3 Nov 2026 20:02:58 -0800 Subject: [PATCH 1/2] fix: mark failed tool results on the wire --- src/protocol/body.ts | 8 ++++++-- src/responses.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/protocol/body.ts b/src/protocol/body.ts index 5228c79..28153bf 100644 --- a/src/protocol/body.ts +++ b/src/protocol/body.ts @@ -42,7 +42,10 @@ type ResponsesInputItem = // Tool results carry a content array; the Responses API wants a string. Join // the text parts; non-text content is not representable here and is dropped // with a marker so the model is not misled into thinking it went missing -// silently. +// silently. Only the join and the `` wrapping match the Chat +// Completions adapter's `role: "tool"` encoding (whose schema likewise has +// no `is_error` field); the Chat adapter drops non-text content with no +// marker, so the two wires differ there. function toolResultText( block: Extract, ): string { @@ -51,7 +54,8 @@ function toolResultText( if (c.type === "text") parts.push(c.text); else parts.push(`[unsupported ${c.type} content omitted]`); } - return parts.join(""); + const text = parts.join("\n"); + return block.isError ? `\n${text}\n` : text; } // Grounded on the OpenAI Responses API document input shape: a base64 diff --git a/src/responses.test.ts b/src/responses.test.ts index 52f90ca..85ce2e5 100644 --- a/src/responses.test.ts +++ b/src/responses.test.ts @@ -526,6 +526,42 @@ describe("Responses request builder — tool-item dedupe", () => { }); }); +describe("Responses request builder — tool-result encoding", () => { + // The wire has no error flag; without the `` wrapper the model + // cannot tell a failed tool from a successful one. + test.each([ + { name: "success", isError: false, output: "a\nb" }, + { name: "error", isError: true, output: "\na\nb\n" }, + ])("encodes a $name result like the Chat adapter", ({ isError, output }) => { + const adapter = createOpenAIResponsesAdapter(source, {}); + const request = adapter.buildRequest( + [ + { + role: "user", + timestamp: 0, + content: [ + { + type: "tool_result", + callId: "call_1", + isError, + content: [ + { type: "text", text: "a" }, + { type: "text", text: "b" }, + ], + }, + ], + }, + ], + "model", + {}, + ); + const outputs = inputItemsOf(request).filter( + (item) => item["type"] === "function_call_output", + ); + expect(outputs[0]?.["output"]).toBe(output); + }); +}); + describe("Responses request builder — un-replayable reasoning", () => { const history = (model: string, signature: string): ConversationTurn[] => [ { role: "user", timestamp: 0, content: [{ type: "text", text: "hi" }] }, From 3e7ed953a46f80da5c33f12a091c496ff3d37504 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 3 Nov 2026 20:27:39 -0800 Subject: [PATCH 2/2] docs: say why failed tool results are wrapped --- src/protocol/body.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/protocol/body.ts b/src/protocol/body.ts index 28153bf..d3e7a0b 100644 --- a/src/protocol/body.ts +++ b/src/protocol/body.ts @@ -42,10 +42,8 @@ type ResponsesInputItem = // Tool results carry a content array; the Responses API wants a string. Join // the text parts; non-text content is not representable here and is dropped // with a marker so the model is not misled into thinking it went missing -// silently. Only the join and the `` wrapping match the Chat -// Completions adapter's `role: "tool"` encoding (whose schema likewise has -// no `is_error` field); the Chat adapter drops non-text content with no -// marker, so the two wires differ there. +// silently. The wire has no error flag, so a failed result is wrapped in +// `` as Interchange's Chat Completions adapter does. function toolResultText( block: Extract, ): string {