diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 0e81671cc1..061fa0eb14 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -7,6 +7,26 @@ describe("NativeToolCallParser", () => { }) describe("parseToolCall", () => { + it("should resolve apply_patch to edit_file and preserve originalName", () => { + const toolCall = { + id: "toolu_apply_patch_123", + name: "apply_patch" as any, + arguments: JSON.stringify({ + patch: "*** Begin Patch\n*** End Patch", + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + if (result?.type === "tool_use") { + expect(result.name).toBe("edit_file") + expect(result.originalName).toBe("apply_patch") + expect(result.nativeArgs).toEqual({ patch: "*** Begin Patch\n*** End Patch" }) + } + }) + describe("read_file tool", () => { it("should handle line_ranges as tuples (new format)", () => { const toolCall = { diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts index f4caeb6c4e..594fab86a6 100644 --- a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts +++ b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts @@ -11,7 +11,7 @@ import * as toolsModule from "../../../../shared/tools" // and replaced with the unified "edit_file" tool. describe("filterNativeToolsForMode", () => { - // Use edit_file_roo as the default edit tool variant in mock tools + // Include all edit tool variants to mirror the production native tools array. const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ { type: "function", @@ -37,6 +37,38 @@ describe("filterNativeToolsForMode", () => { parameters: {}, }, }, + { + type: "function", + function: { + name: "edit_file_anthropic", + description: "Edit file (Anthropic variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_grok", + description: "Edit file (Grok variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_gemini", + description: "Edit file (Gemini variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_codex", + description: "Edit file (Codex variant)", + parameters: {}, + }, + }, { type: "function", function: { @@ -123,10 +155,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 + // Note: edit tool variants get renamed to a single display name. + // Default is "edit_file" (roo variant), but codex can be presented as "apply_patch". expect(toolNames).toContain("read_file") expect(toolNames).toContain("write_to_file") - expect(toolNames).toContain("edit_file") // Unified edit tool + expect(toolNames).toContain("edit_file") // Unified edit tool (default) expect(toolNames).not.toContain("edit_file_roo") // Variant name should be renamed expect(toolNames).toContain("execute_command") expect(toolNames).toContain("browser_action") @@ -134,6 +167,29 @@ describe("filterNativeToolsForMode", () => { expect(toolNames).toContain("attempt_completion") }) + it("should present codex edit tool variant as apply_patch when editToolVariant is codex", () => { + const codeMode: ModeConfig = { + slug: "code", + name: "Code", + roleDefinition: "Test", + groups: ["read", "edit", "browser", "command", "mcp"] as const, + } + + const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, { + modelInfo: { + contextWindow: 100000, + supportsPromptCache: false, + editToolVariant: "codex", + }, + }) + + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) + + expect(toolNames).toContain("apply_patch") + expect(toolNames).not.toContain("edit_file") + expect(toolNames).not.toContain("edit_file_codex") + }) + it("should always include always-available tools regardless of mode groups", () => { const restrictiveMode: ModeConfig = { slug: "restrictive", @@ -718,6 +774,38 @@ describe("filterMcpToolsForMode", () => { parameters: {}, }, }, + { + type: "function", + function: { + name: "edit_file_anthropic", + description: "Edit file (Anthropic variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_grok", + description: "Edit file (Grok variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_gemini", + description: "Edit file (Gemini variant)", + parameters: {}, + }, + }, + { + type: "function", + function: { + name: "edit_file_codex", + description: "Edit file (Codex variant)", + parameters: {}, + }, + }, { type: "function", function: { diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index 0342557cf1..e8dfb106d6 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -8,22 +8,30 @@ 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. + * Central edit tool configuration. + * + * `toolName` is the internal native tool name. + * `displayName` is the name shown to (and called by) the model. + * + * By default, edit tool variants are presented as "edit_file". + * The codex variant is presented as "apply_patch" so: + * - the model calls `apply_patch` + * - the tool is stored in API conversation history as `apply_patch` + * - execution is still routed through the canonical `edit_file` tool via aliases */ -const EDIT_TOOL_VARIANT_MAP: Record = { - roo: "edit_file_roo", - anthropic: "edit_file_anthropic", - grok: "edit_file_grok", - gemini: "edit_file_gemini", - codex: "edit_file_codex", +const EDIT_TOOL_VARIANTS: Record = { + roo: { toolName: "edit_file_roo", displayName: "edit_file" }, + anthropic: { toolName: "edit_file_anthropic", displayName: "edit_file" }, + grok: { toolName: "edit_file_grok", displayName: "edit_file" }, + gemini: { toolName: "edit_file_gemini", displayName: "edit_file" }, + codex: { toolName: "edit_file_codex", displayName: "apply_patch" }, } /** * All edit tool variant names that should be filtered. - * Only one of these (based on editToolVariant) will be included and renamed to "edit_file". + * Only one of these (based on editToolVariant) will be included and renamed to the configured display name. */ -const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANT_MAP)) +const ALL_EDIT_TOOL_VARIANTS = new Set(Object.values(EDIT_TOOL_VARIANTS).map((variant) => variant.toolName)) /** * Reverse lookup map - maps alias name to canonical tool name. @@ -326,7 +334,8 @@ export function filterNativeToolsForMode( // Determine which edit tool variant to use (default: "roo") const editToolVariant: EditToolVariant = modelInfo?.editToolVariant ?? "roo" - const selectedEditToolName = EDIT_TOOL_VARIANT_MAP[editToolVariant] + const selectedEditToolName = EDIT_TOOL_VARIANTS[editToolVariant].toolName + const editToolDisplayName = EDIT_TOOL_VARIANTS[editToolVariant].displayName // Check if diffs are disabled - if so, skip edit tool entirely const diffEnabled = settings?.diffEnabled !== false @@ -358,8 +367,9 @@ export function filterNativeToolsForMode( // 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")) + // Rename the selected variant to the configured display name. + // The tool will still execute via the unified `edit_file` tool name through aliases. + filteredTools.push(getOrCreateRenamedTool(tool, editToolDisplayName)) } continue } diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 2ed86d18cd..bec4d0b3d9 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -339,12 +339,14 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ * When a model calls a tool by its alias, the system resolves it to the canonical name for execution, * but preserves the alias in API conversation history for consistency. * - * Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace, apply_patch) - * have been removed. Native protocol now uses editToolVariant to select the edit tool schema, - * and all edit tools are presented to the LLM as "edit_file". XML protocol still uses apply_diff directly. + * Note: Legacy edit tool aliases (apply_diff, search_and_replace, search_replace) + * have been removed. Native protocol uses editToolVariant to select the edit tool schema. + * The codex variant can optionally be presented to the LLM as "apply_patch". + * XML protocol still uses apply_diff directly. */ export const TOOL_ALIASES: Record = { write_file: "write_to_file", + apply_patch: "edit_file", } as const export type DiffResult =