fix(ai): preserve tool-result boundary whitespace for Mistral and Ollama (#128266)

The shared formatToolResultText helper emitted its trim() result instead
of using it as an emptiness predicate only, so native Mistral and Ollama
requests silently stripped leading indentation and trailing
whitespace from durable tool output (#127587). Chat Completions already
emits the original sanitized text and uses trim solely to detect blank
content; align the shared formatter with that contract. Blank output
keeps the placeholder fallback, error prefix, and omitted-media suffix.

Closes #127587

AI-assisted (Claude); shared + Mistral + Ollama boundary regression
tests fail pre-fix and pass post-fix.

Co-authored-by: Parker Fawcett <Parkerscottfawcett@gmail.com>
This commit is contained in:
Parker Fawcett
2026-08-24 00:15:55 -06:00
committed by GitHub
parent 1e7ad721f7
commit 6a65e79529
4 changed files with 65 additions and 1 deletions
@@ -802,6 +802,17 @@ describe("convertToOllamaMessages", () => {
expect(result).toEqual([{ role: "tool", content: "file1.txt\nfile2.txt" }]);
});
it("preserves significant boundary whitespace in tool results", () => {
const result = convertToOllamaMessages([
{
role: "toolResult",
toolCallId: "call_ws",
content: [{ type: "text", text: " indented\n" }],
},
]);
expect(result).toEqual([{ role: "tool", content: " indented\n", tool_call_id: "call_ws" }]);
});
it("converts SDK 'toolResult' role to Ollama 'tool' role", () => {
const messages = [{ role: "toolResult", content: "command output here" }];
const result = convertToOllamaMessages(messages);
+16
View File
@@ -802,6 +802,22 @@ describe("Mistral provider", () => {
});
});
it("preserves tool-result boundary whitespace in the request payload", async () => {
const testContext = makeMistralToolResultContext("read_file", [
{ type: "text", text: " indented\n" },
]);
await runMistralFixture(testContext);
const payload = mistralMockState.payloads[0] as {
messages: Array<{ role: string; content: Array<{ type: string; text?: string }> }>;
};
const toolMessage = payload.messages.find((message) => message.role === "tool");
const toolContent = Array.isArray(toolMessage?.content) ? toolMessage.content : [];
const textBlock = toolContent.find((block) => block.type === "text");
expect(textBlock?.text).toBe(" indented\n");
});
it("serializes structured non-image blocks in tool results as JSON text", async () => {
// Prove the host redaction port is applied to structured tool-result text.
configureAiTransportHost({
@@ -3,10 +3,44 @@ import {
describeToolResultMediaPlaceholder,
describeUnsupportedToolResultMedia,
extractToolResultText,
formatToolResultText,
hasMediaPayload,
isImageWithMediaPayload,
} from "./tool-result-text.js";
describe("formatToolResultText", () => {
it("preserves significant boundary whitespace in nonblank tool output", () => {
expect(formatToolResultText({ text: " indented\n", isError: false })).toBe(" indented\n");
expect(formatToolResultText({ text: "row1 \nrow2\n", isError: false })).toBe(
"row1 \nrow2\n",
);
});
it("falls back to placeholders only for blank tool output", () => {
expect(formatToolResultText({ text: " \n\t", isError: false })).toBe("(no tool output)");
expect(
formatToolResultText({ text: "", mediaPlaceholder: "(see attached image)", isError: false }),
).toBe("(see attached image)");
});
it("keeps the error prefix on unmodified output", () => {
expect(formatToolResultText({ text: " failed ", isError: true })).toBe(
"[tool error] failed ",
);
});
it("appends the omitted-media suffix after unmodified output", () => {
const text = "line with trailing spaces ";
expect(
formatToolResultText({
text,
omittedMediaPlaceholder: "[tool image omitted]",
isError: false,
}),
).toBe(`${text}\n[tool image omitted]`);
});
});
describe("hasMediaPayload", () => {
it("requires non-empty inline data instead of media metadata", () => {
expect(hasMediaPayload({ type: "image", data: "aW1n", mimeType: "image/png" })).toBe(true);
@@ -223,8 +223,11 @@ export function formatToolResultText(params: {
isError: boolean;
}): string {
const trimmed = params.text.trim();
// trim() is only an emptiness predicate here: tool output boundary
// whitespace (indentation, trailing newlines) is significant durable
// content, so the nonblank body must emit params.text unmodified.
const body = trimmed
? `${trimmed}${params.omittedMediaPlaceholder ? `\n${params.omittedMediaPlaceholder}` : ""}`
? `${params.text}${params.omittedMediaPlaceholder ? `\n${params.omittedMediaPlaceholder}` : ""}`
: (params.omittedMediaPlaceholder ?? params.mediaPlaceholder ?? "(no tool output)");
return `${params.isError ? "[tool error] " : ""}${body}`;
}