From 33f3e402e50325df3e3c38519941e89ee896a996 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Sun, 21 Dec 2025 17:50:03 -0700 Subject: [PATCH] Track edit_file variant tool usage in analytics --- packages/types/src/providers/gemini.ts | 44 +++--- packages/types/src/providers/minimax.ts | 9 +- packages/types/src/providers/openai.ts | 68 ++++----- packages/types/src/providers/vertex.ts | 76 ++++----- packages/types/src/providers/xai.ts | 24 +-- packages/types/src/tool.ts | 8 +- .../providers/__tests__/openrouter.spec.ts | 25 ++- .../providers/fetchers/__tests__/roo.spec.ts | 24 +-- .../utils/router-tool-preferences.ts | 30 ++-- .../assistant-message/NativeToolCallParser.ts | 100 ++++++------ ...sistantMessage-tool-usage-variants.spec.ts | 144 ++++++++++++++++++ .../presentAssistantMessage.ts | 105 ++++++------- .../prompts/tools/filter-tools-for-mode.ts | 12 -- ...eplaceTool.ts => EditFileAnthropicTool.ts} | 26 ++-- ...ApplyPatchTool.ts => EditFileCodexTool.ts} | 27 ++-- ...{EditFileTool.ts => EditFileGeminiTool.ts} | 4 +- ...archReplaceTool.ts => EditFileGrokTool.ts} | 32 ++-- .../{ApplyDiffTool.ts => EditFileRooTool.ts} | 36 ++--- src/core/tools/MultiApplyDiffTool.ts | 6 +- ...eTool.spec.ts => EditFileGrokTool.spec.ts} | 18 +-- .../applyDiffTool.experiment.spec.ts | 6 +- src/core/tools/__tests__/editFileTool.spec.ts | 4 +- src/shared/__tests__/modes.spec.ts | 10 +- src/shared/tools.ts | 82 +++++----- 24 files changed, 506 insertions(+), 414 deletions(-) create mode 100644 src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts rename src/core/tools/{SearchAndReplaceTool.ts => EditFileAnthropicTool.ts} (92%) rename src/core/tools/{ApplyPatchTool.ts => EditFileCodexTool.ts} (95%) rename src/core/tools/{EditFileTool.ts => EditFileGeminiTool.ts} (99%) rename src/core/tools/{SearchReplaceTool.ts => EditFileGrokTool.ts} (90%) rename src/core/tools/{ApplyDiffTool.ts => EditFileRooTool.ts} (89%) rename src/core/tools/__tests__/{searchReplaceTool.spec.ts => EditFileGrokTool.spec.ts} (96%) diff --git a/packages/types/src/providers/gemini.ts b/packages/types/src/providers/gemini.ts index 17aa16db27..01e089e96f 100644 --- a/packages/types/src/providers/gemini.ts +++ b/packages/types/src/providers/gemini.ts @@ -15,8 +15,8 @@ export const geminiModels = { supportsPromptCache: true, supportsReasoningEffort: ["low", "high"], reasoningEffort: "low", - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], supportsTemperature: true, defaultTemperature: 1, inputPrice: 4.0, @@ -43,8 +43,8 @@ export const geminiModels = { supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], reasoningEffort: "medium", - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], supportsTemperature: true, defaultTemperature: 1, inputPrice: 0.3, @@ -60,8 +60,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. outputPrice: 15, cacheReadsPrice: 0.625, @@ -91,8 +91,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. outputPrice: 15, cacheReadsPrice: 0.625, @@ -121,8 +121,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. outputPrice: 15, cacheReadsPrice: 0.625, @@ -149,8 +149,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, // This is the pricing for prompts above 200k tokens. outputPrice: 15, cacheReadsPrice: 0.625, @@ -181,8 +181,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.3, outputPrice: 2.5, cacheReadsPrice: 0.075, @@ -197,8 +197,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.3, outputPrice: 2.5, cacheReadsPrice: 0.075, @@ -213,8 +213,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.3, outputPrice: 2.5, cacheReadsPrice: 0.075, @@ -231,8 +231,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.1, outputPrice: 0.4, cacheReadsPrice: 0.025, @@ -247,8 +247,8 @@ export const geminiModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.1, outputPrice: 0.4, cacheReadsPrice: 0.025, diff --git a/packages/types/src/providers/minimax.ts b/packages/types/src/providers/minimax.ts index 7152946f7f..b824c270fd 100644 --- a/packages/types/src/providers/minimax.ts +++ b/packages/types/src/providers/minimax.ts @@ -15,8 +15,7 @@ export const minimaxModels = { supportsPromptCache: true, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["search_and_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "anthropic", preserveReasoning: true, inputPrice: 0.3, outputPrice: 1.2, @@ -32,8 +31,7 @@ export const minimaxModels = { supportsPromptCache: true, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["search_and_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "anthropic", preserveReasoning: true, inputPrice: 0.3, outputPrice: 1.2, @@ -49,8 +47,7 @@ export const minimaxModels = { supportsPromptCache: true, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["search_and_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "anthropic", preserveReasoning: true, inputPrice: 0.3, outputPrice: 1.2, diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 47b883e0e1..1d83cff431 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -11,8 +11,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, promptCacheRetention: "24h", @@ -31,8 +31,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, promptCacheRetention: "24h", @@ -54,8 +54,8 @@ export const openAiNativeModels = { contextWindow: 128_000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, inputPrice: 1.75, @@ -68,8 +68,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, promptCacheRetention: "24h", @@ -91,8 +91,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, promptCacheRetention: "24h", @@ -110,8 +110,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, promptCacheRetention: "24h", @@ -128,8 +128,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], @@ -150,8 +150,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], @@ -172,8 +172,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["low", "medium", "high"], @@ -190,8 +190,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], @@ -209,8 +209,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, inputPrice: 1.25, @@ -223,8 +223,8 @@ export const openAiNativeModels = { contextWindow: 1_047_576, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, inputPrice: 2, @@ -240,8 +240,8 @@ export const openAiNativeModels = { contextWindow: 1_047_576, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, inputPrice: 0.4, @@ -257,8 +257,8 @@ export const openAiNativeModels = { contextWindow: 1_047_576, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, inputPrice: 0.1, @@ -483,8 +483,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], @@ -505,8 +505,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], @@ -527,8 +527,8 @@ export const openAiNativeModels = { contextWindow: 400000, supportsNativeTools: true, defaultToolProtocol: "native", - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], supportsImages: true, supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index 384b78de4d..0307fe6f66 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -15,8 +15,8 @@ export const vertexModels = { supportsPromptCache: true, supportsReasoningEffort: ["low", "high"], reasoningEffort: "low", - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], supportsTemperature: true, defaultTemperature: 1, inputPrice: 4.0, @@ -43,8 +43,8 @@ export const vertexModels = { supportsPromptCache: true, supportsReasoningEffort: ["minimal", "low", "medium", "high"], reasoningEffort: "medium", - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], supportsTemperature: true, defaultTemperature: 1, inputPrice: 0.3, @@ -59,8 +59,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.15, outputPrice: 3.5, maxThinkingTokens: 24_576, @@ -74,8 +74,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.15, outputPrice: 0.6, }, @@ -86,8 +86,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.3, outputPrice: 2.5, cacheReadsPrice: 0.075, @@ -102,8 +102,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.15, outputPrice: 3.5, maxThinkingTokens: 24_576, @@ -117,8 +117,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.15, outputPrice: 0.6, }, @@ -129,8 +129,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, outputPrice: 15, }, @@ -141,8 +141,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, outputPrice: 15, }, @@ -153,8 +153,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, outputPrice: 15, maxThinkingTokens: 32_768, @@ -167,8 +167,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 2.5, outputPrice: 15, maxThinkingTokens: 32_768, @@ -196,8 +196,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0, outputPrice: 0, }, @@ -208,8 +208,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0, outputPrice: 0, }, @@ -220,8 +220,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.15, outputPrice: 0.6, }, @@ -232,8 +232,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.075, outputPrice: 0.3, }, @@ -244,8 +244,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0, outputPrice: 0, }, @@ -256,8 +256,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.075, outputPrice: 0.3, }, @@ -268,8 +268,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: false, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 1.25, outputPrice: 5, }, @@ -463,8 +463,8 @@ export const vertexModels = { supportsNativeTools: true, defaultToolProtocol: "native", supportsPromptCache: true, - includedTools: ["write_file", "edit_file"], - excludedTools: ["apply_diff"], + editToolVariant: "gemini", + includedTools: ["write_file"], inputPrice: 0.1, outputPrice: 0.4, cacheReadsPrice: 0.025, diff --git a/packages/types/src/providers/xai.ts b/packages/types/src/providers/xai.ts index 23acb487aa..2a3b786f61 100644 --- a/packages/types/src/providers/xai.ts +++ b/packages/types/src/providers/xai.ts @@ -18,8 +18,7 @@ export const xaiModels = { cacheWritesPrice: 0.02, cacheReadsPrice: 0.02, description: "xAI's Grok Code Fast model with 256K context window", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-4-1-fast-reasoning": { maxTokens: 65_536, @@ -34,8 +33,7 @@ export const xaiModels = { cacheReadsPrice: 0.05, description: "xAI's Grok 4.1 Fast model with 2M context window, optimized for high-performance agentic tool calling with reasoning", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-4-1-fast-non-reasoning": { maxTokens: 65_536, @@ -50,8 +48,7 @@ export const xaiModels = { cacheReadsPrice: 0.05, description: "xAI's Grok 4.1 Fast model with 2M context window, optimized for high-performance agentic tool calling", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-4-fast-reasoning": { maxTokens: 65_536, @@ -66,8 +63,7 @@ export const xaiModels = { cacheReadsPrice: 0.05, description: "xAI's Grok 4 Fast model with 2M context window, optimized for high-performance agentic tool calling with reasoning", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-4-fast-non-reasoning": { maxTokens: 65_536, @@ -82,8 +78,7 @@ export const xaiModels = { cacheReadsPrice: 0.05, description: "xAI's Grok 4 Fast model with 2M context window, optimized for high-performance agentic tool calling", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-4-0709": { maxTokens: 8192, @@ -97,8 +92,7 @@ export const xaiModels = { cacheWritesPrice: 0.75, cacheReadsPrice: 0.75, description: "xAI's Grok-4 model with 256K context window", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-3-mini": { maxTokens: 8192, @@ -114,8 +108,7 @@ export const xaiModels = { description: "xAI's Grok-3 mini model with 128K context window", supportsReasoningEffort: ["low", "high"], reasoningEffort: "low", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, "grok-3": { maxTokens: 8192, @@ -129,7 +122,6 @@ export const xaiModels = { cacheWritesPrice: 0.75, cacheReadsPrice: 0.75, description: "xAI's Grok-3 model with 128K context window", - includedTools: ["search_replace"], - excludedTools: ["apply_diff"], + editToolVariant: "grok", }, } as const satisfies Record diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 8b20dffb51..46bc6e26a6 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -30,13 +30,11 @@ export const toolNames = [ "execute_command", "read_file", "write_to_file", - // Legacy edit tool names (deprecated, use edit_file_* variants) + // apply_diff is kept for XML protocol backward compatibility "apply_diff", - "search_and_replace", - "search_replace", + // Unified edit tool for native protocol (variant selected via modelInfo.editToolVariant) "edit_file", - "apply_patch", - // New edit tool variant names + // Internal edit tool variant names (used by native protocol, presented to LLM as "edit_file") "edit_file_roo", "edit_file_anthropic", "edit_file_grok", diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index 8875df9a47..c08405775f 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -161,7 +161,7 @@ describe("OpenRouterHandler", () => { expect(result.temperature).toBe(0) }) - it("adds excludedTools and includedTools for OpenAI models", async () => { + it("sets editToolVariant to codex for OpenAI models", async () => { const handler = new OpenRouterHandler({ openRouterApiKey: "test-key", openRouterModelId: "openai/gpt-4o", @@ -169,12 +169,10 @@ describe("OpenRouterHandler", () => { const result = await handler.fetchModel() expect(result.id).toBe("openai/gpt-4o") - expect(result.info.excludedTools).toContain("apply_diff") - expect(result.info.excludedTools).toContain("write_to_file") - expect(result.info.includedTools).toContain("apply_patch") + expect(result.info.editToolVariant).toBe("codex") }) - it("merges excludedTools and includedTools with existing values for OpenAI models", async () => { + it("sets editToolVariant to codex for OpenAI models while preserving existing excludedTools/includedTools", async () => { const handler = new OpenRouterHandler({ openRouterApiKey: "test-key", openRouterModelId: "openai/o1", @@ -182,18 +180,14 @@ describe("OpenRouterHandler", () => { const result = await handler.fetchModel() expect(result.id).toBe("openai/o1") - // Should have the new exclusions - expect(result.info.excludedTools).toContain("apply_diff") - expect(result.info.excludedTools).toContain("write_to_file") - // Should preserve existing exclusions + // Should have editToolVariant set + expect(result.info.editToolVariant).toBe("codex") + // Should preserve existing exclusions/inclusions from model info expect(result.info.excludedTools).toContain("existing_excluded") - // Should have the new inclusions - expect(result.info.includedTools).toContain("apply_patch") - // Should preserve existing inclusions expect(result.info.includedTools).toContain("existing_included") }) - it("does not add excludedTools or includedTools for non-OpenAI models", async () => { + it("does not set editToolVariant for non-OpenAI models", async () => { const handler = new OpenRouterHandler({ openRouterApiKey: "test-key", openRouterModelId: "anthropic/claude-sonnet-4", @@ -201,9 +195,8 @@ describe("OpenRouterHandler", () => { const result = await handler.fetchModel() expect(result.id).toBe("anthropic/claude-sonnet-4") - // Should NOT have the tool exclusions/inclusions - expect(result.info.excludedTools).toBeUndefined() - expect(result.info.includedTools).toBeUndefined() + // Should NOT have editToolVariant set + expect(result.info.editToolVariant).toBeUndefined() }) }) diff --git a/src/api/providers/fetchers/__tests__/roo.spec.ts b/src/api/providers/fetchers/__tests__/roo.spec.ts index cd86be0b69..f52ee853c1 100644 --- a/src/api/providers/fetchers/__tests__/roo.spec.ts +++ b/src/api/providers/fetchers/__tests__/roo.spec.ts @@ -741,8 +741,8 @@ describe("getRooModels", () => { output: "0.0002", }, settings: { - includedTools: ["apply_patch"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "codex", + excludedTools: ["write_to_file"], reasoningEffort: "high", }, }, @@ -756,8 +756,8 @@ describe("getRooModels", () => { const models = await getRooModels(baseUrl, apiKey) - expect(models["test/model-with-settings"].includedTools).toEqual(["apply_patch"]) - expect(models["test/model-with-settings"].excludedTools).toEqual(["apply_diff", "write_to_file"]) + expect(models["test/model-with-settings"].editToolVariant).toBe("codex") + expect(models["test/model-with-settings"].excludedTools).toEqual(["write_to_file"]) expect(models["test/model-with-settings"].reasoningEffort).toBe("high") }) @@ -824,14 +824,14 @@ describe("getRooModels", () => { }, // Plain settings for backward compatibility with old clients settings: { - includedTools: ["apply_patch"], + editToolVariant: "codex", excludedTools: ["write_to_file"], }, // Versioned settings keyed by version number (low version - always met) versionedSettings: { "1.0.0": { - includedTools: ["apply_patch", "search_replace"], - excludedTools: ["apply_diff", "write_to_file"], + editToolVariant: "grok", + excludedTools: ["write_to_file", "browser_action"], }, }, }, @@ -846,8 +846,8 @@ describe("getRooModels", () => { const models = await getRooModels(baseUrl, apiKey) // Versioned settings should be used instead of plain settings - expect(models["test/versioned-model"].includedTools).toEqual(["apply_patch", "search_replace"]) - expect(models["test/versioned-model"].excludedTools).toEqual(["apply_diff", "write_to_file"]) + expect(models["test/versioned-model"].editToolVariant).toBe("grok") + expect(models["test/versioned-model"].excludedTools).toEqual(["write_to_file", "browser_action"]) }) it("should use plain settings when no versioned settings version matches", async () => { @@ -870,12 +870,12 @@ describe("getRooModels", () => { output: "0.0002", }, settings: { - includedTools: ["apply_patch"], + editToolVariant: "codex", }, // Versioned settings keyed by very high version - never met versionedSettings: { "99.0.0": { - includedTools: ["apply_patch", "search_replace"], + editToolVariant: "grok", }, }, }, @@ -890,7 +890,7 @@ describe("getRooModels", () => { const models = await getRooModels(baseUrl, apiKey) // Should use plain settings since no versioned settings match current version - expect(models["test/old-version-model"].includedTools).toEqual(["apply_patch"]) + expect(models["test/old-version-model"].editToolVariant).toBe("codex") }) it("should handle model with only versionedSettings and no plain settings", async () => { diff --git a/src/api/providers/utils/router-tool-preferences.ts b/src/api/providers/utils/router-tool-preferences.ts index bb5ece3b96..6ee63130c9 100644 --- a/src/api/providers/utils/router-tool-preferences.ts +++ b/src/api/providers/utils/router-tool-preferences.ts @@ -3,12 +3,13 @@ import type { ModelInfo } from "@roo-code/types" /** * Apply tool preferences for models accessed through dynamic routers (OpenRouter, Requesty). * - * Different model families perform better with specific tools: - * - OpenAI models: Better results with apply_patch instead of apply_diff/write_to_file - * - Gemini models: Higher quality results with write_file and edit_file + * Different model families perform better with specific edit tool schemas: + * - OpenAI models: Better results with the codex (patch) format + * - Gemini models: Higher quality results with the gemini (search/replace) format + * - xAI/Grok models: Better results with the grok format * * This function modifies the model info to apply these preferences consistently - * across all dynamic router providers. + * across all dynamic router providers via `editToolVariant`. * * @param modelId The model identifier (e.g., "openai/gpt-4", "google/gemini-2.5-pro") * @param info The original model info object @@ -17,23 +18,32 @@ import type { ModelInfo } from "@roo-code/types" export function applyRouterToolPreferences(modelId: string, info: ModelInfo): ModelInfo { let result = info - // For OpenAI models via routers, exclude write_to_file and apply_diff, and include apply_patch + // For OpenAI models via routers, use codex variant and exclude write_to_file // This matches the behavior of the native OpenAI provider if (modelId.includes("openai")) { result = { ...result, - excludedTools: [...new Set([...(result.excludedTools || []), "apply_diff", "write_to_file"])], - includedTools: [...new Set([...(result.includedTools || []), "apply_patch"])], + editToolVariant: result.editToolVariant ?? "codex", + excludedTools: [...new Set([...(result.excludedTools || []), "write_to_file"])], } } - // For Gemini models via routers, include write_file and edit_file + // For Gemini models via routers, use gemini variant // This matches the behavior of the native Gemini provider if (modelId.includes("gemini")) { result = { ...result, - excludedTools: [...new Set([...(result.excludedTools || []), "apply_diff"])], - includedTools: [...new Set([...(result.includedTools || []), "write_file", "edit_file"])], + editToolVariant: result.editToolVariant ?? "gemini", + includedTools: [...new Set([...(result.includedTools || []), "write_file"])], + } + } + + // For xAI/Grok models via routers, use grok variant + // This matches the behavior of the native xAI provider + if (modelId.includes("grok") || modelId.includes("xai")) { + result = { + ...result, + editToolVariant: result.editToolVariant ?? "grok", } } diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index a90845769f..a2b1f23bb2 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -497,43 +497,36 @@ export class NativeToolCallParser { } break - case "apply_patch": - if (partialArgs.patch !== undefined) { + case "edit_file": + // Handle unified edit_file with variant-aware argument detection + // Detect variant based on which arguments are present: + // - roo variant: { path, diff } + // - anthropic variant: { path, edits: [{ old_text, new_text }] } + // - grok/gemini variant: { file_path, old_string, new_string } + // - codex variant: { patch } + if (partialArgs.diff !== undefined) { + // Roo variant (apply_diff format) nativeArgs = { - patch: partialArgs.patch, + path: partialArgs.path, + diff: partialArgs.diff, } - } - break - - case "search_replace": - if ( - partialArgs.file_path !== undefined || - partialArgs.old_string !== undefined || - partialArgs.new_string !== undefined - ) { - nativeArgs = { - file_path: partialArgs.file_path, - old_string: partialArgs.old_string, - new_string: partialArgs.new_string, - } - } - break - - case "search_and_replace": - if (partialArgs.path !== undefined || partialArgs.edits !== undefined) { + } else if (partialArgs.edits !== undefined) { + // Anthropic variant (search_and_replace format) nativeArgs = { path: partialArgs.path, edits: partialArgs.edits, } - } - break - - case "edit_file": - if ( + } else if (partialArgs.patch !== undefined) { + // Codex variant (apply_patch format) + nativeArgs = { + patch: partialArgs.patch, + } + } else if ( partialArgs.file_path !== undefined || partialArgs.old_string !== undefined || partialArgs.new_string !== undefined ) { + // Grok/Gemini variant (search_replace/edit_file format) nativeArgs = { file_path: partialArgs.file_path, old_string: partialArgs.old_string, @@ -660,15 +653,6 @@ export class NativeToolCallParser { } break - case "search_and_replace": - if (args.path !== undefined && args.edits !== undefined && Array.isArray(args.edits)) { - nativeArgs = { - path: args.path, - edits: args.edits, - } as NativeArgsFor - } - break - case "ask_followup_question": if (args.question !== undefined && args.follow_up !== undefined) { nativeArgs = { @@ -782,34 +766,36 @@ export class NativeToolCallParser { } break - case "apply_patch": - if (args.patch !== undefined) { + case "edit_file": + // Handle unified edit_file with variant-aware argument detection + // Detect variant based on which arguments are present: + // - roo variant: { path, diff } + // - anthropic variant: { path, edits: [{ old_text, new_text }] } + // - grok/gemini variant: { file_path, old_string, new_string } + // - codex variant: { patch } + if (args.diff !== undefined && args.path !== undefined) { + // Roo variant (apply_diff format) + nativeArgs = { + path: args.path, + diff: args.diff, + } as NativeArgsFor + } else if (args.edits !== undefined && Array.isArray(args.edits) && args.path !== undefined) { + // Anthropic variant (search_and_replace format) + nativeArgs = { + path: args.path, + edits: args.edits, + } as NativeArgsFor + } else if (args.patch !== undefined) { + // Codex variant (apply_patch format) nativeArgs = { patch: args.patch, } as NativeArgsFor - } - break - - case "search_replace": - if ( - args.file_path !== undefined && - args.old_string !== undefined && - args.new_string !== undefined - ) { - nativeArgs = { - file_path: args.file_path, - old_string: args.old_string, - new_string: args.new_string, - } as NativeArgsFor - } - break - - case "edit_file": - if ( + } else if ( args.file_path !== undefined && args.old_string !== undefined && args.new_string !== undefined ) { + // Grok/Gemini variant (search_replace/edit_file format) nativeArgs = { file_path: args.file_path, old_string: args.old_string, diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts new file mode 100644 index 0000000000..efdecaceda --- /dev/null +++ b/src/core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts @@ -0,0 +1,144 @@ +// npx vitest run core/assistant-message/__tests__/presentAssistantMessage-tool-usage-variants.spec.ts + +import { describe, it, expect, beforeEach, vi } from "vitest" + +vi.mock("../../tools/validateToolUse", () => ({ + validateToolUse: vi.fn(), +})) + +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureToolUsage: vi.fn(), + captureConsecutiveMistakeError: vi.fn(), + captureException: vi.fn(), + }, + }, +})) + +const { mockEditToolHandle } = vi.hoisted(() => ({ + mockEditToolHandle: vi.fn(async (_task: unknown, _toolUse: unknown, callbacks: any) => { + callbacks.pushToolResult("ok") + }), +})) + +vi.mock("../../tools/EditFileRooTool", () => ({ + editFileRooTool: { handle: mockEditToolHandle }, +})) + +vi.mock("../../tools/EditFileAnthropicTool", () => ({ + editFileAnthropicTool: { handle: mockEditToolHandle }, +})) + +vi.mock("../../tools/EditFileGrokTool", () => ({ + editFileGrokTool: { handle: mockEditToolHandle }, +})) + +vi.mock("../../tools/EditFileGeminiTool", () => ({ + editFileGeminiTool: { handle: mockEditToolHandle }, +})) + +vi.mock("../../tools/EditFileCodexTool", () => ({ + editFileCodexTool: { handle: mockEditToolHandle }, +})) + +// Import AFTER mocks +import { TelemetryService } from "@roo-code/telemetry" +import { presentAssistantMessage } from "../presentAssistantMessage" + +describe("presentAssistantMessage - tool usage analytics names", () => { + let mockTask: any + + beforeEach(() => { + vi.clearAllMocks() + + mockTask = { + taskId: "test-task-id", + instanceId: "test-instance", + abort: false, + presentAssistantMessageLocked: false, + presentAssistantMessageHasPendingUpdates: false, + currentStreamingContentIndex: 0, + assistantMessageContent: [], + userMessageContent: [], + didCompleteReadingStream: false, + didRejectTool: false, + didAlreadyUseTool: false, + diffEnabled: false, + consecutiveMistakeCount: 0, + consecutiveMistakeLimit: 3, + clineMessages: [], + currentStreamingDidCheckpoint: false, + checkpointSave: vi.fn().mockResolvedValue(undefined), + apiConfiguration: { apiProvider: "openai" }, + api: { + getModel: () => ({ id: "test-model", info: { editToolVariant: "roo" } }), + }, + browserSession: { + closeBrowser: vi.fn().mockResolvedValue(undefined), + }, + recordToolUsage: vi.fn(), + recordToolError: vi.fn(), + toolRepetitionDetector: { + check: vi.fn().mockReturnValue({ allowExecution: true }), + }, + providerRef: { + deref: () => ({ + getState: vi.fn().mockResolvedValue({ mode: "code", customModes: [], experiments: {} }), + }), + }, + say: vi.fn().mockResolvedValue(undefined), + ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), + } + }) + + it.each([ + ["roo", "edit_file_roo"], + ["anthropic", "edit_file_anthropic"], + ["grok", "edit_file_grok"], + ["gemini", "edit_file_gemini"], + ["codex", "edit_file_codex"], + ] as const)("records derived analytics tool name for edit_file (%s)", async (variant, expected) => { + mockTask.api.getModel = () => ({ id: "test-model", info: { editToolVariant: variant } }) + + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "tool_call_123", + name: "edit_file", + params: { path: "file.txt" }, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + expect(mockTask.recordToolUsage).toHaveBeenCalledWith(expected) + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + expected, + expect.any(String), + ) + }) + + it("does not change other tool names", async () => { + mockTask.assistantMessageContent = [ + { + type: "tool_use", + id: "tool_call_456", + name: "read_file", + params: { path: "file.txt" }, + partial: false, + }, + ] + + await presentAssistantMessage(mockTask) + + expect(mockTask.recordToolUsage).toHaveBeenCalledWith("read_file") + expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith( + mockTask.taskId, + "read_file", + expect.any(String), + ) + }) +}) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index e5ba2c26dd..33c769995a 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -22,10 +22,10 @@ import { readFileTool } from "../tools/ReadFileTool" import { TOOL_PROTOCOL } from "@roo-code/types" import { writeToFileTool } from "../tools/WriteToFileTool" import { applyDiffTool } from "../tools/MultiApplyDiffTool" -import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool" -import { searchReplaceTool } from "../tools/SearchReplaceTool" -import { editFileTool } from "../tools/EditFileTool" -import { applyPatchTool } from "../tools/ApplyPatchTool" +import { editFileAnthropicTool } from "../tools/EditFileAnthropicTool" +import { editFileGrokTool } from "../tools/EditFileGrokTool" +import { editFileGeminiTool } from "../tools/EditFileGeminiTool" +import { editFileCodexTool } from "../tools/EditFileCodexTool" import { searchFilesTool } from "../tools/SearchFilesTool" import { browserActionTool } from "../tools/BrowserActionTool" import { executeCommandTool } from "../tools/ExecuteCommandTool" @@ -38,12 +38,28 @@ import { newTaskTool } from "../tools/NewTaskTool" import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { generateImageTool } from "../tools/GenerateImageTool" -import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool" +import { editFileRooTool } from "../tools/EditFileRooTool" import { validateToolUse } from "../tools/validateToolUse" import { codebaseSearchTool } from "../tools/CodebaseSearchTool" import { formatResponse } from "../prompts/responses" +function getAnalyticsToolNameForToolUse(toolName: ToolName, editToolVariant: EditToolVariant): ToolName { + if (toolName !== "edit_file") { + return toolName + } + + const analyticsToolNameByVariant = { + roo: "edit_file_roo", + anthropic: "edit_file_anthropic", + grok: "edit_file_grok", + gemini: "edit_file_gemini", + codex: "edit_file_codex", + } satisfies Record + + return analyticsToolNameByVariant[editToolVariant] +} + /** * Processes and presents assistant message content to the user interface. * @@ -395,10 +411,6 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name} for '${block.params.regex}'${ block.params.file_pattern ? ` in '${block.params.file_pattern}'` : "" }]` - case "search_and_replace": - return `[${block.name} for '${block.params.path}']` - case "search_replace": - return `[${block.name} for '${block.params.file_path}']` case "edit_file": { // Unified edit_file tool - path location depends on variant // Gemini/Grok variants use file_path, Roo/Anthropic use path @@ -412,8 +424,6 @@ export async function presentAssistantMessage(cline: Task) { } return `[${block.name}]` } - case "apply_patch": - return `[${block.name}]` case "list_files": return `[${block.name} for '${block.params.path}']` case "browser_action": @@ -706,11 +716,22 @@ export async function presentAssistantMessage(cline: Task) { } if (!block.partial) { + // Determine the tool name for analytics + let toolNameForAnalytics: ToolName = block.name + // Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools) const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name) - const recordName = isCustomTool ? "custom_tool" : block.name - cline.recordToolUsage(recordName) - TelemetryService.instance.captureToolUsage(cline.taskId, recordName, toolProtocol) + if (isCustomTool) { + toolNameForAnalytics = "custom_tool" + } else if (toolNameForAnalytics === "edit_file") { + // Map edit_file to specific variant for analytics + const modelInfo = cline.api.getModel() + const editToolVariant: EditToolVariant = modelInfo?.info?.editToolVariant ?? "roo" + toolNameForAnalytics = getAnalyticsToolNameForToolUse(toolNameForAnalytics, editToolVariant) + } + + cline.recordToolUsage(toolNameForAnalytics) + TelemetryService.instance.captureToolUsage(cline.taskId, toolNameForAnalytics, toolProtocol) } // Validate tool use before execution - ONLY for complete (non-partial) blocks. @@ -840,7 +861,7 @@ export async function presentAssistantMessage(cline: Task) { // Check if this tool call came from native protocol by checking for ID // Native calls always have IDs, XML calls never do if (toolProtocol === TOOL_PROTOCOL.NATIVE) { - await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, @@ -865,7 +886,7 @@ export async function presentAssistantMessage(cline: Task) { if (isMultiFileApplyDiffEnabled) { await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) } else { - await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, @@ -875,26 +896,6 @@ export async function presentAssistantMessage(cline: Task) { } break } - case "search_and_replace": - await checkpointSaveAndMark(cline) - await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, { - askApproval, - handleError, - pushToolResult, - removeClosingTag, - toolProtocol, - }) - break - case "search_replace": - await checkpointSaveAndMark(cline) - await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, { - askApproval, - handleError, - pushToolResult, - removeClosingTag, - toolProtocol, - }) - break case "edit_file": { // Unified edit_file tool - route to correct handler based on editToolVariant await checkpointSaveAndMark(cline) @@ -903,8 +904,8 @@ export async function presentAssistantMessage(cline: Task) { switch (editToolVariant) { case "roo": - // Route to apply_diff handler (Roo variant) - await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + // Route to Roo variant (unified diff format) + await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, @@ -913,8 +914,8 @@ export async function presentAssistantMessage(cline: Task) { }) break case "anthropic": - // Route to search_and_replace handler (Anthropic variant) - await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, { + // Route to Anthropic variant (multi-edit search/replace) + await editFileAnthropicTool.handle(cline, block as ToolUse<"edit_file_anthropic">, { askApproval, handleError, pushToolResult, @@ -923,8 +924,8 @@ export async function presentAssistantMessage(cline: Task) { }) break case "grok": - // Route to search_replace handler (Grok variant) - await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, { + // Route to Grok variant (single search/replace) + await editFileGrokTool.handle(cline, block as ToolUse<"edit_file_grok">, { askApproval, handleError, pushToolResult, @@ -933,8 +934,8 @@ export async function presentAssistantMessage(cline: Task) { }) break case "gemini": - // Route to edit_file handler (Gemini variant) - await editFileTool.handle(cline, block as ToolUse<"edit_file">, { + // Route to Gemini variant (search/replace with expected_replacements) + await editFileGeminiTool.handle(cline, block as ToolUse<"edit_file">, { askApproval, handleError, pushToolResult, @@ -943,8 +944,8 @@ export async function presentAssistantMessage(cline: Task) { }) break case "codex": - // Route to apply_patch handler (Codex variant) - await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, { + // Route to Codex variant (unified patch format) + await editFileCodexTool.handle(cline, block as ToolUse<"edit_file_codex">, { askApproval, handleError, pushToolResult, @@ -955,7 +956,7 @@ export async function presentAssistantMessage(cline: Task) { default: { // Should never happen, but default to roo variant const _exhaustiveCheck: never = editToolVariant - await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + await editFileRooTool.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, @@ -966,16 +967,6 @@ export async function presentAssistantMessage(cline: Task) { } break } - case "apply_patch": - await checkpointSaveAndMark(cline) - await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, { - askApproval, - handleError, - pushToolResult, - removeClosingTag, - toolProtocol, - }) - break case "read_file": // Type assertion is safe here because we're in the "read_file" case await readFileTool.handle(cline, block as ToolUse<"read_file">, { diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index 92231d5c67..0342557cf1 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -25,12 +25,6 @@ const EDIT_TOOL_VARIANT_MAP: Record = { */ const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANT_MAP)) -/** - * Legacy edit tool names that are now aliases. - * These should be excluded from the tool list since they're replaced by the variants. - */ -const LEGACY_EDIT_TOOL_NAMES = new Set(["apply_diff", "search_and_replace", "search_replace", "apply_patch"]) - /** * Reverse lookup map - maps alias name to canonical tool name. * Built once at module load from the central TOOL_ALIASES constant. @@ -326,12 +320,6 @@ export function filterNativeToolsForMode( } // Handle edit tool variant selection: - // 1. Remove legacy edit tool names (they're now aliases) - // 2. Remove non-selected edit tool variants - // 3. The selected variant will be renamed to "edit_file" below - for (const legacyTool of LEGACY_EDIT_TOOL_NAMES) { - allowedToolNames.delete(legacyTool) - } for (const variantTool of ALL_EDIT_TOOL_VARIANTS) { allowedToolNames.delete(variantTool) } diff --git a/src/core/tools/SearchAndReplaceTool.ts b/src/core/tools/EditFileAnthropicTool.ts similarity index 92% rename from src/core/tools/SearchAndReplaceTool.ts rename to src/core/tools/EditFileAnthropicTool.ts index 8b1d21803f..e015486cce 100644 --- a/src/core/tools/SearchAndReplaceTool.ts +++ b/src/core/tools/EditFileAnthropicTool.ts @@ -24,8 +24,8 @@ interface SearchAndReplaceParams { edits: EditOperation[] } -export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { - readonly name = "search_and_replace" as const +export class SearchAndReplaceTool extends BaseTool<"edit_file_anthropic"> { + readonly name = "edit_file_anthropic" as const parseLegacy(params: Partial>): SearchAndReplaceParams { // Parse edits from JSON string if provided @@ -52,14 +52,14 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { // Validate required parameters if (!relPath) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") - pushToolResult(await task.sayAndCreateMissingParamError("search_and_replace", "path")) + task.recordToolError("edit_file_anthropic") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_anthropic", "path")) return } if (!edits || !Array.isArray(edits) || edits.length === 0) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") + task.recordToolError("edit_file_anthropic") pushToolResult( formatResponse.toolError( "Missing or empty 'edits' parameter. At least one edit operation is required.", @@ -73,13 +73,13 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { const op = edits[i] if (!op.old_text) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") + task.recordToolError("edit_file_anthropic") pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'old_text' field.`)) return } if (op.new_text === undefined) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") + task.recordToolError("edit_file_anthropic") pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'new_text' field.`)) return } @@ -101,7 +101,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { const fileExists = await fileExistsAtPath(absolutePath) if (!fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") + task.recordToolError("edit_file_anthropic") const errorMessage = `File not found: ${relPath}. Cannot perform search and replace on a non-existent file.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -115,7 +115,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { fileContent = fileContent.replace(/\r\n/g, "\n") } catch (error) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace") + task.recordToolError("edit_file_anthropic") const errorMessage = `Failed to read file '${relPath}'. Please verify file permissions and try again.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -152,7 +152,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { // If all edits failed, return error if (errors.length === edits.length) { task.consecutiveMistakeCount++ - task.recordToolError("search_and_replace", "no_match") + task.recordToolError("edit_file_anthropic", "no_match") pushToolResult(formatResponse.toolError(`All edits failed:\n${errors.join("\n")}`)) return } @@ -256,8 +256,6 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { pushToolResult(message) } - // Record successful tool usage and cleanup - task.recordToolUsage("search_and_replace") await task.diffViewProvider.reset() // Process any queued messages after file edit completes @@ -268,7 +266,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> { } } - override async handlePartial(task: Task, block: ToolUse<"search_and_replace">): Promise { + override async handlePartial(task: Task, block: ToolUse<"edit_file_anthropic">): Promise { const relPath: string | undefined = block.params.path const editsStr: string | undefined = block.params.edits @@ -308,3 +306,5 @@ function escapeRegExp(input: string): string { } export const searchAndReplaceTool = new SearchAndReplaceTool() +// Alias for new naming convention +export const editFileAnthropicTool = searchAndReplaceTool diff --git a/src/core/tools/ApplyPatchTool.ts b/src/core/tools/EditFileCodexTool.ts similarity index 95% rename from src/core/tools/ApplyPatchTool.ts rename to src/core/tools/EditFileCodexTool.ts index 000bc14729..26b5619e96 100644 --- a/src/core/tools/ApplyPatchTool.ts +++ b/src/core/tools/EditFileCodexTool.ts @@ -20,8 +20,8 @@ interface ApplyPatchParams { patch: string } -export class ApplyPatchTool extends BaseTool<"apply_patch"> { - readonly name = "apply_patch" as const +export class ApplyPatchTool extends BaseTool<"edit_file_codex"> { + readonly name = "edit_file_codex" as const parseLegacy(params: Partial>): ApplyPatchParams { return { @@ -37,8 +37,8 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { // Validate required parameters if (!patch) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") - pushToolResult(await task.sayAndCreateMissingParamError("apply_patch", "patch")) + task.recordToolError("edit_file_codex") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_codex", "patch")) return } @@ -48,7 +48,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { parsedPatch = parsePatch(patch) } catch (error) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = error instanceof ParseError ? `Invalid patch format: ${error.message}` @@ -73,7 +73,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { changes = await processAllHunks(parsedPatch.hunks, readFile) } catch (error) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `Failed to process patch: ${error instanceof Error ? error.message : String(error)}` pushToolResult(formatResponse.toolError(errorMessage)) return @@ -108,7 +108,6 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { } task.consecutiveMistakeCount = 0 - task.recordToolUsage("apply_patch") } catch (error) { await handleError("apply patch", error as Error) await task.diffViewProvider.reset() @@ -129,7 +128,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { const fileExists = await fileExistsAtPath(absolutePath) if (fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `File already exists: ${relPath}. Use Update File instead.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -220,7 +219,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { const fileExists = await fileExistsAtPath(absolutePath) if (!fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `File not found: ${relPath}. Cannot delete a non-existent file.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -278,7 +277,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { const fileExists = await fileExistsAtPath(absolutePath) if (!fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `File not found: ${relPath}. Cannot update a non-existent file.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -363,7 +362,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { const isMovePathWriteProtected = task.rooProtectedController?.isWriteProtected(change.movePath) || false if (isMovePathWriteProtected) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `Cannot move file to write-protected path: ${change.movePath}` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -375,7 +374,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { const isMoveOutsideWorkspace = isPathOutsideWorkspace(moveAbsolutePath) if (isMoveOutsideWorkspace) { task.consecutiveMistakeCount++ - task.recordToolError("apply_patch") + task.recordToolError("edit_file_codex") const errorMessage = `Cannot move file to path outside workspace: ${change.movePath}` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage)) @@ -426,7 +425,7 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { task.processQueuedMessages() } - override async handlePartial(task: Task, block: ToolUse<"apply_patch">): Promise { + override async handlePartial(task: Task, block: ToolUse<"edit_file_codex">): Promise { const patch: string | undefined = block.params.patch let patchPreview: string | undefined @@ -448,3 +447,5 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> { } export const applyPatchTool = new ApplyPatchTool() +// Alias for new naming convention +export const editFileCodexTool = applyPatchTool diff --git a/src/core/tools/EditFileTool.ts b/src/core/tools/EditFileGeminiTool.ts similarity index 99% rename from src/core/tools/EditFileTool.ts rename to src/core/tools/EditFileGeminiTool.ts index 8d04fe2301..3072c6f86e 100644 --- a/src/core/tools/EditFileTool.ts +++ b/src/core/tools/EditFileGeminiTool.ts @@ -324,8 +324,6 @@ export class EditFileTool extends BaseTool<"edit_file"> { pushToolResult(message + replacementInfo) - // Record successful tool usage and cleanup - task.recordToolUsage("edit_file") await task.diffViewProvider.reset() // Process any queued messages after file edit completes @@ -371,3 +369,5 @@ export class EditFileTool extends BaseTool<"edit_file"> { } export const editFileTool = new EditFileTool() +// Alias for new naming convention +export const editFileGeminiTool = editFileTool diff --git a/src/core/tools/SearchReplaceTool.ts b/src/core/tools/EditFileGrokTool.ts similarity index 90% rename from src/core/tools/SearchReplaceTool.ts rename to src/core/tools/EditFileGrokTool.ts index dadb97fde5..8e76160e17 100644 --- a/src/core/tools/SearchReplaceTool.ts +++ b/src/core/tools/EditFileGrokTool.ts @@ -20,8 +20,8 @@ interface SearchReplaceParams { new_string: string } -export class SearchReplaceTool extends BaseTool<"search_replace"> { - readonly name = "search_replace" as const +export class SearchReplaceTool extends BaseTool<"edit_file_grok"> { + readonly name = "edit_file_grok" as const parseLegacy(params: Partial>): SearchReplaceParams { return { @@ -39,29 +39,29 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { // Validate required parameters if (!file_path) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") - pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "file_path")) + task.recordToolError("edit_file_grok") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "file_path")) return } if (!old_string) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") - pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "old_string")) + task.recordToolError("edit_file_grok") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "old_string")) return } if (new_string === undefined) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") - pushToolResult(await task.sayAndCreateMissingParamError("search_replace", "new_string")) + task.recordToolError("edit_file_grok") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_grok", "new_string")) return } // Validate that old_string and new_string are different if (old_string === new_string) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") + task.recordToolError("edit_file_grok") pushToolResult( formatResponse.toolError( "The 'old_string' and 'new_string' parameters must be different.", @@ -95,7 +95,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { const fileExists = await fileExistsAtPath(absolutePath) if (!fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") + task.recordToolError("edit_file_grok") const errorMessage = `File not found: ${relPath}. Cannot perform search and replace on a non-existent file.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage, toolProtocol)) @@ -109,7 +109,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { fileContent = fileContent.replace(/\r\n/g, "\n") } catch (error) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace") + task.recordToolError("edit_file_grok") const errorMessage = `Failed to read file '${relPath}'. Please verify file permissions and try again.` await task.say("error", errorMessage) pushToolResult(formatResponse.toolError(errorMessage, toolProtocol)) @@ -125,7 +125,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { if (matchCount === 0) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace", "no_match") + task.recordToolError("edit_file_grok", "no_match") pushToolResult( formatResponse.toolError( `No match found for the specified 'old_string'. Please ensure it matches the file contents exactly, including whitespace and indentation.`, @@ -137,7 +137,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { if (matchCount > 1) { task.consecutiveMistakeCount++ - task.recordToolError("search_replace", "multiple_matches") + task.recordToolError("edit_file_grok", "multiple_matches") pushToolResult( formatResponse.toolError( `Found ${matchCount} matches for the specified 'old_string'. This tool can only replace ONE occurrence at a time. Please provide more context (3-5 lines before and after) to uniquely identify the specific instance you want to change.`, @@ -237,8 +237,6 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { const message = await task.diffViewProvider.pushToolWriteResult(task, task.cwd, false) pushToolResult(message) - // Record successful tool usage and cleanup - task.recordToolUsage("search_replace") await task.diffViewProvider.reset() // Process any queued messages after file edit completes @@ -249,7 +247,7 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { } } - override async handlePartial(task: Task, block: ToolUse<"search_replace">): Promise { + override async handlePartial(task: Task, block: ToolUse<"edit_file_grok">): Promise { const filePath: string | undefined = block.params.file_path const oldString: string | undefined = block.params.old_string @@ -281,3 +279,5 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> { } export const searchReplaceTool = new SearchReplaceTool() +// Alias for new naming convention +export const editFileGrokTool = searchReplaceTool diff --git a/src/core/tools/ApplyDiffTool.ts b/src/core/tools/EditFileRooTool.ts similarity index 89% rename from src/core/tools/ApplyDiffTool.ts rename to src/core/tools/EditFileRooTool.ts index 7161c7c08e..dbc9372df4 100644 --- a/src/core/tools/ApplyDiffTool.ts +++ b/src/core/tools/EditFileRooTool.ts @@ -16,22 +16,22 @@ import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats" import { BaseTool, ToolCallbacks } from "./BaseTool" import type { ToolUse } from "../../shared/tools" -interface ApplyDiffParams { +interface EditFileRooParams { path: string diff: string } -export class ApplyDiffTool extends BaseTool<"apply_diff"> { - readonly name = "apply_diff" as const +export class EditFileRooTool extends BaseTool<"edit_file_roo"> { + readonly name = "edit_file_roo" as const - parseLegacy(params: Partial>): ApplyDiffParams { + parseLegacy(params: Partial>): EditFileRooParams { return { path: params.path || "", diff: params.diff || "", } } - async execute(params: ApplyDiffParams, task: Task, callbacks: ToolCallbacks): Promise { + async execute(params: EditFileRooParams, task: Task, callbacks: ToolCallbacks): Promise { const { askApproval, handleError, pushToolResult, toolProtocol } = callbacks let { path: relPath, diff: diffContent } = params @@ -42,15 +42,15 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { try { if (!relPath) { task.consecutiveMistakeCount++ - task.recordToolError("apply_diff") - pushToolResult(await task.sayAndCreateMissingParamError("apply_diff", "path")) + task.recordToolError("edit_file_roo") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_roo", "path")) return } if (!diffContent) { task.consecutiveMistakeCount++ - task.recordToolError("apply_diff") - pushToolResult(await task.sayAndCreateMissingParamError("apply_diff", "diff")) + task.recordToolError("edit_file_roo") + pushToolResult(await task.sayAndCreateMissingParamError("edit_file_roo", "diff")) return } @@ -67,7 +67,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { if (!fileExists) { task.consecutiveMistakeCount++ - task.recordToolError("apply_diff") + task.recordToolError("edit_file_roo") const formattedError = `File does not exist at path: ${absolutePath}\n\n\nThe specified file could not be found. Please verify the file path and try again.\n` await task.say("error", formattedError) task.didToolFailInCurrentTurn = true @@ -118,7 +118,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { await task.say("diff_error", formattedError) } - task.recordToolError("apply_diff", formattedError) + task.recordToolError("edit_file_roo", formattedError) pushToolResult(formattedError) return @@ -164,9 +164,9 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { let toolProgressStatus if (task.diffStrategy && task.diffStrategy.getProgressStatus) { - const block: ToolUse<"apply_diff"> = { + const block: ToolUse<"edit_file_roo"> = { type: "tool_use", - name: "apply_diff", + name: "edit_file_roo", params: { path: relPath, diff: diffContent }, partial: false, } @@ -208,9 +208,9 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { let toolProgressStatus if (task.diffStrategy && task.diffStrategy.getProgressStatus) { - const block: ToolUse<"apply_diff"> = { + const block: ToolUse<"edit_file_roo"> = { type: "tool_use", - name: "apply_diff", + name: "edit_file_roo", params: { path: relPath, diff: diffContent }, partial: false, } @@ -272,7 +272,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { } } - override async handlePartial(task: Task, block: ToolUse<"apply_diff">): Promise { + override async handlePartial(task: Task, block: ToolUse<"edit_file_roo">): Promise { const relPath: string | undefined = block.params.path const diffContent: string | undefined = block.params.diff @@ -296,4 +296,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> { } } -export const applyDiffTool = new ApplyDiffTool() +export const editFileRooTool = new EditFileRooTool() +// Legacy alias for backward compatibility +export const applyDiffTool = editFileRooTool diff --git a/src/core/tools/MultiApplyDiffTool.ts b/src/core/tools/MultiApplyDiffTool.ts index 94cdb3fd49..695c2ca9a0 100644 --- a/src/core/tools/MultiApplyDiffTool.ts +++ b/src/core/tools/MultiApplyDiffTool.ts @@ -14,7 +14,7 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes" import { unescapeHtmlEntities } from "../../utils/text-normalization" import { parseXmlForDiff } from "../../utils/xml" import { EXPERIMENT_IDS, experiments } from "../../shared/experiments" -import { applyDiffTool as applyDiffToolClass } from "./ApplyDiffTool" +import { applyDiffTool as applyDiffToolClass } from "./EditFileRooTool" import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats" import { isNativeProtocol } from "@roo-code/types" import { resolveToolProtocol } from "../../utils/resolveToolProtocol" @@ -65,7 +65,7 @@ export async function applyDiffTool( // Use the task's locked protocol for consistency throughout the task lifetime const toolProtocol = resolveToolProtocol(cline.apiConfiguration, cline.api.getModel().info, cline.taskToolProtocol) if (isNativeProtocol(toolProtocol)) { - return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + return applyDiffToolClass.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, @@ -85,7 +85,7 @@ export async function applyDiffTool( // If experiment is disabled, use single-file class-based tool if (!isMultiFileApplyDiffEnabled) { - return applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { + return applyDiffToolClass.handle(cline, block as ToolUse<"edit_file_roo">, { askApproval, handleError, pushToolResult, diff --git a/src/core/tools/__tests__/searchReplaceTool.spec.ts b/src/core/tools/__tests__/EditFileGrokTool.spec.ts similarity index 96% rename from src/core/tools/__tests__/searchReplaceTool.spec.ts rename to src/core/tools/__tests__/EditFileGrokTool.spec.ts index 984808e971..282adf4411 100644 --- a/src/core/tools/__tests__/searchReplaceTool.spec.ts +++ b/src/core/tools/__tests__/EditFileGrokTool.spec.ts @@ -7,7 +7,7 @@ import { fileExistsAtPath } from "../../../utils/fs" import { isPathOutsideWorkspace } from "../../../utils/pathUtils" import { getReadablePath } from "../../../utils/path" import { ToolUse, ToolResponse } from "../../../shared/tools" -import { searchReplaceTool } from "../SearchReplaceTool" +import { searchReplaceTool } from "../EditFileGrokTool" vi.mock("fs/promises", () => ({ default: { @@ -179,7 +179,7 @@ describe("searchReplaceTool", () => { const toolUse: ToolUse = { type: "tool_use", - name: "search_replace", + name: "edit_file_grok", params: { file_path: testFilePath, old_string: testOldString, @@ -193,7 +193,7 @@ describe("searchReplaceTool", () => { toolResult = result }) - await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"search_replace">, { + await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"edit_file_grok">, { askApproval: mockAskApproval, handleError: mockHandleError, pushToolResult: mockPushToolResult, @@ -210,7 +210,7 @@ describe("searchReplaceTool", () => { expect(result).toBe("Missing param error") expect(mockCline.consecutiveMistakeCount).toBe(1) - expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace") + expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok") }) it("returns error when old_string is missing", async () => { @@ -268,7 +268,7 @@ describe("searchReplaceTool", () => { expect(result).toContain("Error:") expect(result).toContain("No match found") expect(mockCline.consecutiveMistakeCount).toBe(1) - expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace", "no_match") + expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok", "no_match") }) it("returns error when multiple matches are found", async () => { @@ -280,7 +280,7 @@ describe("searchReplaceTool", () => { expect(result).toContain("Error:") expect(result).toContain("3 matches") expect(mockCline.consecutiveMistakeCount).toBe(1) - expect(mockCline.recordToolError).toHaveBeenCalledWith("search_replace", "multiple_matches") + expect(mockCline.recordToolError).toHaveBeenCalledWith("edit_file_grok", "multiple_matches") }) it("successfully replaces single unique match", async () => { @@ -306,7 +306,7 @@ describe("searchReplaceTool", () => { expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled() expect(mockCline.didEditFile).toBe(true) - expect(mockCline.recordToolUsage).toHaveBeenCalledWith("search_replace") + expect(mockCline.recordToolUsage).not.toHaveBeenCalled() }) it("reverts changes when user rejects", async () => { @@ -335,7 +335,7 @@ describe("searchReplaceTool", () => { const toolUse: ToolUse = { type: "tool_use", - name: "search_replace", + name: "edit_file_grok", params: { file_path: testFilePath, old_string: testOldString, @@ -349,7 +349,7 @@ describe("searchReplaceTool", () => { capturedResult = result }) - await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"search_replace">, { + await searchReplaceTool.handle(mockCline, toolUse as ToolUse<"edit_file_grok">, { askApproval: mockAskApproval, handleError: mockHandleError, pushToolResult: localPushToolResult, diff --git a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts index 65d7cb6774..0e368c19a9 100644 --- a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts +++ b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts @@ -8,8 +8,8 @@ vi.mock("vscode", () => ({ }, })) -// Mock the ApplyDiffTool module -vi.mock("../ApplyDiffTool", () => ({ +// Mock the EditFileRooTool module +vi.mock("../EditFileRooTool", () => ({ applyDiffTool: { handle: vi.fn(), }, @@ -17,7 +17,7 @@ vi.mock("../ApplyDiffTool", () => ({ // Import after mocking to get the mocked version import { applyDiffTool as multiApplyDiffTool } from "../MultiApplyDiffTool" -import { applyDiffTool as applyDiffToolClass } from "../ApplyDiffTool" +import { applyDiffTool as applyDiffToolClass } from "../EditFileRooTool" describe("applyDiffTool experiment routing", () => { let mockCline: any diff --git a/src/core/tools/__tests__/editFileTool.spec.ts b/src/core/tools/__tests__/editFileTool.spec.ts index ab632252df..59bff54f0f 100644 --- a/src/core/tools/__tests__/editFileTool.spec.ts +++ b/src/core/tools/__tests__/editFileTool.spec.ts @@ -7,7 +7,7 @@ import { fileExistsAtPath } from "../../../utils/fs" import { isPathOutsideWorkspace } from "../../../utils/pathUtils" import { getReadablePath } from "../../../utils/path" import { ToolUse, ToolResponse } from "../../../shared/tools" -import { editFileTool } from "../EditFileTool" +import { editFileTool } from "../EditFileGeminiTool" vi.mock("fs/promises", () => ({ default: { @@ -348,7 +348,7 @@ describe("editFileTool", () => { expect(mockTask.diffViewProvider.saveChanges).toHaveBeenCalled() expect(mockTask.didEditFile).toBe(true) - expect(mockTask.recordToolUsage).toHaveBeenCalledWith("edit_file") + expect(mockTask.recordToolUsage).not.toHaveBeenCalled() }) it("reverts changes when user rejects", async () => { diff --git a/src/shared/__tests__/modes.spec.ts b/src/shared/__tests__/modes.spec.ts index a00abde787..7593e44565 100644 --- a/src/shared/__tests__/modes.spec.ts +++ b/src/shared/__tests__/modes.spec.ts @@ -326,7 +326,7 @@ describe("isToolAllowedForMode", () => { it("disallows customTools by default (not in includedTools)", () => { // search_and_replace is a customTool in the edit group, should be disallowed by default - expect(isToolAllowedForMode("search_and_replace", "test-custom-tools", customModesWithEditGroup)).toBe( + expect(isToolAllowedForMode("edit_file_anthropic", "test-custom-tools", customModesWithEditGroup)).toBe( false, ) }) @@ -335,13 +335,13 @@ describe("isToolAllowedForMode", () => { // search_and_replace should be allowed when explicitly included expect( isToolAllowedForMode( - "search_and_replace", + "edit_file_anthropic", "test-custom-tools", customModesWithEditGroup, undefined, undefined, undefined, - ["search_and_replace"], + ["edit_file_anthropic"], ), ).toBe(true) }) @@ -359,13 +359,13 @@ describe("isToolAllowedForMode", () => { // Even if included, should be disallowed because the mode doesn't have edit group expect( isToolAllowedForMode( - "search_and_replace", + "edit_file_anthropic", "no-edit-mode", customModesWithoutEdit, undefined, undefined, undefined, - ["search_and_replace"], + ["edit_file_anthropic"], ), ).toBe(false) }) diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 36ea1b0fcc..2ed86d18cd 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -70,14 +70,14 @@ export const toolParamNames = [ "prompt", "image", "files", // Native protocol parameter for read_file - "edits", // edit_file_anthropic parameter for multiple edit operations - "old_text", // edit_file_anthropic parameter for text to replace - "new_text", // edit_file_anthropic parameter for replacement text - "patch", // apply_patch parameter - "file_path", // search_replace and edit_file parameter - "old_string", // search_replace and edit_file parameter - "new_string", // search_replace and edit_file parameter - "expected_replacements", // edit_file parameter for multiple occurrences + "edits", // edit_file (anthropic variant) parameter for multiple edit operations + "old_text", // edit_file (anthropic variant) parameter for text to replace + "new_text", // edit_file (anthropic variant) parameter for replacement text + "patch", // edit_file (codex variant) parameter + "file_path", // edit_file (grok/gemini variant) parameter + "old_string", // edit_file (grok/gemini variant) parameter + "new_string", // edit_file (grok/gemini variant) parameter + "expected_replacements", // edit_file (gemini variant) parameter for multiple occurrences ] as const export type ToolParamName = (typeof toolParamNames)[number] @@ -93,13 +93,22 @@ export type NativeToolArgs = { read_file: { files: FileEntry[] } attempt_completion: { result: string } execute_command: { command: string; cwd?: string } - // Legacy edit tool names (deprecated, mapped to new names via aliases) + // apply_diff is kept for XML protocol backward compatibility apply_diff: { path: string; diff: string } - search_and_replace: { path: string; edits: Array<{ old_text: string; new_text: string }> } - search_replace: { file_path: string; old_string: string; new_string: string } - edit_file: { file_path: string; old_string: string; new_string: string; expected_replacements?: number } - apply_patch: { patch: string } - // New edit tool variant names (all present "edit_file" to LLM) + // Unified edit_file for native protocol - variant-specific args based on modelInfo.editToolVariant + // The actual arg structure depends on which variant is selected: + // - roo: { path, diff } + // - anthropic: { path, edits } + // - grok: { file_path, old_string, new_string } + // - gemini: { file_path, old_string, new_string, expected_replacements? } + // - codex: { patch } + edit_file: + | { path: string; diff: string } + | { path: string; edits: Array<{ old_text: string; new_text: string }> } + | { file_path: string; old_string: string; new_string: string } + | { file_path: string; old_string: string; new_string: string; expected_replacements?: number } + | { patch: string } + // Internal edit tool variant names (used by native protocol, presented to LLM as "edit_file") edit_file_roo: { path: string; diff: string } edit_file_anthropic: { path: string; edits: Array<{ old_text: string; new_text: string }> } edit_file_grok: { file_path: string; old_string: string; new_string: string } @@ -257,13 +266,11 @@ export const TOOL_DISPLAY_NAMES: Record = { read_file: "read files", fetch_instructions: "fetch instructions", write_to_file: "write files", - // Legacy edit tool names (deprecated) + // apply_diff is kept for XML protocol backward compatibility apply_diff: "apply changes", - search_and_replace: "apply changes using search and replace", - search_replace: "apply single search and replace", - edit_file: "edit files using search and replace", - apply_patch: "apply patches using codex format", - // New edit tool variant names + // Unified edit tool for native protocol + edit_file: "edit files", + // Internal edit tool variant names (used by native protocol) edit_file_roo: "edit files (roo format)", edit_file_anthropic: "edit files (anthropic format)", edit_file_grok: "edit files (grok format)", @@ -292,25 +299,12 @@ export const TOOL_GROUPS: Record = { tools: ["read_file", "fetch_instructions", "search_files", "list_files", "codebase_search"], }, edit: { - // apply_diff is included for XML protocol backward compatibility - // For native protocol, filterNativeToolsForMode selects the appropriate edit_file_* variant + // For XML protocol: apply_diff is included for backward compatibility + // For native protocol: filterNativeToolsForMode selects the appropriate edit_file_* variant + // and presents it as "edit_file" to the LLM tools: ["write_to_file", "apply_diff", "edit_file", "generate_image"], - // All edit tool variants and legacy names - one is selected based on modelInfo.editToolVariant - // "edit_file" is the unified name that LLMs see (for modelInfo.includedTools validation) - customTools: [ - // Unified edit tool name (for includedTools validation) - "edit_file", - // Legacy names (for backward compatibility with existing includedTools configs) - "search_and_replace", - "search_replace", - "apply_patch", - // Variant names (for native protocol internal use) - "edit_file_roo", - "edit_file_anthropic", - "edit_file_grok", - "edit_file_gemini", - "edit_file_codex", - ], + // Variant names for native protocol internal use - one is selected based on modelInfo.editToolVariant + customTools: ["edit_file_roo", "edit_file_anthropic", "edit_file_grok", "edit_file_gemini", "edit_file_codex"], }, browser: { tools: ["browser_action"], @@ -341,20 +335,16 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ * Central registry of tool aliases. * Maps alias name -> canonical tool name. * - * This allows models to use alternative names for tools (e.g., "edit_file" instead of "apply_diff"). + * This allows models to use alternative names for tools. * When a model calls a tool by its alias, the system resolves it to the canonical name for execution, * but preserves the alias in API conversation history for consistency. * - * To add a new alias, simply add an entry here. No other files need to be modified. + * Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace, apply_patch) + * have been removed. Native protocol now uses editToolVariant to select the edit tool schema, + * and all edit tools are presented to the LLM as "edit_file". XML protocol still uses apply_diff directly. */ export const TOOL_ALIASES: Record = { write_file: "write_to_file", - // Backward compatibility: map old edit tool names to new variant names - apply_diff: "edit_file_roo", - search_and_replace: "edit_file_anthropic", - search_replace: "edit_file_grok", - // Note: edit_file is kept as a tool name (for gemini variant) but also serves as the unified LLM-facing name - apply_patch: "edit_file_codex", } as const export type DiffResult =