From 797d51c82814d51f0d2c12a5d431d1828b41f716 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 30 Aug 2026 16:25:21 -0500 Subject: [PATCH 1/3] fix(ai): normalize tool result history --- packages/ai/src/route/client.ts | 15 ++-- packages/ai/src/tool-history.ts | 72 +++++++++++++++++ packages/ai/test/compile.test.ts | 20 +++++ .../ai/test/provider/bedrock-converse.test.ts | 30 +++++++ packages/ai/test/provider/gemini.test.ts | 20 +++++ .../ai/test/provider/mistral-chat.test.ts | 1 + packages/ai/test/provider/openai-chat.test.ts | 1 + .../ai/test/provider/openai-responses.test.ts | 4 + packages/ai/test/tool-history.test.ts | 80 +++++++++++++++++++ 9 files changed, 237 insertions(+), 6 deletions(-) create mode 100644 packages/ai/src/tool-history.ts create mode 100644 packages/ai/test/tool-history.test.ts diff --git a/packages/ai/src/route/client.ts b/packages/ai/src/route/client.ts index f2fb5b4fa9c5..186c20143f84 100644 --- a/packages/ai/src/route/client.ts +++ b/packages/ai/src/route/client.ts @@ -7,6 +7,7 @@ import { HttpTransport } from "./transport/index.js" import type { HttpMiddleware, Transport, TransportRuntime, WebSocketChannelExecutor } from "./transport/index.js" import type { Protocol } from "./protocol.js" import { applyCachePolicy } from "../cache-policy.js" +import { normalizeToolHistory } from "../tool-history.js" import { sanitizeSurrogates } from "../utils/sanitize.js" import * as ProviderShared from "../protocols/shared.js" import type { ProtocolID, ProviderOptions } from "../schema/index.js" @@ -169,17 +170,19 @@ export interface GenerateMethod { export class Service extends Context.Service()("@opencode/LLMClient") {} const resolveRequestOptions = (request: LLMRequest) => { - const routeDefaults = request.model.route.defaults - const modelDefaults = request.model.defaults - const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation) - return LLMRequest.update(request, { + const messages = normalizeToolHistory(request.messages) + const normalized = messages === request.messages ? request : LLMRequest.update(request, { messages }) + const routeDefaults = normalized.model.route.defaults + const modelDefaults = normalized.model.defaults + const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, normalized.generation) + return LLMRequest.update(normalized, { generation: generation ?? new GenerationOptions({}), providerOptions: mergeProviderOptions( routeDefaults.providerOptions, modelDefaults?.providerOptions, - request.providerOptions, + normalized.providerOptions, ), - http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http), + http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, normalized.http), }) } diff --git a/packages/ai/src/tool-history.ts b/packages/ai/src/tool-history.ts new file mode 100644 index 000000000000..b7006270f825 --- /dev/null +++ b/packages/ai/src/tool-history.ts @@ -0,0 +1,72 @@ +import { Message, ToolResultPart, type ToolCallPart } from "./schema/messages.js" + +const EMPTY_OUTPUT = "(no tool output)" +const MISSING_RESULT = "Tool result missing" + +export function normalizeToolHistory(messages: ReadonlyArray) { + const output: Message[] = [] + const pending = new Map() + const settle = () => { + if (pending.size === 0) return + output.push( + new Message({ + role: "tool", + content: [...pending.values()].map((call) => + ToolResultPart.make({ id: call.id, name: call.name, result: MISSING_RESULT, resultType: "error" }), + ), + }), + ) + pending.clear() + } + + for (const message of messages) { + if (message.role === "user" || message.role === "assistant") settle() + + if (message.role === "tool") { + const content = message.content.flatMap((part) => { + if (part.type !== "tool-result" || part.providerExecuted === true) return [part] + const call = pending.get(part.id) + if (!call) return [normalizeToolResult(part, part.name)] + pending.delete(part.id) + return [normalizeToolResult(part, call.name)] + }) + if (content.length === 0) continue + output.push( + content.length === message.content.length && content.every((part, index) => part === message.content[index]) + ? message + : new Message({ + id: message.id, + role: message.role, + content, + metadata: message.metadata, + native: message.native, + }), + ) + continue + } + + output.push(message) + if (message.role !== "assistant") continue + for (const part of message.content) { + if (part.type === "tool-call" && part.providerExecuted !== true) pending.set(part.id, part) + } + } + + settle() + return output.length === messages.length && output.every((message, index) => message === messages[index]) + ? messages + : output +} + +function normalizeToolResult(part: ToolResultPart, name: string): ToolResultPart { + const named = part.name === name ? part : { ...part, name } + if (named.result.type === "text" && named.result.value === "") + return { ...named, result: { type: "text", value: EMPTY_OUTPUT } } + if (named.result.type === "error" && named.result.value === "") + return { ...named, result: { type: "error", value: EMPTY_OUTPUT } } + if (named.result.type !== "content") return named + const value = named.result.value.filter((item) => item.type !== "text" || item.text !== "") + if (value.length === 0) return { ...named, result: { type: "text", value: EMPTY_OUTPUT } } + if (value.length === named.result.value.length) return named + return { ...named, result: { type: "content", value } } +} diff --git a/packages/ai/test/compile.test.ts b/packages/ai/test/compile.test.ts index d0d374c689e9..502381e864b8 100644 --- a/packages/ai/test/compile.test.ts +++ b/packages/ai/test/compile.test.ts @@ -106,6 +106,26 @@ describe("request option precedence", () => { }), ) + it.effect("normalizes tool history before protocol lowering", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: OpenAIChat.route.model({ id: "gpt-4o-mini" }), + messages: [ + Message.assistant(ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })), + Message.user("Continue."), + ], + }), + ) + + expect(prepared.body.messages).toMatchObject([ + { role: "assistant", tool_calls: [{ id: "call_1", function: { name: "lookup" } }] }, + { role: "tool", tool_call_id: "call_1", content: "Tool result missing" }, + { role: "user", content: "Continue." }, + ]) + }), + ) + it.effect("applies model HTTP defaults before request HTTP overlays", () => LLMClient.generate( LLM.request({ diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index dfaca9f6f263..43c1ef3e5118 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -351,6 +351,18 @@ describe("Bedrock Converse route", () => { }, ], }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "tool_1", + content: [{ text: "Tool result missing" }], + status: "error", + }, + }, + ], + }, ]) expect(input).toEqual(original) expect(call.input).toBe(input) @@ -380,6 +392,12 @@ describe("Bedrock Converse route", () => { { toolUse: { toolUseId: "tool_empty_object", name: "second", input: {} } }, ], }, + { + role: "user", + content: ["tool_empty_key", "tool_empty_object"].map((toolUseId) => ({ + toolResult: { toolUseId, content: [{ text: "Tool result missing" }], status: "error" as const }, + })), + }, ]) }), ) @@ -865,6 +883,18 @@ describe("Bedrock Converse route", () => { { toolUse: { toolUseId: "call_1", name: "lookup", input: {} } }, ], }, + { + role: "user", + content: [ + { + toolResult: { + toolUseId: "call_1", + content: [{ text: "Tool result missing" }], + status: "error", + }, + }, + ], + }, ]) }), ) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 436748ab1597..79cc03a6fbca 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -1271,6 +1271,16 @@ describe("Gemini route", () => { }, ], }, + { + role: "user", + parts: ["tool_0", "tool_1", "tool_2"].map((id) => ({ + functionResponse: { + id, + name: "lookup", + response: { name: "lookup", content: "Tool result missing" }, + }, + })), + }, ]) }), ) @@ -1303,6 +1313,16 @@ describe("Gemini route", () => { }, ], }, + { + role: "user", + parts: ["tool_0", "tool_1"].map((id) => ({ + functionResponse: { + id, + name: "lookup", + response: { name: "lookup", content: "Tool result missing" }, + }, + })), + }, ]) }), ) diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index 934b2c5a7755..f2f35890a050 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -218,6 +218,7 @@ describe("Mistral Chat", () => { content: "", tool_calls: [{ id: "Ab12Cd34E", type: "function", function: { name: "lookup", arguments: "{}" } }], }, + { role: "tool", tool_call_id: "Ab12Cd34E", name: "lookup", content: "Tool result missing" }, ]) }), ) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 376c6a7b87e0..6908b314312d 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1068,6 +1068,7 @@ describe("OpenAI Chat route", () => { }, ], }, + { role: "tool", tool_call_id: "call_1", content: "Tool result missing", cache_control: undefined }, ]) }), ) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index c2d3347c00d1..9437ba6cdc19 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -3474,6 +3474,7 @@ describe("OpenAI Responses route", () => { { type: "reasoning", summary: [{ type: "summary_text", text: "No prefix separator." }], + encrypted_content: undefined, }, { type: "function_call", @@ -3488,6 +3489,8 @@ describe("OpenAI Responses route", () => { name: "lookup", arguments: '{"query":"news"}', }, + { type: "function_call_output", call_id: "call_1", output: "Tool result missing" }, + { type: "function_call_output", call_id: "call_2", output: "Tool result missing" }, ]) }), ) @@ -3789,6 +3792,7 @@ describe("OpenAI Responses route", () => { name: "lookup", arguments: '{"query":"weather"}', }, + { type: "function_call_output", call_id: "call_1", output: "Tool result missing" }, ]) }), ) diff --git a/packages/ai/test/tool-history.test.ts b/packages/ai/test/tool-history.test.ts new file mode 100644 index 000000000000..4fef16be7f3c --- /dev/null +++ b/packages/ai/test/tool-history.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "bun:test" +import { Message, ToolCallPart, ToolResultPart } from "../src/schema/messages.js" +import { normalizeToolHistory } from "../src/tool-history.js" + +const call = (id: string, name = id) => ToolCallPart.make({ id, name, input: {} }) +const result = (id: string, value: unknown, name = id, resultType?: "text" | "content" | "error") => + Message.tool(ToolResultPart.make({ id, name, result: value, resultType })) + +describe("tool history normalization", () => { + test("settles missing calls before the next message and at history end", () => { + const next = Message.user("Continue.") + const normalized = normalizeToolHistory([ + Message.assistant([call("first"), call("second")]), + result("first", "done", "wrong", "text"), + next, + Message.assistant(call("trailing")), + ]) + + expect(normalized.map((message) => message.role)).toEqual([ + "assistant", + "tool", + "tool", + "user", + "assistant", + "tool", + ]) + expect(normalized[1]?.content[0]).toMatchObject({ type: "tool-result", id: "first", name: "first" }) + expect(normalized[2]?.content).toEqual([ + { type: "tool-result", id: "second", name: "second", result: { type: "error", value: "Tool result missing" } }, + ]) + expect(normalized[5]?.content).toEqual([ + { + type: "tool-result", + id: "trailing", + name: "trailing", + result: { type: "error", value: "Tool result missing" }, + }, + ]) + }) + + test("normalizes empty results without changing whitespace or media", () => { + const media = { type: "file" as const, uri: "data:image/png;base64,AQID", mime: "image/png" } + const normalized = normalizeToolHistory([ + Message.assistant([call("text"), call("content"), call("error"), call("mixed"), call("whitespace")]), + result("text", "", "text", "text"), + result("content", [], "content", "content"), + result("error", "", "error", "error"), + result("mixed", [{ type: "text", text: "" }, media], "mixed", "content"), + result("whitespace", " ", "whitespace", "text"), + ]) + + expect(normalized.slice(1).map((message) => message.content[0])).toEqual([ + { type: "tool-result", id: "text", name: "text", result: { type: "text", value: "(no tool output)" } }, + { type: "tool-result", id: "content", name: "content", result: { type: "text", value: "(no tool output)" } }, + { type: "tool-result", id: "error", name: "error", result: { type: "error", value: "(no tool output)" } }, + { type: "tool-result", id: "mixed", name: "mixed", result: { type: "content", value: [media] } }, + { type: "tool-result", id: "whitespace", name: "whitespace", result: { type: "text", value: " " } }, + ]) + }) + + test("preserves unmatched and provider-executed results", () => { + const hostedCall = ToolCallPart.make({ + id: "hosted", + name: "web_search", + input: {}, + providerExecuted: true, + }) + const hostedResult = ToolResultPart.make({ + id: "hosted", + name: "web_search", + result: "", + resultType: "text", + providerExecuted: true, + }) + const hosted = Message.assistant([hostedCall, hostedResult]) + const orphan = result("orphan", "ignored", "orphan", "text") + + expect(normalizeToolHistory([orphan, hosted])).toEqual([orphan, hosted]) + }) +}) From c4b9a2f29a4474e7bfdf693ef70fbeb46ed4d429 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 30 Aug 2026 16:35:52 -0500 Subject: [PATCH 2/3] refactor(ai): simplify tool history normalization --- packages/ai/src/tool-history.ts | 81 ++++++++++++++------------- packages/ai/test/tool-history.test.ts | 37 ++++++------ 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/packages/ai/src/tool-history.ts b/packages/ai/src/tool-history.ts index b7006270f825..72f31b769a06 100644 --- a/packages/ai/src/tool-history.ts +++ b/packages/ai/src/tool-history.ts @@ -1,72 +1,75 @@ import { Message, ToolResultPart, type ToolCallPart } from "./schema/messages.js" -const EMPTY_OUTPUT = "(no tool output)" -const MISSING_RESULT = "Tool result missing" +const EMPTY_TOOL_OUTPUT = "(no tool output)" +const MISSING_TOOL_RESULT = "Tool result missing" export function normalizeToolHistory(messages: ReadonlyArray) { - const output: Message[] = [] + const normalized: Message[] = [] const pending = new Map() - const settle = () => { + const appendMissingResults = () => { if (pending.size === 0) return - output.push( - new Message({ - role: "tool", - content: [...pending.values()].map((call) => - ToolResultPart.make({ id: call.id, name: call.name, result: MISSING_RESULT, resultType: "error" }), - ), - }), - ) + normalized.push(missingToolResults(pending.values())) pending.clear() } for (const message of messages) { - if (message.role === "user" || message.role === "assistant") settle() + if (message.role === "user" || message.role === "assistant") appendMissingResults() if (message.role === "tool") { - const content = message.content.flatMap((part) => { - if (part.type !== "tool-result" || part.providerExecuted === true) return [part] - const call = pending.get(part.id) - if (!call) return [normalizeToolResult(part, part.name)] - pending.delete(part.id) - return [normalizeToolResult(part, call.name)] - }) - if (content.length === 0) continue - output.push( - content.length === message.content.length && content.every((part, index) => part === message.content[index]) - ? message - : new Message({ - id: message.id, - role: message.role, - content, - metadata: message.metadata, - native: message.native, - }), - ) + const tool = normalizeToolMessage(message, pending) + if (tool) normalized.push(tool) continue } - output.push(message) + normalized.push(message) if (message.role !== "assistant") continue for (const part of message.content) { if (part.type === "tool-call" && part.providerExecuted !== true) pending.set(part.id, part) } } - settle() - return output.length === messages.length && output.every((message, index) => message === messages[index]) + appendMissingResults() + return normalized.length === messages.length && normalized.every((message, index) => message === messages[index]) ? messages - : output + : normalized +} + +function missingToolResults(calls: Iterable) { + return new Message({ + role: "tool", + content: [...calls].map((call) => + ToolResultPart.make({ id: call.id, name: call.name, result: MISSING_TOOL_RESULT, resultType: "error" }), + ), + }) +} + +function normalizeToolMessage(message: Message, pending: Map): Message | undefined { + const content = message.content.map((part) => { + if (part.type !== "tool-result" || part.providerExecuted === true) return part + const call = pending.get(part.id) + if (call) pending.delete(part.id) + return normalizeToolResult(part, call?.name ?? part.name) + }) + if (content.length === 0) return undefined + if (content.every((part, index) => part === message.content[index])) return message + return new Message({ + id: message.id, + role: message.role, + content, + metadata: message.metadata, + native: message.native, + }) } function normalizeToolResult(part: ToolResultPart, name: string): ToolResultPart { const named = part.name === name ? part : { ...part, name } if (named.result.type === "text" && named.result.value === "") - return { ...named, result: { type: "text", value: EMPTY_OUTPUT } } + return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } } if (named.result.type === "error" && named.result.value === "") - return { ...named, result: { type: "error", value: EMPTY_OUTPUT } } + return { ...named, result: { type: "error", value: EMPTY_TOOL_OUTPUT } } if (named.result.type !== "content") return named const value = named.result.value.filter((item) => item.type !== "text" || item.text !== "") - if (value.length === 0) return { ...named, result: { type: "text", value: EMPTY_OUTPUT } } + if (value.length === 0) return { ...named, result: { type: "text", value: EMPTY_TOOL_OUTPUT } } if (value.length === named.result.value.length) return named return { ...named, result: { type: "content", value } } } diff --git a/packages/ai/test/tool-history.test.ts b/packages/ai/test/tool-history.test.ts index 4fef16be7f3c..ccea1aa8db2e 100644 --- a/packages/ai/test/tool-history.test.ts +++ b/packages/ai/test/tool-history.test.ts @@ -2,18 +2,17 @@ import { describe, expect, test } from "bun:test" import { Message, ToolCallPart, ToolResultPart } from "../src/schema/messages.js" import { normalizeToolHistory } from "../src/tool-history.js" -const call = (id: string, name = id) => ToolCallPart.make({ id, name, input: {} }) -const result = (id: string, value: unknown, name = id, resultType?: "text" | "content" | "error") => +const toolCall = (id: string, name = id) => ToolCallPart.make({ id, name, input: {} }) +const toolResult = (id: string, value: unknown, name = id, resultType?: "text" | "content" | "error") => Message.tool(ToolResultPart.make({ id, name, result: value, resultType })) describe("tool history normalization", () => { - test("settles missing calls before the next message and at history end", () => { - const next = Message.user("Continue.") + test("fills missing local results at step boundaries", () => { const normalized = normalizeToolHistory([ - Message.assistant([call("first"), call("second")]), - result("first", "done", "wrong", "text"), - next, - Message.assistant(call("trailing")), + Message.assistant([toolCall("first"), toolCall("second")]), + toolResult("first", "done", "wrong", "text"), + Message.user("Continue."), + Message.assistant(toolCall("trailing")), ]) expect(normalized.map((message) => message.role)).toEqual([ @@ -41,12 +40,18 @@ describe("tool history normalization", () => { test("normalizes empty results without changing whitespace or media", () => { const media = { type: "file" as const, uri: "data:image/png;base64,AQID", mime: "image/png" } const normalized = normalizeToolHistory([ - Message.assistant([call("text"), call("content"), call("error"), call("mixed"), call("whitespace")]), - result("text", "", "text", "text"), - result("content", [], "content", "content"), - result("error", "", "error", "error"), - result("mixed", [{ type: "text", text: "" }, media], "mixed", "content"), - result("whitespace", " ", "whitespace", "text"), + Message.assistant([ + toolCall("text"), + toolCall("content"), + toolCall("error"), + toolCall("mixed"), + toolCall("whitespace"), + ]), + toolResult("text", "", "text", "text"), + toolResult("content", [], "content", "content"), + toolResult("error", "", "error", "error"), + toolResult("mixed", [{ type: "text", text: "" }, media], "mixed", "content"), + toolResult("whitespace", " ", "whitespace", "text"), ]) expect(normalized.slice(1).map((message) => message.content[0])).toEqual([ @@ -58,7 +63,7 @@ describe("tool history normalization", () => { ]) }) - test("preserves unmatched and provider-executed results", () => { + test("leaves unmatched and provider-executed history unchanged", () => { const hostedCall = ToolCallPart.make({ id: "hosted", name: "web_search", @@ -73,7 +78,7 @@ describe("tool history normalization", () => { providerExecuted: true, }) const hosted = Message.assistant([hostedCall, hostedResult]) - const orphan = result("orphan", "ignored", "orphan", "text") + const orphan = toolResult("orphan", "ignored", "orphan", "text") expect(normalizeToolHistory([orphan, hosted])).toEqual([orphan, hosted]) }) From fdd4ddaf35184df593ffc761bc7361d3063d1841 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 30 Aug 2026 21:47:41 -0500 Subject: [PATCH 3/3] test(ai): isolate tool history coverage --- packages/ai/src/tool-history.ts | 1 - .../ai/test/provider/bedrock-converse.test.ts | 30 ------------------- packages/ai/test/provider/gemini.test.ts | 20 ------------- .../ai/test/provider/mistral-chat.test.ts | 1 - packages/ai/test/provider/openai-chat.test.ts | 1 - .../ai/test/provider/openai-responses.test.ts | 4 --- packages/ai/test/tool-history.test.ts | 12 ++------ 7 files changed, 2 insertions(+), 67 deletions(-) diff --git a/packages/ai/src/tool-history.ts b/packages/ai/src/tool-history.ts index 72f31b769a06..a36bbbee3cc1 100644 --- a/packages/ai/src/tool-history.ts +++ b/packages/ai/src/tool-history.ts @@ -28,7 +28,6 @@ export function normalizeToolHistory(messages: ReadonlyArray) { } } - appendMissingResults() return normalized.length === messages.length && normalized.every((message, index) => message === messages[index]) ? messages : normalized diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 43c1ef3e5118..dfaca9f6f263 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -351,18 +351,6 @@ describe("Bedrock Converse route", () => { }, ], }, - { - role: "user", - content: [ - { - toolResult: { - toolUseId: "tool_1", - content: [{ text: "Tool result missing" }], - status: "error", - }, - }, - ], - }, ]) expect(input).toEqual(original) expect(call.input).toBe(input) @@ -392,12 +380,6 @@ describe("Bedrock Converse route", () => { { toolUse: { toolUseId: "tool_empty_object", name: "second", input: {} } }, ], }, - { - role: "user", - content: ["tool_empty_key", "tool_empty_object"].map((toolUseId) => ({ - toolResult: { toolUseId, content: [{ text: "Tool result missing" }], status: "error" as const }, - })), - }, ]) }), ) @@ -883,18 +865,6 @@ describe("Bedrock Converse route", () => { { toolUse: { toolUseId: "call_1", name: "lookup", input: {} } }, ], }, - { - role: "user", - content: [ - { - toolResult: { - toolUseId: "call_1", - content: [{ text: "Tool result missing" }], - status: "error", - }, - }, - ], - }, ]) }), ) diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 79cc03a6fbca..436748ab1597 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -1271,16 +1271,6 @@ describe("Gemini route", () => { }, ], }, - { - role: "user", - parts: ["tool_0", "tool_1", "tool_2"].map((id) => ({ - functionResponse: { - id, - name: "lookup", - response: { name: "lookup", content: "Tool result missing" }, - }, - })), - }, ]) }), ) @@ -1313,16 +1303,6 @@ describe("Gemini route", () => { }, ], }, - { - role: "user", - parts: ["tool_0", "tool_1"].map((id) => ({ - functionResponse: { - id, - name: "lookup", - response: { name: "lookup", content: "Tool result missing" }, - }, - })), - }, ]) }), ) diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index f2f35890a050..934b2c5a7755 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -218,7 +218,6 @@ describe("Mistral Chat", () => { content: "", tool_calls: [{ id: "Ab12Cd34E", type: "function", function: { name: "lookup", arguments: "{}" } }], }, - { role: "tool", tool_call_id: "Ab12Cd34E", name: "lookup", content: "Tool result missing" }, ]) }), ) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 6908b314312d..376c6a7b87e0 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -1068,7 +1068,6 @@ describe("OpenAI Chat route", () => { }, ], }, - { role: "tool", tool_call_id: "call_1", content: "Tool result missing", cache_control: undefined }, ]) }), ) diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 9437ba6cdc19..c2d3347c00d1 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -3474,7 +3474,6 @@ describe("OpenAI Responses route", () => { { type: "reasoning", summary: [{ type: "summary_text", text: "No prefix separator." }], - encrypted_content: undefined, }, { type: "function_call", @@ -3489,8 +3488,6 @@ describe("OpenAI Responses route", () => { name: "lookup", arguments: '{"query":"news"}', }, - { type: "function_call_output", call_id: "call_1", output: "Tool result missing" }, - { type: "function_call_output", call_id: "call_2", output: "Tool result missing" }, ]) }), ) @@ -3792,7 +3789,6 @@ describe("OpenAI Responses route", () => { name: "lookup", arguments: '{"query":"weather"}', }, - { type: "function_call_output", call_id: "call_1", output: "Tool result missing" }, ]) }), ) diff --git a/packages/ai/test/tool-history.test.ts b/packages/ai/test/tool-history.test.ts index ccea1aa8db2e..302e3f4b334e 100644 --- a/packages/ai/test/tool-history.test.ts +++ b/packages/ai/test/tool-history.test.ts @@ -7,7 +7,7 @@ const toolResult = (id: string, value: unknown, name = id, resultType?: "text" | Message.tool(ToolResultPart.make({ id, name, result: value, resultType })) describe("tool history normalization", () => { - test("fills missing local results at step boundaries", () => { + test("fills missing local results before the next step", () => { const normalized = normalizeToolHistory([ Message.assistant([toolCall("first"), toolCall("second")]), toolResult("first", "done", "wrong", "text"), @@ -21,20 +21,12 @@ describe("tool history normalization", () => { "tool", "user", "assistant", - "tool", ]) expect(normalized[1]?.content[0]).toMatchObject({ type: "tool-result", id: "first", name: "first" }) expect(normalized[2]?.content).toEqual([ { type: "tool-result", id: "second", name: "second", result: { type: "error", value: "Tool result missing" } }, ]) - expect(normalized[5]?.content).toEqual([ - { - type: "tool-result", - id: "trailing", - name: "trailing", - result: { type: "error", value: "Tool result missing" }, - }, - ]) + expect(normalized[4]?.content).toEqual([toolCall("trailing")]) }) test("normalizes empty results without changing whitespace or media", () => {