diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 1a6bb5af33b4..ef1f19b70313 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -843,6 +843,7 @@ const requireThinkingSignature = (request: LLMRequest) => { ) return false if (provider.includes("xiaomi") || model.includes("mimo") || baseURL.includes("xiaomimimo.com")) return false + if (model.startsWith("qwen") || model.startsWith("minimax-")) return false return true } @@ -971,19 +972,9 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest, breakpoi continue } if (!signature || signature.trim().length === 0) { - if (part.text.trim().length === 0) continue - if (!requireThinkingSignature(request)) { + if (part.text.trim().length > 0 && !requireThinkingSignature(request)) { content.push({ type: "thinking", thinking: part.text, signature: "" }) - continue } - // Without a signature this cannot be a valid thinking block per - // the SDK ThinkingBlockParam:3217 — demote to text so the - // conversation remains sendable. - content.push({ - type: "text", - text: part.text, - cache_control: cacheControl(breakpoints, part.cache), - }) continue } content.push({ type: "thinking", thinking: part.text, signature }) diff --git a/packages/ai/src/protocols/bedrock-converse.ts b/packages/ai/src/protocols/bedrock-converse.ts index e86a77334ae3..353f2b09d6e4 100644 --- a/packages/ai/src/protocols/bedrock-converse.ts +++ b/packages/ai/src/protocols/bedrock-converse.ts @@ -427,12 +427,7 @@ const lowerMessages = Effect.fnUntraced(function* ( content.push({ reasoningContent: { redactedContent: redactedData } }) continue } - if (signature === undefined || signature.trim().length === 0) { - // Interrupted streams and model switches can leave unsigned reasoning. - // Preserve readable history as text rather than replay invalid reasoningContent. - if (part.text.trim().length > 0) content.push(...textWithCache(breakpoints, part.text, part.cache)) - continue - } + if (signature === undefined || signature.trim().length === 0) continue content.push({ reasoningContent: { reasoningText: { text: part.text, signature } } }) continue } diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index 29218420003f..be9e929ca208 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -32,22 +32,12 @@ import { Lifecycle } from "./utils/lifecycle.js" const ADAPTER = "gemini" const CONTINUATION = "CONTINUATION" -// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost. +// Google documents this sentinel for replaying function calls after their original signature was lost. const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator" // Gemini 2.5 rejects a budget under the model's minimum: 512 on Flash-Lite, the highest, and 128 on Pro. const MIN_THINKING_BUDGET = 512 export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" -// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by -// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call -// from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior. -const requiresThoughtSignatureFallback = (modelID: string) => { - if (!/(^|\/)gemini-/i.test(modelID)) return false - if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID)) return false - if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID)) return false - return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID) -} - // Gemini 3 accepts media nested inside function responses; matched Gemini 2.5 variants reject it, // so their tool-result attachments lower as a separate user turn instead. const routesLegacyToolMedia = (modelID: string) => /gemini-2[.-]5(?:[.-]|$)/i.test(modelID) @@ -390,11 +380,7 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { const signature = lowered.thoughtSignature parts.push({ ...lowered, - thoughtSignature: - signature ?? - (requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall - ? SKIP_THOUGHT_SIGNATURE_VALIDATOR - : undefined), + thoughtSignature: signature ?? (!hasSignedToolCall ? SKIP_THOUGHT_SIGNATURE_VALIDATOR : undefined), }) if (signature !== undefined) hasSignedToolCall = true continue diff --git a/packages/ai/src/protocols/google-interactions.ts b/packages/ai/src/protocols/google-interactions.ts index a04083fd0783..24d2179afb19 100644 --- a/packages/ai/src/protocols/google-interactions.ts +++ b/packages/ai/src/protocols/google-interactions.ts @@ -23,6 +23,7 @@ import { GeminiGenerateContent } from "./utils/gemini-generate-content.js" import { ToolStream } from "./utils/tool-stream.js" const ADAPTER = "google-interactions" +const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" // ============================================================================= @@ -259,6 +260,7 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { steps[steps.length - 1] = { type, content: [...last.content, content] } else steps.push({ type, content: [content] }) } + let hasSignedStep = false for (const part of message.content) { if (message.role === "tool") { if (part.type !== "tool-result") @@ -281,21 +283,25 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { continue } if (message.role === "assistant" && part.type === "reasoning") { + const thoughtSignature = signature(part.providerMetadata, key) steps.push({ type: "thought", - signature: signature(part.providerMetadata, key), + signature: thoughtSignature, summary: part.text ? [{ type: "text", text: part.text }] : undefined, }) + if (thoughtSignature !== undefined) hasSignedStep = true continue } if (message.role === "assistant" && part.type === "tool-call") { + const toolSignature = signature(part.providerMetadata, key) steps.push({ type: "function_call", id: part.id, name: part.name, arguments: part.input, - signature: signature(part.providerMetadata, key), + signature: toolSignature ?? (!hasSignedStep ? SKIP_THOUGHT_SIGNATURE_VALIDATOR : undefined), }) + if (toolSignature !== undefined) hasSignedStep = true continue } return yield* ProviderShared.unsupportedContent( diff --git a/packages/ai/src/protocols/mistral-chat.ts b/packages/ai/src/protocols/mistral-chat.ts index 619f169d2a2a..4c1fbfbab8f1 100644 --- a/packages/ai/src/protocols/mistral-chat.ts +++ b/packages/ai/src/protocols/mistral-chat.ts @@ -254,7 +254,6 @@ const lowerToolCall = (part: ToolCallPart, normalizeID: (id: string) => string): const lowerAssistant = Effect.fnUntraced(function* ( message: LLMRequest["messages"][number], normalizeID: (id: string) => string, - prefix: boolean, ) { const structured = message.content.some( (part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking), @@ -271,8 +270,6 @@ const lowerAssistant = Effect.fnUntraced(function* ( if (part.type === "reasoning") { const native = part.providerMetadata?.mistral?.thinking if (structured && isMistralThinkingContent(native)) content.push(native) - else if (structured) content.push({ type: "text", text: part.text }) - else text.push(part.text) continue } if (part.type === "tool-call") { @@ -285,7 +282,6 @@ const lowerAssistant = Effect.fnUntraced(function* ( role: "assistant" as const, content: structured ? content : text.join(""), ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), - ...(prefix ? { prefix: true as const } : {}), } }) @@ -358,15 +354,17 @@ const lowerMessages = Effect.fnUntraced(function* (request: LLMRequest) { const hasNativeThinking = message.content.some( (part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking), ) - const text = message.content - .flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : [])) - .join("") + const text = message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("") if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue - messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1))) + messages.push(yield* lowerAssistant(message, normalizeID)) continue } messages.push(...(yield* lowerToolResults(message, normalizeID))) } + const trailing = messages.at(-1) + if (trailing?.role === "assistant" && !trailing.tool_calls) { + messages[messages.length - 1] = { ...trailing, prefix: true } + } return messages }) diff --git a/packages/ai/src/providers/meta.ts b/packages/ai/src/providers/meta.ts index eecbb8125479..13fac64f3b4f 100644 --- a/packages/ai/src/providers/meta.ts +++ b/packages/ai/src/providers/meta.ts @@ -138,7 +138,6 @@ export const configure = (input: Config = {}) => { const messages = (modelID: string | ModelID) => configuredMessages.model({ id: modelID, - compatibility: { requireSignature: false }, }) const media = MediaRoute.deployment(input, options.auth) const image = (modelID: string | ModelID) => MetaImages.model({ ...media, id: modelID }) diff --git a/packages/ai/src/providers/minimax.ts b/packages/ai/src/providers/minimax.ts index 18c69016199c..ec5d77e7333b 100644 --- a/packages/ai/src/providers/minimax.ts +++ b/packages/ai/src/providers/minimax.ts @@ -117,7 +117,10 @@ export const configure = (input: Config = {}) => { auth, } const messages = (modelID: string | ModelID) => - messagesRoute.with(defaults).model({ id: modelID }) + messagesRoute.with(defaults).model({ + id: modelID, + compatibility: { requireSignature: false }, + }) const chat = (modelID: string | ModelID) => chatRoute.with(defaults).model({ id: modelID, diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index 41dcc6decb5f..0ba341755951 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -1035,13 +1035,11 @@ describe("Anthropic Messages route", () => { }), ) - it.effect("demotes unsigned reasoning when signatures are required", () => + it.effect("drops unsigned reasoning when signatures are required", () => Effect.gen(function* () { const prepared = yield* compileUnsignedReasoning(model) - expect(prepared.body.messages).toEqual([ - { role: "assistant", content: [{ type: "text", text: "unsigned reasoning" }] }, - ]) + expect(prepared.body.messages).toEqual([]) }), ) @@ -1061,19 +1059,19 @@ describe("Anthropic Messages route", () => { .model({ id: "kimi-k2.6" }) const codingPrepared = yield* compileUnsignedReasoning(coding.model({ id: "k3" })) const moonshotPrepared = yield* compileUnsignedReasoning(moonshot) + const qwenPrepared = yield* compileUnsignedReasoning(coding.with({ provider: "opencode" }).model({ id: "qwen3.8-flash" })) + const minimaxPrepared = yield* compileUnsignedReasoning( + coding.with({ provider: "opencode-go" }).model({ id: "minimax-m3" }), + ) - expect(codingPrepared.body.messages).toEqual([ - { - role: "assistant", - content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], - }, - ]) - expect(moonshotPrepared.body.messages).toEqual([ - { - role: "assistant", - content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], - }, - ]) + for (const prepared of [codingPrepared, moonshotPrepared, qwenPrepared, minimaxPrepared]) { + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ type: "thinking", thinking: "unsigned reasoning", signature: "" }], + }, + ]) + } }), ) @@ -1088,9 +1086,7 @@ describe("Anthropic Messages route", () => { .model({ id: "k3", compatibility: { requireSignature: true } }) const prepared = yield* compileUnsignedReasoning(compatible) - expect(prepared.body.messages).toEqual([ - { role: "assistant", content: [{ type: "text", text: "unsigned reasoning" }] }, - ]) + expect(prepared.body.messages).toEqual([]) }), ) diff --git a/packages/ai/test/provider/bedrock-converse-cache.test.ts b/packages/ai/test/provider/bedrock-converse-cache.test.ts index 3ec178fdcec5..263ebd3b7772 100644 --- a/packages/ai/test/provider/bedrock-converse-cache.test.ts +++ b/packages/ai/test/provider/bedrock-converse-cache.test.ts @@ -61,7 +61,7 @@ describe("Bedrock Converse cache policy", () => { { role: "user", content: [{ text: "Question" }, { text: "\nUpdate\n" }] }, { role: "assistant", - content: [{ text: "Answer" }, { text: "Unsigned reasoning" }, { toolUse: { name: "lookup" } }], + content: [{ text: "Answer" }, { toolUse: { name: "lookup" } }], }, { role: "user", content: [{ toolResult: { content: [{ json: "Result" }] } }] }, ], diff --git a/packages/ai/test/provider/bedrock-converse.test.ts b/packages/ai/test/provider/bedrock-converse.test.ts index 99e0e72e17b0..8464b9137930 100644 --- a/packages/ai/test/provider/bedrock-converse.test.ts +++ b/packages/ai/test/provider/bedrock-converse.test.ts @@ -1197,7 +1197,7 @@ describe("Bedrock Converse route", () => { for (const signature of [undefined, "", " "]) { for (const cache of ["none", "auto"] as const) { - it.effect(`demotes unsigned reasoning to text (${JSON.stringify(signature)}, cache: ${cache})`, () => + it.effect(`drops unsigned reasoning (${JSON.stringify(signature)}, cache: ${cache})`, () => Effect.gen(function* () { const prepared = yield* compileRequest( ai.request({ @@ -1217,10 +1217,15 @@ describe("Bedrock Converse route", () => { cache, }), ) - expect(prepared.body.messages[1]).toEqual({ - role: "assistant", - content: [{ text: "Partial thought" }, { cachePoint: { type: "default" } }], - }) + expect(prepared.body.messages).toEqual([ + { + role: "user", + content: + cache === "auto" + ? [{ text: "Think" }, { text: "Continue" }, { cachePoint: { type: "default" } }] + : [{ text: "Think" }, { text: "Continue" }], + }, + ]) }), ) } @@ -1252,7 +1257,7 @@ describe("Bedrock Converse route", () => { }), ) - it.effect("demotes foreign reasoning while preserving signed, redacted, text, and tool blocks", () => + it.effect("drops foreign reasoning while preserving signed, redacted, text, and tool blocks", () => Effect.gen(function* () { const cache = new CacheHint({ type: "ephemeral" }) const prepared = yield* compileRequest( @@ -1281,7 +1286,6 @@ describe("Bedrock Converse route", () => { { role: "assistant", content: [ - { text: "Foreign thought" }, { reasoningContent: { reasoningText: { text: "Signed thought", signature: "sig_1" } } }, { reasoningContent: { reasoningText: { text: "", signature: "sig_2" } } }, { reasoningContent: { redactedContent: "cmVkYWN0ZWQ=" } }, diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index 22bda497745f..7a0bd6e57afc 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -175,7 +175,12 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }], + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], }, { role: "user", @@ -219,8 +224,14 @@ describe("Gemini route", () => { { role: "model", parts: [ - { functionCall: { name: "lookup", args: { query: "weather" } } }, - { functionCall: { name: "lookup", args: { query: "time" } } }, + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + { + functionCall: { name: "lookup", args: { query: "time" } }, + thoughtSignature: "skip_thought_signature_validator", + }, ], }, { @@ -302,7 +313,15 @@ describe("Gemini route", () => { ) expect(legacy.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [{ functionResponse: { name: "lookup", response: { name: "lookup", content: "done" } } }], @@ -331,7 +350,15 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { id: "call_1", name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [ @@ -380,7 +407,12 @@ describe("Gemini route", () => { }, { role: "model", - parts: [{ functionCall: { name: "lookup", args: { query: "weather" } } }], + parts: [ + { + functionCall: { name: "lookup", args: { query: "weather" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], }, { role: "user", @@ -434,7 +466,15 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "read", args: { path: "pixel.png" } } }] }, + { + role: "model", + parts: [ + { + functionCall: { name: "read", args: { path: "pixel.png" } }, + thoughtSignature: "skip_thought_signature_validator", + }, + ], + }, { role: "user", parts: [ @@ -608,7 +648,13 @@ describe("Gemini route", () => { model: "gemini-3-flash-preview", input: [ { type: "user_input", content: [{ type: "video", uri, mime_type: "video/mp4" }] }, - { type: "function_call", id: "call_video", name: "read", arguments: {} }, + { + type: "function_call", + id: "call_video", + name: "read", + arguments: {}, + signature: "skip_thought_signature_validator", + }, { type: "function_result", call_id: "call_video", @@ -652,7 +698,10 @@ describe("Gemini route", () => { ) expect(prepared.body.contents).toEqual([ - { role: "model", parts: [{ functionCall: { name: "shot", args: {} } }] }, + { + role: "model", + parts: [{ functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }], + }, { role: "user", parts: [{ functionResponse: { name: "shot", response: { name: "shot", content: "" } } }], @@ -702,7 +751,10 @@ describe("Gemini route", () => { expect(prepared.body.contents).toEqual([ { role: "model", - parts: [{ functionCall: { name: "shot", args: {} } }, { functionCall: { name: "shot", args: {} } }], + parts: [ + { functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }, + { functionCall: { name: "shot", args: {} }, thoughtSignature: "skip_thought_signature_validator" }, + ], }, { role: "user", diff --git a/packages/ai/test/provider/google-vertex-interactions.test.ts b/packages/ai/test/provider/google-vertex-interactions.test.ts index e3b34d021696..172e0ddd1dcc 100644 --- a/packages/ai/test/provider/google-vertex-interactions.test.ts +++ b/packages/ai/test/provider/google-vertex-interactions.test.ts @@ -259,6 +259,49 @@ describe("Google Vertex Interactions", () => { ], }) expect(prepared.body.previous_interaction_id).toBeUndefined() + + const unsigned = yield* compileRequest( + ai.request({ + model, + messages: [ + Message.assistant([ + ToolCallPart.make({ id: "call_foreign", name: "lookup", input: { city: "Paris" } }), + ]), + Message.tool({ id: "call_foreign", name: "lookup", result: { weather: "sunny" } }), + Message.assistant([ + ToolCallPart.make({ + id: "call_signed", + name: "lookup", + input: { city: "Lyon" }, + providerMetadata: { vertex: { interactionSignature: "sig_1" } }, + }), + ToolCallPart.make({ id: "call_sibling", name: "lookup", input: { city: "Nice" } }), + ]), + Message.tool({ id: "call_signed", name: "lookup", result: { weather: "cloudy" } }), + Message.tool({ id: "call_sibling", name: "lookup", result: { weather: "warm" } }), + ], + }), + ) + expect(unsigned.body.input).toEqual([ + { + type: "function_call", + id: "call_foreign", + name: "lookup", + arguments: { city: "Paris" }, + signature: "skip_thought_signature_validator", + }, + { type: "function_result", call_id: "call_foreign", name: "lookup", result: { weather: "sunny" } }, + { + type: "function_call", + id: "call_signed", + name: "lookup", + arguments: { city: "Lyon" }, + signature: "sig_1", + }, + { type: "function_call", id: "call_sibling", name: "lookup", arguments: { city: "Nice" } }, + { type: "function_result", call_id: "call_signed", name: "lookup", result: { weather: "cloudy" } }, + { type: "function_result", call_id: "call_sibling", name: "lookup", result: { weather: "warm" } }, + ]) }), ) }) diff --git a/packages/ai/test/provider/mistral-chat.test.ts b/packages/ai/test/provider/mistral-chat.test.ts index ded7bf59e5ec..6efdedf04bae 100644 --- a/packages/ai/test/provider/mistral-chat.test.ts +++ b/packages/ai/test/provider/mistral-chat.test.ts @@ -123,7 +123,7 @@ describe("Mistral Chat", () => { }, { role: "assistant", - content: "ThinkCalling", + content: "Calling", }, ]) const assistant = prepared.body.messages[3] @@ -177,6 +177,21 @@ describe("Mistral Chat", () => { { role: "assistant", content: "Prefix", prefix: true }, ]) + const trailingDropped = yield* compileRequest( + ai.request({ + model, + messages: [ + Message.user("Continue"), + Message.assistant("Prefix"), + Message.assistant({ type: "reasoning", text: "Dropped unsigned reasoning" }), + ], + }), + ) + expect(trailingDropped.body.messages).toEqual([ + { role: "user", content: "Continue" }, + { role: "assistant", content: "Prefix", prefix: true }, + ]) + const uncached = yield* compileRequest( ai.request({ model, @@ -227,6 +242,7 @@ describe("Mistral Chat", () => { messages: [ Message.assistant(" \n "), Message.assistant({ type: "reasoning", text: "\t" }), + Message.assistant({ type: "reasoning", text: "Foreign reasoning without mistral metadata" }), Message.assistant({ type: "tool-call", id: "Ab12Cd34E", name: "lookup", input: {} }), ], }), diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 3d8921c0911b..26750bfec515 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -171,8 +171,9 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider }, ] // Let the destination adapter handle readable reasoning after a model/provider switch. - if (item.type === "reasoning") - return reuseProviderMetadata && !reasoningInterrupted + if (item.type === "reasoning") { + if (message.error !== undefined || reasoningInterrupted) return [] + return reuseProviderMetadata ? [ { type: "reasoning", @@ -181,8 +182,9 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider }, ] : item.text.length > 0 - ? [{ type: message.error === undefined && !reasoningInterrupted ? "reasoning" : "text", text: item.text }] + ? [{ type: "reasoning", text: item.text }] : [] + } // Call-side metadata is model-scoped proof of generation (Gemini thought // signatures, OpenAI encrypted reasoning): only the producing model may // replay it. diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index ed9265766cc6..c81143d34e9d 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -933,7 +933,7 @@ Earlier work ]) }) - test("lowers failed assistant reasoning to text", () => { + test("drops failed assistant reasoning while preserving settled hosted tools", () => { const messages = toLLMMessages( [ SessionMessage.Assistant.make({ @@ -983,7 +983,6 @@ Earlier work ) expect(messages[0]?.content).toEqual([ - { type: "text", text: "Partial thought" }, { type: "tool-call", id: "hosted-completed", @@ -1029,7 +1028,7 @@ Earlier work ]) }) - test("drops unfinished reasoning metadata from an interrupted assistant message", () => { + test("omits unfinished reasoning from an interrupted assistant message", () => { const messages = toLLMMessages( [ SessionMessage.Assistant.make({ @@ -1057,7 +1056,7 @@ Earlier work model, ) - expect(messages[0]?.content).toEqual([{ type: "text", text: "Completed summary part" }]) + expect(messages).toEqual([]) }) test("drops model-scoped continuation metadata after a model switch but keeps hosted result payloads", () => { diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index ce2dc61e1b2e..948d3d861768 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -6153,7 +6153,7 @@ describe("SessionRunnerLLM", () => { ]) }) - scenario("lowers interrupted reasoning before continuing an incomplete stream", function* (s) { + scenario("drops interrupted reasoning before continuing an incomplete stream", function* (s) { yield* s.admit("Continue interrupted reasoning") yield* s.llm.push( TestLLM.failAfter( @@ -6171,19 +6171,10 @@ describe("SessionRunnerLLM", () => { yield* TestClock.adjust("2400 millis") yield* Fiber.join(run) - expect(s.requests[1]?.messages.at(-2)).toMatchObject({ - role: "assistant", - content: [{ type: "text", text: "Partial thought" }], - }) - expect(s.requests[1]?.messages.at(-1)).toMatchObject({ - role: "user", - content: [ - { - type: "text", - text: INCOMPLETE_STREAM_CONTINUATION, - }, - ], - }) + expect(s.requests[1]?.messages.slice(-2)).toMatchObject([ + { role: "user", content: [{ type: "text", text: "Continue interrupted reasoning" }] }, + { role: "user", content: [{ type: "text", text: INCOMPLETE_STREAM_CONTINUATION }] }, + ]) expect(yield* s.context).toMatchObject([ { type: "user" }, Expected.assistant({ finish: "error" }, [Expected.reasoning("Partial thought")]),