refactor: rename native edit tools with provider-specific suffixes

- Rename apply_diff.ts -> edit_file_roo.ts
- Rename apply_patch.ts -> edit_file_codex.ts
- Rename edit_file.ts -> edit_file_gemini.ts
- Rename search_replace.ts -> edit_file_grok.ts
- Add edit_file_anthropic.ts
- Remove search_and_replace.ts
- Update related imports and tool filtering logic
This commit is contained in:
Hannes Rudolph 2025-12-21 14:41:33 -07:00
parent 861139ca24
commit 0126cb0b15
19 changed files with 519 additions and 258 deletions

View file

@ -64,6 +64,8 @@ export type ModelParameter = z.infer<typeof modelParametersSchema>
export const isModelParameter = (value: string): value is ModelParameter =>
modelParameters.includes(value as ModelParameter)
import { editToolVariantSchema } from "./tool.js"
/**
* ModelInfo
*/
@ -120,6 +122,9 @@ export const modelInfoSchema = z.object({
// These tools will be added if they belong to an allowed group in the current mode
// Cannot force-add tools from groups the mode doesn't allow
includedTools: z.array(z.string()).optional(),
// Edit tool variant - determines which edit tool schema is presented to the LLM
// Each variant has a schema optimized for different LLM families (defaults to "roo")
editToolVariant: editToolVariantSchema.optional(),
/**
* Service tiers with pricing information.
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.

View file

@ -10,6 +10,18 @@ export const toolGroupsSchema = z.enum(toolGroups)
export type ToolGroup = z.infer<typeof toolGroupsSchema>
/**
* EditToolVariant
*
* Determines which edit tool schema is presented to the LLM.
* Each variant has a schema optimized for different LLM families.
*/
export const editToolVariants = ["roo", "anthropic", "grok", "gemini", "codex"] as const
export const editToolVariantSchema = z.enum(editToolVariants)
export type EditToolVariant = z.infer<typeof editToolVariantSchema>
/**
* ToolName
*/
@ -18,11 +30,19 @@ export const toolNames = [
"execute_command",
"read_file",
"write_to_file",
// Legacy edit tool names (deprecated, use edit_file_* variants)
"apply_diff",
"search_and_replace",
"search_replace",
"edit_file",
"apply_patch",
// New edit tool variant names
"edit_file_roo",
"edit_file_anthropic",
"edit_file_grok",
"edit_file_gemini",
"edit_file_codex",
// Other tools
"search_files",
"list_files",
"browser_action",

View file

@ -520,10 +520,10 @@ export class NativeToolCallParser {
break
case "search_and_replace":
if (partialArgs.path !== undefined || partialArgs.operations !== undefined) {
if (partialArgs.path !== undefined || partialArgs.edits !== undefined) {
nativeArgs = {
path: partialArgs.path,
operations: partialArgs.operations,
edits: partialArgs.edits,
}
}
break
@ -661,10 +661,10 @@ export class NativeToolCallParser {
break
case "search_and_replace":
if (args.path !== undefined && args.operations !== undefined && Array.isArray(args.operations)) {
if (args.path !== undefined && args.edits !== undefined && Array.isArray(args.edits)) {
nativeArgs = {
path: args.path,
operations: args.operations,
edits: args.edits,
} as NativeArgsFor<TName>
}
break

View file

@ -2,7 +2,7 @@ import cloneDeep from "clone-deep"
import { serializeError } from "serialize-error"
import { Anthropic } from "@anthropic-ai/sdk"
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import type { ToolName, ClineAsk, ToolProgressStatus, EditToolVariant } from "@roo-code/types"
import { ConsecutiveMistakeError } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { customToolRegistry } from "@roo-code/core"
@ -399,8 +399,19 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name} for '${block.params.path}']`
case "search_replace":
return `[${block.name} for '${block.params.file_path}']`
case "edit_file":
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
const filePath = block.params.file_path || block.params.path
if (filePath) {
return `[${block.name} for '${filePath}']`
}
// Codex variant uses patch parameter
if (block.params.patch) {
return `[${block.name}]`
}
return `[${block.name}]`
}
case "apply_patch":
return `[${block.name}]`
case "list_files":
@ -884,16 +895,77 @@ export async function presentAssistantMessage(cline: Task) {
toolProtocol,
})
break
case "edit_file":
case "edit_file": {
// Unified edit_file tool - route to correct handler based on editToolVariant
await checkpointSaveAndMark(cline)
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
const modelInfo = cline.api.getModel()
const editToolVariant: EditToolVariant = modelInfo?.info?.editToolVariant ?? "roo"
switch (editToolVariant) {
case "roo":
// Route to apply_diff handler (Roo variant)
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "anthropic":
// Route to search_and_replace handler (Anthropic variant)
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "grok":
// Route to search_replace handler (Grok variant)
await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "gemini":
// Route to edit_file handler (Gemini variant)
await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
case "codex":
// Route to apply_patch handler (Codex variant)
await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
break
default: {
// Should never happen, but default to roo variant
const _exhaustiveCheck: never = editToolVariant
await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
askApproval,
handleError,
pushToolResult,
removeClosingTag,
toolProtocol,
})
}
}
break
}
case "apply_patch":
await checkpointSaveAndMark(cline)
await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {

View file

@ -149,6 +149,48 @@ Example: Requesting to list all files in the current directory
<recursive>false</recursive>
</list_files>
## write_to_file
Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
Parameters:
- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
Usage:
<write_to_file>
<path>File path here</path>
<content>
Your file content here
</content>
</write_to_file>
Example: Writing a configuration file
<write_to_file>
<path>frontend-config.json</path>
<content>
{
"apiEndpoint": "https://api.example.com",
"theme": {
"primaryColor": "#007bff",
"secondaryColor": "#6c757d",
"fontFamily": "Arial, sans-serif"
},
"features": {
"darkMode": true,
"notifications": true,
"analytics": false
},
"version": "1.0.0"
}
</content>
</write_to_file>
## apply_diff
Description: Request to apply PRECISE, TARGETED modifications to an existing file by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code.
You can perform multiple distinct search and replace operations within a single `apply_diff` call by providing multiple SEARCH/REPLACE blocks in the `diff` parameter. This is the preferred way to make several targeted changes efficiently.
@ -237,48 +279,6 @@ Only use a single line of '=======' between search and replacement content, beca
</diff>
</apply_diff>
## write_to_file
Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
Parameters:
- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
Usage:
<write_to_file>
<path>File path here</path>
<content>
Your file content here
</content>
</write_to_file>
Example: Writing a configuration file
<write_to_file>
<path>frontend-config.json</path>
<content>
{
"apiEndpoint": "https://api.example.com",
"theme": {
"primaryColor": "#007bff",
"secondaryColor": "#6c757d",
"fontFamily": "Arial, sans-serif"
},
"features": {
"darkMode": true,
"notifications": true,
"analytics": false
},
"version": "1.0.0"
}
</content>
</write_to_file>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.

View file

@ -4,7 +4,14 @@ import type { ModeConfig, ModelInfo } from "@roo-code/types"
import { filterNativeToolsForMode, filterMcpToolsForMode, applyModelToolCustomization } from "../filter-tools-for-mode"
import * as toolsModule from "../../../../shared/tools"
// NOTE: Edit tools are now unified under the "edit_file" name. The input tools
// use variant names (edit_file_roo, edit_file_anthropic, etc.) and the filter
// function renames the selected variant to "edit_file" in the output.
// Legacy tool names (apply_diff, search_and_replace, etc.) are filtered out
// and replaced with the unified "edit_file" tool.
describe("filterNativeToolsForMode", () => {
// Use edit_file_roo as the default edit tool variant in mock tools
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
@ -25,8 +32,8 @@ describe("filterNativeToolsForMode", () => {
{
type: "function",
function: {
name: "apply_diff",
description: "Apply diff",
name: "edit_file_roo",
description: "Edit file (Roo variant)",
parameters: {},
},
},
@ -87,9 +94,10 @@ describe("filterNativeToolsForMode", () => {
// Should include read tools
expect(toolNames).toContain("read_file")
// Should NOT include edit tools
// Should NOT include edit tools (no edit group)
expect(toolNames).not.toContain("write_to_file")
expect(toolNames).not.toContain("apply_diff")
expect(toolNames).not.toContain("edit_file")
expect(toolNames).not.toContain("edit_file_roo")
// Should NOT include command tools
expect(toolNames).not.toContain("execute_command")
@ -115,9 +123,11 @@ describe("filterNativeToolsForMode", () => {
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
// Should include all tools (code mode has all groups)
// Note: edit_file_roo gets renamed to edit_file
expect(toolNames).toContain("read_file")
expect(toolNames).toContain("write_to_file")
expect(toolNames).toContain("apply_diff")
expect(toolNames).toContain("edit_file") // Unified edit tool
expect(toolNames).not.toContain("edit_file_roo") // Variant name should be renamed
expect(toolNames).toContain("execute_command")
expect(toolNames).toContain("browser_action")
expect(toolNames).toContain("ask_followup_question")
@ -485,36 +495,37 @@ describe("filterMcpToolsForMode", () => {
}
it("should return original tools when modelInfo is undefined", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
const tools = new Set(["read_file", "write_to_file", "edit_file"])
const result = applyModelToolCustomization(tools, codeMode, undefined)
expect(result.allowedTools).toEqual(tools)
})
it("should exclude tools specified in excludedTools", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
// Note: edit_file is the unified edit tool name
const tools = new Set(["read_file", "write_to_file", "edit_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
excludedTools: ["edit_file"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
expect(result.allowedTools.has("write_to_file")).toBe(true)
expect(result.allowedTools.has("apply_diff")).toBe(false)
expect(result.allowedTools.has("edit_file")).toBe(false)
})
it("should exclude multiple tools", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff", "execute_command"])
const tools = new Set(["read_file", "write_to_file", "edit_file", "execute_command"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff", "write_to_file"],
excludedTools: ["edit_file", "write_to_file"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
expect(result.allowedTools.has("execute_command")).toBe(true)
expect(result.allowedTools.has("write_to_file")).toBe(false)
expect(result.allowedTools.has("apply_diff")).toBe(false)
expect(result.allowedTools.has("edit_file")).toBe(false)
})
it("should include tools only if they belong to allowed groups", () => {
@ -522,12 +533,12 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Both in edit group
includedTools: ["write_to_file", "edit_file"], // Both in edit group
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
expect(result.allowedTools.has("write_to_file")).toBe(true)
expect(result.allowedTools.has("apply_diff")).toBe(true)
expect(result.allowedTools.has("edit_file")).toBe(true)
})
it("should NOT include tools from groups not allowed by mode", () => {
@ -535,28 +546,29 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
includedTools: ["write_to_file", "edit_file"], // Edit group tools
}
// Architect mode doesn't have edit group
const result = applyModelToolCustomization(tools, architectMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
expect(result.allowedTools.has("write_to_file")).toBe(false) // Not in allowed groups
expect(result.allowedTools.has("apply_diff")).toBe(false) // Not in allowed groups
expect(result.allowedTools.has("edit_file")).toBe(false) // Not in allowed groups
})
it("should apply both exclude and include operations", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
// Note: edit_file is the unified edit tool.
const tools = new Set(["read_file", "write_to_file", "execute_command"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
includedTools: ["search_and_replace"], // Another edit tool (customTool)
excludedTools: ["execute_command"],
includedTools: ["edit_file"], // Unified edit tool
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
expect(result.allowedTools.has("write_to_file")).toBe(true)
expect(result.allowedTools.has("apply_diff")).toBe(false) // Excluded
expect(result.allowedTools.has("search_and_replace")).toBe(true) // Included
expect(result.allowedTools.has("execute_command")).toBe(false) // Excluded
expect(result.allowedTools.has("edit_file")).toBe(true) // Included
})
it("should handle empty excludedTools and includedTools arrays", () => {
@ -576,7 +588,7 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff", "nonexistent_tool"],
excludedTools: ["edit_file", "nonexistent_tool"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.allowedTools.has("read_file")).toBe(true)
@ -701,8 +713,8 @@ describe("filterMcpToolsForMode", () => {
{
type: "function",
function: {
name: "apply_diff",
description: "Apply diff",
name: "edit_file_roo",
description: "Edit file (Roo variant)",
parameters: {},
},
},
@ -714,22 +726,6 @@ describe("filterMcpToolsForMode", () => {
parameters: {},
},
},
{
type: "function",
function: {
name: "search_and_replace",
description: "Search and replace",
parameters: {},
},
},
{
type: "function",
function: {
name: "edit_file",
description: "Edit file",
parameters: {},
},
},
]
it("should exclude tools when model specifies excludedTools", () => {
@ -743,7 +739,7 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
excludedTools: ["edit_file"],
}
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
@ -754,7 +750,7 @@ describe("filterMcpToolsForMode", () => {
expect(toolNames).toContain("read_file")
expect(toolNames).toContain("write_to_file")
expect(toolNames).not.toContain("apply_diff") // Excluded by model
expect(toolNames).not.toContain("edit_file") // Excluded by model
})
it("should include tools when model specifies includedTools from allowed groups", () => {
@ -768,7 +764,7 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["search_and_replace"], // Edit group customTool
includedTools: ["edit_file"], // Unified edit tool
}
const filtered = filterNativeToolsForMode(mockNativeTools, "limited", [modeWithOnlyRead], {}, undefined, {
@ -777,7 +773,7 @@ describe("filterMcpToolsForMode", () => {
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).toContain("search_and_replace") // Included by model
expect(toolNames).toContain("edit_file") // Included by model (renamed from edit_file_roo)
})
it("should NOT include tools from groups not allowed by mode", () => {
@ -791,7 +787,7 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
includedTools: ["write_to_file", "edit_file"], // Edit group tools
}
const filtered = filterNativeToolsForMode(mockNativeTools, "architect", [architectMode], {}, undefined, {
@ -802,7 +798,7 @@ describe("filterMcpToolsForMode", () => {
expect(toolNames).toContain("read_file")
expect(toolNames).not.toContain("write_to_file") // Not in mode's allowed groups
expect(toolNames).not.toContain("apply_diff") // Not in mode's allowed groups
expect(toolNames).not.toContain("edit_file") // Not in mode's allowed groups
})
it("should combine excludedTools and includedTools", () => {
@ -816,8 +812,8 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
includedTools: ["search_and_replace"],
excludedTools: ["execute_command"],
includedTools: ["edit_file"],
}
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
@ -827,8 +823,8 @@ describe("filterMcpToolsForMode", () => {
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).toContain("write_to_file")
expect(toolNames).toContain("search_and_replace") // Included
expect(toolNames).not.toContain("apply_diff") // Excluded
expect(toolNames).toContain("edit_file") // Included
expect(toolNames).not.toContain("execute_command") // Excluded
})
it("should honor included aliases while respecting exclusions", () => {
@ -842,7 +838,7 @@ describe("filterMcpToolsForMode", () => {
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
excludedTools: ["execute_command"],
includedTools: ["edit_file", "write_file"],
}
@ -853,9 +849,8 @@ describe("filterMcpToolsForMode", () => {
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
expect(toolNames).toContain("edit_file")
expect(toolNames).toContain("write_file")
expect(toolNames).not.toContain("apply_diff")
expect(toolNames).not.toContain("write_to_file")
expect(toolNames).toContain("write_file") // Alias for write_to_file
expect(toolNames).not.toContain("execute_command")
})
})
})

View file

@ -1,5 +1,5 @@
import type OpenAI from "openai"
import type { ModeConfig, ToolName, ToolGroup, ModelInfo } from "@roo-code/types"
import type { ModeConfig, ToolName, ToolGroup, ModelInfo, EditToolVariant } from "@roo-code/types"
import { getModeBySlug, getToolsForMode } from "../../../shared/modes"
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../../../shared/tools"
import { defaultModeSlug } from "../../../shared/modes"
@ -7,6 +7,30 @@ import type { CodeIndexManager } from "../../../services/code-index/manager"
import type { McpHub } from "../../../services/mcp/McpHub"
import { isToolAllowedForMode } from "../../../core/tools/validateToolUse"
/**
* Mapping from edit tool variant to internal tool name.
* These are the tools that will be selected based on modelInfo.editToolVariant.
*/
const EDIT_TOOL_VARIANT_MAP: Record<EditToolVariant, string> = {
roo: "edit_file_roo",
anthropic: "edit_file_anthropic",
grok: "edit_file_grok",
gemini: "edit_file_gemini",
codex: "edit_file_codex",
}
/**
* All edit tool variant names that should be filtered.
* Only one of these (based on editToolVariant) will be included and renamed to "edit_file".
*/
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.
@ -296,16 +320,40 @@ export function filterNativeToolsForMode(
allowedToolNames.delete("browser_action")
}
// Conditionally exclude apply_diff if diffs are disabled
if (settings?.diffEnabled === false) {
allowedToolNames.delete("apply_diff")
}
// Conditionally exclude access_mcp_resource if MCP is not enabled or there are no resources
if (!mcpHub || !hasAnyMcpResources(mcpHub)) {
allowedToolNames.delete("access_mcp_resource")
}
// 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)
}
// Determine which edit tool variant to use (default: "roo")
const editToolVariant: EditToolVariant = modelInfo?.editToolVariant ?? "roo"
const selectedEditToolName = EDIT_TOOL_VARIANT_MAP[editToolVariant]
// Check if diffs are disabled - if so, skip edit tool entirely
const diffEnabled = settings?.diffEnabled !== false
// Check if mode has "edit" group (required for edit tools)
const allowedGroups = new Set(
modeConfig.groups.map((groupEntry) => (Array.isArray(groupEntry) ? groupEntry[0] : groupEntry)),
)
const modeHasEditGroup = allowedGroups.has("edit")
// Check if edit_file is excluded by model config
const isEditFileExcluded = modelInfo?.excludedTools?.some(
(tool) => resolveToolAlias(tool) === "edit_file" || tool === "edit_file",
)
// Filter native tools based on allowed tool names and apply alias renames
const filteredTools: OpenAI.Chat.ChatCompletionTool[] = []
@ -313,6 +361,22 @@ export function filterNativeToolsForMode(
// Handle both ChatCompletionTool and ChatCompletionCustomTool
if ("function" in tool && tool.function) {
const toolName = tool.function.name
// Special handling for edit tool variants
if (ALL_EDIT_TOOL_VARIANTS.has(toolName)) {
// Only include if:
// 1. This is the selected variant
// 2. Diffs are enabled
// 3. Mode has "edit" group
// 4. edit_file is not excluded by model config
if (toolName === selectedEditToolName && diffEnabled && modeHasEditGroup && !isEditFileExcluded) {
// Rename the selected variant to "edit_file" so LLM always sees that name
filteredTools.push(getOrCreateRenamedTool(tool, "edit_file"))
}
continue
}
// Regular tool processing
if (allowedToolNames.has(toolName)) {
// Check if this tool should be renamed to an alias
const aliasName = aliasRenames.get(toolName)

View file

@ -0,0 +1,46 @@
import type OpenAI from "openai"
const EDIT_FILE_ANTHROPIC_DESCRIPTION = `Edit an existing file by applying one or more exact string replacements.`
const edit_file_anthropic = {
type: "function",
function: {
name: "edit_file_anthropic",
description: EDIT_FILE_ANTHROPIC_DESCRIPTION,
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the file to edit",
},
edits: {
type: "array",
description: "List of edits to apply sequentially",
items: {
type: "object",
properties: {
old_text: {
type: "string",
description: "Exact text to be replaced",
},
new_text: {
type: "string",
description: "Replacement text",
},
},
required: ["old_text", "new_text"],
},
minItems: 1,
},
},
required: ["path", "edits"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
export default edit_file_anthropic
// Backward compatibility export
export const search_and_replace = edit_file_anthropic

View file

@ -1,6 +1,6 @@
import type OpenAI from "openai"
const apply_patch_DESCRIPTION = `Apply patches to files using a stripped-down, file-oriented diff format. This tool supports creating new files, deleting files, and updating existing files with precise changes.
const EDIT_FILE_CODEX_DESCRIPTION = `Apply patches to files using a stripped-down, file-oriented diff format. This tool supports creating new files, deleting files, and updating existing files with precise changes.
The patch format uses a simple, human-readable structure:
@ -38,11 +38,11 @@ Example patch:
*** Delete File: obsolete.txt
*** End Patch`
const apply_patch = {
const edit_file_codex = {
type: "function",
function: {
name: "apply_patch",
description: apply_patch_DESCRIPTION,
name: "edit_file_codex",
description: EDIT_FILE_CODEX_DESCRIPTION,
parameters: {
type: "object",
properties: {
@ -58,4 +58,7 @@ const apply_patch = {
},
} satisfies OpenAI.Chat.ChatCompletionTool
export default apply_patch
export default edit_file_codex
// Backward compatibility export
export const apply_patch = edit_file_codex

View file

@ -1,6 +1,6 @@
import type OpenAI from "openai"
const EDIT_FILE_DESCRIPTION = `Use this tool to replace text in an existing file, or create a new file.
const EDIT_FILE_GEMINI_DESCRIPTION = `Use this tool to replace text in an existing file, or create a new file.
This tool performs literal string replacement with support for multiple occurrences.
@ -31,11 +31,11 @@ CRITICAL REQUIREMENTS:
4. NO ESCAPING: Provide the literal text - do not escape special characters.`
const edit_file = {
const edit_file_gemini = {
type: "function",
function: {
name: "edit_file",
description: EDIT_FILE_DESCRIPTION,
name: "edit_file_gemini",
description: EDIT_FILE_GEMINI_DESCRIPTION,
parameters: {
type: "object",
properties: {
@ -67,4 +67,7 @@ const edit_file = {
},
} satisfies OpenAI.Chat.ChatCompletionTool
export default edit_file
export default edit_file_gemini
// Backward compatibility export (original name was edit_file)
export { edit_file_gemini as edit_file }

View file

@ -1,6 +1,6 @@
import type OpenAI from "openai"
const SEARCH_REPLACE_DESCRIPTION = `Use this tool to propose a search and replace operation on an existing file.
const EDIT_FILE_GROK_DESCRIPTION = `Use this tool to propose a search and replace operation on an existing file.
The tool will replace ONE occurrence of old_string with new_string in the specified file.
@ -19,11 +19,11 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
- If multiple instances exist, gather enough context to uniquely identify each one
- Plan separate tool calls for each instance`
const search_replace = {
const edit_file_grok = {
type: "function",
function: {
name: "search_replace",
description: SEARCH_REPLACE_DESCRIPTION,
name: "edit_file_grok",
description: EDIT_FILE_GROK_DESCRIPTION,
parameters: {
type: "object",
properties: {
@ -48,4 +48,7 @@ const search_replace = {
},
} satisfies OpenAI.Chat.ChatCompletionTool
export default search_replace
export default edit_file_grok
// Backward compatibility export
export const search_replace = edit_file_grok

View file

@ -1,6 +1,6 @@
import type OpenAI from "openai"
const APPLY_DIFF_DESCRIPTION = `Apply precise, targeted modifications to an existing file using one or more search/replace blocks. This tool is for surgical edits only; the 'SEARCH' block must exactly match the existing content, including whitespace and indentation. To make multiple targeted changes, provide multiple SEARCH/REPLACE blocks in the 'diff' parameter. Use the 'read_file' tool first if you are not confident in the exact content to search for.`
const EDIT_FILE_ROO_DESCRIPTION = `Apply precise, targeted modifications to an existing file using one or more search/replace blocks. This tool is for surgical edits only; the 'SEARCH' block must exactly match the existing content, including whitespace and indentation. To make multiple targeted changes, provide multiple SEARCH/REPLACE blocks in the 'diff' parameter. Use the 'read_file' tool first if you are not confident in the exact content to search for.`
const DIFF_PARAMETER_DESCRIPTION = `A string containing one or more search/replace blocks defining the changes. The ':start_line:' is required and indicates the starting line number of the original content. You must not add a start line for the replacement content. Each block must follow this format:
<<<<<<< SEARCH
@ -11,11 +11,11 @@ const DIFF_PARAMETER_DESCRIPTION = `A string containing one or more search/repla
[new content to replace with]
>>>>>>> REPLACE`
export const apply_diff = {
export const edit_file_roo = {
type: "function",
function: {
name: "apply_diff",
description: APPLY_DIFF_DESCRIPTION,
name: "edit_file_roo",
description: EDIT_FILE_ROO_DESCRIPTION,
parameters: {
type: "object",
properties: {
@ -33,3 +33,6 @@ export const apply_diff = {
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
// Backward compatibility export
export const apply_diff = edit_file_roo

View file

@ -1,7 +1,7 @@
import type OpenAI from "openai"
import accessMcpResource from "./access_mcp_resource"
import { apply_diff } from "./apply_diff"
import applyPatch from "./apply_patch"
import { edit_file_roo, apply_diff } from "./edit_file_roo"
import edit_file_codex, { apply_patch } from "./edit_file_codex"
import askFollowupQuestion from "./ask_followup_question"
import attemptCompletion from "./attempt_completion"
import browserAction from "./browser_action"
@ -13,9 +13,9 @@ import listFiles from "./list_files"
import newTask from "./new_task"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import searchAndReplace from "./search_and_replace"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
import edit_file_anthropic, { search_and_replace } from "./edit_file_anthropic"
import edit_file_grok, { search_replace } from "./edit_file_grok"
import edit_file_gemini, { edit_file } from "./edit_file_gemini"
import searchFiles from "./search_files"
import switchMode from "./switch_mode"
import updateTodoList from "./update_todo_list"
@ -38,10 +38,37 @@ export interface NativeToolsOptions {
}
/**
* Get native tools array, optionally customizing based on settings.
* Edit tool variant types - determines which edit tool schema is presented to the LLM
*/
export type EditToolVariant = "roo" | "anthropic" | "grok" | "gemini" | "codex"
/**
* All edit tool definitions mapped by variant
*/
export const EDIT_TOOL_VARIANTS: Record<EditToolVariant, OpenAI.Chat.ChatCompletionTool> = {
roo: edit_file_roo,
anthropic: edit_file_anthropic,
grok: edit_file_grok,
gemini: edit_file_gemini,
codex: edit_file_codex,
}
/**
* Get the edit tool definition for a specific variant
* @param variant The edit tool variant to use
* @returns The tool definition for that variant
*/
export function getEditToolForVariant(variant: EditToolVariant): OpenAI.Chat.ChatCompletionTool {
return EDIT_TOOL_VARIANTS[variant]
}
/**
* Get native tools array, including all edit tool variants.
* The filterNativeToolsForMode function will select the appropriate variant
* based on modelInfo.editToolVariant and rename it to "edit_file".
*
* @param options - Configuration options for the tools
* @returns Array of native tool definitions
* @returns Array of native tool definitions (including all edit tool variants)
*/
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
const { partialReadsEnabled = true, maxConcurrentFileReads = 5, supportsImages = false } = options
@ -54,8 +81,12 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
return [
accessMcpResource,
apply_diff,
applyPatch,
// All edit tool variants - filterNativeToolsForMode will select one and rename to "edit_file"
edit_file_roo,
edit_file_anthropic,
edit_file_grok,
edit_file_gemini,
edit_file_codex,
askFollowupQuestion,
attemptCompletion,
browserAction,
@ -67,9 +98,6 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
newTask,
createReadFileTool(readFileOptions),
runSlashCommand,
searchAndReplace,
searchReplace,
edit_file,
searchFiles,
switchMode,
updateTodoList,
@ -78,4 +106,21 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
}
// Backward compatibility: export default tools with line ranges enabled
// Note: filterNativeToolsForMode will select the edit tool variant based on modelInfo
export const nativeTools = getNativeTools()
// Re-export individual tools for backward compatibility
export {
// New names
edit_file_roo,
edit_file_anthropic,
edit_file_grok,
edit_file_gemini,
edit_file_codex,
// Old names (aliases)
apply_diff,
search_and_replace,
search_replace,
edit_file,
apply_patch,
}

View file

@ -1,44 +0,0 @@
import type OpenAI from "openai"
const SEARCH_AND_REPLACE_DESCRIPTION = `Apply precise, targeted modifications to an existing file using search and replace operations. This tool is for surgical edits only; provide an array of operations where each operation specifies the exact text to search for and what to replace it with. The search text must exactly match the existing content, including whitespace and indentation.`
const search_and_replace = {
type: "function",
function: {
name: "search_and_replace",
description: SEARCH_AND_REPLACE_DESCRIPTION,
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "The path of the file to modify, relative to the current workspace directory.",
},
operations: {
type: "array",
description: "Array of search and replace operations to perform on the file.",
items: {
type: "object",
properties: {
search: {
type: "string",
description:
"The exact text to find in the file. Must match exactly, including whitespace.",
},
replace: {
type: "string",
description: "The text to replace the search text with.",
},
},
required: ["search", "replace"],
},
minItems: 1,
},
},
required: ["path", "operations"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool
export default search_and_replace

View file

@ -41,8 +41,9 @@ describe("Native Tools Filtering by Mode", () => {
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => architectAllowedTools.add(tool))
// Architect should NOT have edit tools
// Note: apply_diff is now a legacy name; edit tools are now in customTools
// and the unified "edit_file" is added via filterNativeToolsForMode
expect(architectAllowedTools.has("write_to_file")).toBe(false)
expect(architectAllowedTools.has("apply_diff")).toBe(false)
// Architect SHOULD have read tools
expect(architectAllowedTools.has("read_file")).toBe(true)
@ -68,8 +69,9 @@ describe("Native Tools Filtering by Mode", () => {
ALWAYS_AVAILABLE_TOOLS.forEach((tool) => codeAllowedTools.add(tool))
// Code SHOULD have edit tools
// Note: apply_diff is now a legacy name; the unified edit tool "edit_file"
// is added via filterNativeToolsForMode, not from TOOL_GROUPS.edit.tools
expect(codeAllowedTools.has("write_to_file")).toBe(true)
expect(codeAllowedTools.has("apply_diff")).toBe(true)
// Code SHOULD have read tools
expect(codeAllowedTools.has("read_file")).toBe(true)

View file

@ -14,38 +14,38 @@ import { sanitizeUnifiedDiff, computeDiffStats } from "../diff/stats"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
interface SearchReplaceOperation {
search: string
replace: string
interface EditOperation {
old_text: string
new_text: string
}
interface SearchAndReplaceParams {
path: string
operations: SearchReplaceOperation[]
edits: EditOperation[]
}
export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
readonly name = "search_and_replace" as const
parseLegacy(params: Partial<Record<string, string>>): SearchAndReplaceParams {
// Parse operations from JSON string if provided
let operations: SearchReplaceOperation[] = []
if (params.operations) {
// Parse edits from JSON string if provided
let edits: EditOperation[] = []
if (params.edits) {
try {
operations = JSON.parse(params.operations)
edits = JSON.parse(params.edits)
} catch {
operations = []
edits = []
}
}
return {
path: params.path || "",
operations,
edits,
}
}
async execute(params: SearchAndReplaceParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { path: relPath, operations } = params
const { path: relPath, edits } = params
const { askApproval, handleError, pushToolResult, toolProtocol } = callbacks
try {
@ -57,30 +57,30 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
return
}
if (!operations || !Array.isArray(operations) || operations.length === 0) {
if (!edits || !Array.isArray(edits) || edits.length === 0) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
pushToolResult(
formatResponse.toolError(
"Missing or empty 'operations' parameter. At least one search/replace operation is required.",
"Missing or empty 'edits' parameter. At least one edit operation is required.",
),
)
return
}
// Validate each operation has search and replace fields
for (let i = 0; i < operations.length; i++) {
const op = operations[i]
if (!op.search) {
// Validate each edit has old_text and new_text fields
for (let i = 0; i < edits.length; i++) {
const op = edits[i]
if (!op.old_text) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
pushToolResult(formatResponse.toolError(`Operation ${i + 1} is missing the 'search' field.`))
pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'old_text' field.`))
return
}
if (op.replace === undefined) {
if (op.new_text === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace")
pushToolResult(formatResponse.toolError(`Operation ${i + 1} is missing the 'replace' field.`))
pushToolResult(formatResponse.toolError(`Edit ${i + 1} is missing the 'new_text' field.`))
return
}
}
@ -122,38 +122,38 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
return
}
// Apply all operations sequentially
// Apply all edits sequentially
let newContent = fileContent
const errors: string[] = []
for (let i = 0; i < operations.length; i++) {
// Normalize line endings in search/replace strings to match file content
const search = operations[i].search.replace(/\r\n/g, "\n")
const replace = operations[i].replace.replace(/\r\n/g, "\n")
const searchPattern = new RegExp(escapeRegExp(search), "g")
for (let i = 0; i < edits.length; i++) {
// Normalize line endings in search/replace strings to match file content
const old_text = edits[i].old_text.replace(/\r\n/g, "\n")
const new_text = edits[i].new_text.replace(/\r\n/g, "\n")
const searchPattern = new RegExp(escapeRegExp(old_text), "g")
const matchCount = newContent.match(searchPattern)?.length ?? 0
if (matchCount === 0) {
errors.push(`Operation ${i + 1}: No match found for search text.`)
errors.push(`Edit ${i + 1}: No match found for old_text.`)
continue
}
if (matchCount > 1) {
errors.push(
`Operation ${i + 1}: Found ${matchCount} matches. Please provide more context to make a unique match.`,
`Edit ${i + 1}: Found ${matchCount} matches. Please provide more context to make a unique match.`,
)
continue
}
// Apply the replacement
newContent = newContent.replace(searchPattern, replace)
newContent = newContent.replace(searchPattern, new_text)
}
// If all operations failed, return error
if (errors.length === operations.length) {
// If all edits failed, return error
if (errors.length === edits.length) {
task.consecutiveMistakeCount++
task.recordToolError("search_and_replace", "no_match")
pushToolResult(formatResponse.toolError(`All operations failed:\n${errors.join("\n")}`))
pushToolResult(formatResponse.toolError(`All edits failed:\n${errors.join("\n")}`))
return
}
@ -201,7 +201,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
// Include any partial errors in the message
let resultMessage = ""
if (errors.length > 0) {
resultMessage = `Some operations failed:\n${errors.join("\n")}\n\n`
resultMessage = `Some edits failed:\n${errors.join("\n")}\n\n`
}
const completeMessage = JSON.stringify({
@ -270,17 +270,17 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
override async handlePartial(task: Task, block: ToolUse<"search_and_replace">): Promise<void> {
const relPath: string | undefined = block.params.path
const operationsStr: string | undefined = block.params.operations
const editsStr: string | undefined = block.params.edits
let operationsPreview: string | undefined
if (operationsStr) {
let editsPreview: string | undefined
if (editsStr) {
try {
const ops = JSON.parse(operationsStr)
const ops = JSON.parse(editsStr)
if (Array.isArray(ops) && ops.length > 0) {
operationsPreview = `${ops.length} operation(s)`
editsPreview = `${ops.length} edit(s)`
}
} catch {
operationsPreview = "parsing..."
editsPreview = "parsing..."
}
}
@ -290,7 +290,7 @@ export class SearchAndReplaceTool extends BaseTool<"search_and_replace"> {
const sharedMessageProps: ClineSayTool = {
tool: "appliedDiff",
path: getReadablePath(task.cwd, relPath || ""),
diff: operationsPreview,
diff: editsPreview,
isOutsideWorkspace,
}

View file

@ -93,13 +93,15 @@ describe("mode-validator", () => {
groups: ["edit"] as const,
},
]
const requirements = { apply_diff: false }
// Use write_to_file for requirement testing (edit_file is the unified tool name
// which is handled separately from TOOL_GROUPS in filterNativeToolsForMode)
const requirements = { write_to_file: false }
// Should respect disabled requirement even if tool group is allowed
expect(isToolAllowedForMode("apply_diff", "custom-mode", customModes, requirements)).toBe(false)
expect(isToolAllowedForMode("write_to_file", "custom-mode", customModes, requirements)).toBe(false)
// Should allow other edit tools
expect(isToolAllowedForMode("write_to_file", "custom-mode", customModes, requirements)).toBe(true)
// Should allow other edit tools when not disabled
expect(isToolAllowedForMode("generate_image", "custom-mode", customModes, requirements)).toBe(true)
})
})
@ -140,28 +142,30 @@ describe("mode-validator", () => {
})
describe("tool requirements", () => {
// Note: apply_diff is now a legacy tool name. The unified edit tool is "edit_file".
// For testing requirements, we use write_to_file which is still in TOOL_GROUPS.
it("respects tool requirements when provided", () => {
const requirements = { apply_diff: false }
expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(false)
const requirements = { write_to_file: false }
expect(isToolAllowedForMode("write_to_file", codeMode, [], requirements)).toBe(false)
const enabledRequirements = { apply_diff: true }
expect(isToolAllowedForMode("apply_diff", codeMode, [], enabledRequirements)).toBe(true)
const enabledRequirements = { write_to_file: true }
expect(isToolAllowedForMode("write_to_file", codeMode, [], enabledRequirements)).toBe(true)
})
it("allows tools when their requirements are not specified", () => {
const requirements = { some_other_tool: true }
expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(true)
expect(isToolAllowedForMode("write_to_file", codeMode, [], requirements)).toBe(true)
})
it("handles undefined and empty requirements", () => {
expect(isToolAllowedForMode("apply_diff", codeMode, [], undefined)).toBe(true)
expect(isToolAllowedForMode("apply_diff", codeMode, [], {})).toBe(true)
expect(isToolAllowedForMode("write_to_file", codeMode, [], undefined)).toBe(true)
expect(isToolAllowedForMode("write_to_file", codeMode, [], {})).toBe(true)
})
it("prioritizes requirements over mode configuration", () => {
const requirements = { apply_diff: false }
const requirements = { write_to_file: false }
// Even in code mode which allows all tools, disabled requirement should take precedence
expect(isToolAllowedForMode("apply_diff", codeMode, [], requirements)).toBe(false)
expect(isToolAllowedForMode("write_to_file", codeMode, [], requirements)).toBe(false)
})
})
})
@ -186,19 +190,19 @@ describe("mode-validator", () => {
})
it("throws error when tool requirement is not met", () => {
const requirements = { apply_diff: false }
expect(() => validateToolUse("apply_diff", codeMode, [], requirements)).toThrow(
'Tool "apply_diff" is not allowed in code mode.',
const requirements = { write_to_file: false }
expect(() => validateToolUse("write_to_file", codeMode, [], requirements)).toThrow(
'Tool "write_to_file" is not allowed in code mode.',
)
})
it("does not throw when tool requirement is met", () => {
const requirements = { apply_diff: true }
expect(() => validateToolUse("apply_diff", codeMode, [], requirements)).not.toThrow()
const requirements = { write_to_file: true }
expect(() => validateToolUse("write_to_file", codeMode, [], requirements)).not.toThrow()
})
it("handles undefined requirements gracefully", () => {
expect(() => validateToolUse("apply_diff", codeMode, [], undefined)).not.toThrow()
expect(() => validateToolUse("write_to_file", codeMode, [], undefined)).not.toThrow()
})
})
})

View file

@ -62,7 +62,7 @@ export function validateToolUse(
}
}
const EDIT_OPERATION_PARAMS = ["diff", "content", "operations", "search", "replace", "args", "line"] as const
const EDIT_OPERATION_PARAMS = ["diff", "content", "edits", "old_text", "new_text", "args", "line"] as const
function getGroupOptions(group: GroupEntry): GroupOptions | undefined {
return Array.isArray(group) ? group[1] : undefined

View file

@ -70,7 +70,9 @@ export const toolParamNames = [
"prompt",
"image",
"files", // Native protocol parameter for read_file
"operations", // search_and_replace parameter for multiple operations
"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
@ -91,11 +93,18 @@ 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: { path: string; diff: string }
search_and_replace: { path: string; operations: Array<{ search: string; replace: 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)
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 }
edit_file_gemini: { file_path: string; old_string: string; new_string: string; expected_replacements?: number }
edit_file_codex: { patch: string }
ask_followup_question: {
question: string
follow_up: Array<{ text: string; mode?: string }>
@ -248,11 +257,19 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
read_file: "read files",
fetch_instructions: "fetch instructions",
write_to_file: "write files",
// Legacy edit tool names (deprecated)
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
edit_file_roo: "edit files (roo format)",
edit_file_anthropic: "edit files (anthropic format)",
edit_file_grok: "edit files (grok format)",
edit_file_gemini: "edit files (gemini format)",
edit_file_codex: "edit files (codex format)",
// Other tools
search_files: "search files",
list_files: "list files",
browser_action: "use a browser",
@ -275,8 +292,25 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["read_file", "fetch_instructions", "search_files", "list_files", "codebase_search"],
},
edit: {
tools: ["apply_diff", "write_to_file", "generate_image"],
customTools: ["search_and_replace", "search_replace", "edit_file", "apply_patch"],
// apply_diff is included for XML protocol backward compatibility
// For native protocol, filterNativeToolsForMode selects the appropriate edit_file_* variant
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",
],
},
browser: {
tools: ["browser_action"],
@ -315,6 +349,12 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
*/
export const TOOL_ALIASES: Record<string, ToolName> = {
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 =