mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
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:
parent
861139ca24
commit
0126cb0b15
19 changed files with 519 additions and 258 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">, {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
46
src/core/prompts/tools/native-tools/edit_file_anthropic.ts
Normal file
46
src/core/prompts/tools/native-tools/edit_file_anthropic.ts
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue