diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 76c72dd..8b67186 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -8,7 +8,6 @@ import { buildToolIdList, createSyntheticAssistantMessageWithToolPart, isIgnoredUserMessage, - hasReasoningInCurrentAssistantTurn, } from "./utils" import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" import { getLastUserMessage } from "../shared-utils" @@ -139,31 +138,16 @@ export const insertPruneToolContext = ( return } - const userInfo = lastUserMessage.info as UserMessage - const providerID = userInfo.model.providerID - const modelID = userInfo.model.modelID - const isGitHubCopilot = - providerID === "github-copilot" || providerID === "github-copilot-enterprise" - - // TODO: This can probably be improved further to only trigger for the appropriate thinking settings - // This setting is also potentially only necessary for claude subscription, API seems to not need this - // validation. See more here: https://platform.claude.com/docs/en/build-with-claude/extended-thinking - const isAnthropic = modelID.includes("claude") - - if (isGitHubCopilot) { - const lastMessage = messages[messages.length - 1] - if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) { - return - } - } - - // Anthropic extended thinking models require a thinking block at the start of its turn - if (isAnthropic) { - if (!hasReasoningInCurrentAssistantTurn(messages)) { - return - } + // Never inject immediately following a user message - wait until assistant has started its turn + // This avoids interfering with model reasoning/thinking phases + // TODO: This can be skipped if there is a good way to check if the model has reasoning, + // can't find a good way to do this yet + const lastMessage = messages[messages.length - 1] + if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) { + return } + const userInfo = lastUserMessage.info as UserMessage const variant = state.variant ?? userInfo.variant messages.push( createSyntheticAssistantMessageWithToolPart(lastUserMessage, prunableToolsContent, variant), diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 26fc29a..fafdccd 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -7,6 +7,11 @@ const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" const SYNTHETIC_CALL_ID = "call_01234567890123456789012345" +const isGeminiModel = (modelID: string): boolean => { + const lowerModelID = modelID.toLowerCase() + return lowerModelID.includes("gemini") +} + export const createSyntheticAssistantMessageWithToolPart = ( baseMessage: WithParts, content: string, @@ -14,25 +19,46 @@ export const createSyntheticAssistantMessageWithToolPart = ( ): WithParts => { const userInfo = baseMessage.info as UserMessage const now = Date.now() - return { - info: { - id: SYNTHETIC_MESSAGE_ID, - sessionID: userInfo.sessionID, - role: "assistant", - agent: userInfo.agent || "code", - parentID: userInfo.id, - modelID: userInfo.model.modelID, - providerID: userInfo.model.providerID, - mode: "default", - path: { - cwd: "/", - root: "/", - }, - time: { created: now, completed: now }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - ...(variant !== undefined && { variant }), + + const baseInfo = { + id: SYNTHETIC_MESSAGE_ID, + sessionID: userInfo.sessionID, + role: "assistant" as const, + agent: userInfo.agent || "code", + parentID: userInfo.id, + modelID: userInfo.model.modelID, + providerID: userInfo.model.providerID, + mode: "default", + path: { + cwd: "/", + root: "/", }, + time: { created: now, completed: now }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...(variant !== undefined && { variant }), + } + + // For Gemini models, inject as text to avoid thought signature requirements + // Gemini 3+ has strict validation requiring thoughtSignature on functionCall parts + if (isGeminiModel(userInfo.model.modelID)) { + return { + info: baseInfo, + parts: [ + { + id: SYNTHETIC_PART_ID, + sessionID: userInfo.sessionID, + messageID: SYNTHETIC_MESSAGE_ID, + type: "text", + text: content, + }, + ], + } + } + + // For other models, use tool part for cleaner context + return { + info: baseInfo, parts: [ { id: SYNTHETIC_PART_ID, @@ -207,23 +233,3 @@ export const isIgnoredUserMessage = (message: WithParts): boolean => { return true } - -export const hasReasoningInCurrentAssistantTurn = (messages: WithParts[]): boolean => { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i] - if (message.info?.role === "user") { - if (isIgnoredUserMessage(message)) { - continue - } - return false - } - if (message.info?.role === "assistant" && message.parts) { - for (const part of message.parts) { - if (part.type === "reasoning") { - return true - } - } - } - } - return false -}