Răsfoiți Sursa

fix: sanitize tool_use_id in tool_result blocks to match API history (#11131)

Tool IDs from providers like Gemini/OpenRouter contain special characters
(e.g., 'functions.read_file:0') that are sanitized when saving tool_use
blocks to API history. However, tool_result blocks were using the original
unsanitized IDs, causing ToolResultIdMismatchError.

This fix ensures tool_result blocks use sanitizeToolUseId() to match the
sanitized tool_use IDs in conversation history.

Fixes EXT-711
Daniel 1 săptămână în urmă
părinte
comite
3400499917

+ 8 - 7
src/core/assistant-message/presentAssistantMessage.ts

@@ -40,6 +40,7 @@ import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
 import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
 
 import { formatResponse } from "../prompts/responses"
+import { sanitizeToolUseId } from "../../utils/tool-id"
 
 /**
  * Processes and presents assistant message content to the user interface.
@@ -118,7 +119,7 @@ export async function presentAssistantMessage(cline: Task) {
 				if (toolCallId) {
 					cline.pushToolResultToUserContent({
 						type: "tool_result",
-						tool_use_id: toolCallId,
+						tool_use_id: sanitizeToolUseId(toolCallId),
 						content: errorMessage,
 						is_error: true,
 					})
@@ -169,7 +170,7 @@ export async function presentAssistantMessage(cline: Task) {
 				if (toolCallId) {
 					cline.pushToolResultToUserContent({
 						type: "tool_result",
-						tool_use_id: toolCallId,
+						tool_use_id: sanitizeToolUseId(toolCallId),
 						content: resultContent,
 					})
 
@@ -410,7 +411,7 @@ export async function presentAssistantMessage(cline: Task) {
 
 				cline.pushToolResultToUserContent({
 					type: "tool_result",
-					tool_use_id: toolCallId,
+					tool_use_id: sanitizeToolUseId(toolCallId),
 					content: errorMessage,
 					is_error: true,
 				})
@@ -447,7 +448,7 @@ export async function presentAssistantMessage(cline: Task) {
 					// continue gracefully.
 					cline.pushToolResultToUserContent({
 						type: "tool_result",
-						tool_use_id: toolCallId,
+						tool_use_id: sanitizeToolUseId(toolCallId),
 						content: formatResponse.toolError(errorMessage),
 						is_error: true,
 					})
@@ -493,7 +494,7 @@ export async function presentAssistantMessage(cline: Task) {
 
 				cline.pushToolResultToUserContent({
 					type: "tool_result",
-					tool_use_id: toolCallId,
+					tool_use_id: sanitizeToolUseId(toolCallId),
 					content: resultContent,
 				})
 
@@ -644,7 +645,7 @@ export async function presentAssistantMessage(cline: Task) {
 					// Push tool_result directly without setting didAlreadyUseTool
 					cline.pushToolResultToUserContent({
 						type: "tool_result",
-						tool_use_id: toolCallId,
+						tool_use_id: sanitizeToolUseId(toolCallId),
 						content: typeof errorContent === "string" ? errorContent : "(validation error)",
 						is_error: true,
 					})
@@ -947,7 +948,7 @@ export async function presentAssistantMessage(cline: Task) {
 					// This prevents the stream from being interrupted with "Response interrupted by tool use result"
 					cline.pushToolResultToUserContent({
 						type: "tool_result",
-						tool_use_id: toolCallId,
+						tool_use_id: sanitizeToolUseId(toolCallId),
 						content: formatResponse.toolError(errorMessage),
 						is_error: true,
 					})

+ 8 - 0
src/utils/__tests__/tool-id.spec.ts

@@ -47,6 +47,14 @@ describe("sanitizeToolUseId", () => {
 		it("should replace multiple invalid characters", () => {
 			expect(sanitizeToolUseId("mcp.server:tool/name")).toBe("mcp_server_tool_name")
 		})
+
+		it("should sanitize Gemini/OpenRouter function call IDs with dots and colons", () => {
+			// This is the exact pattern seen in PostHog errors where tool_result IDs
+			// didn't match tool_use IDs due to missing sanitization
+			expect(sanitizeToolUseId("functions.read_file:0")).toBe("functions_read_file_0")
+			expect(sanitizeToolUseId("functions.write_to_file:1")).toBe("functions_write_to_file_1")
+			expect(sanitizeToolUseId("read_file:0")).toBe("read_file_0")
+		})
 	})
 
 	describe("real-world MCP tool use ID patterns", () => {