Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ describe("CustomToolRegistry", () => {
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)

expect(result.loaded).toContain("cached")
}, 30000)
}, 120_000)
})

describe.sequential("loadFromDirectories", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const toolNames = [
"read_command_output",
"write_to_file",
"apply_diff",
"edit",
"search_and_replace",
"search_replace",
"edit_file",
Expand Down
26 changes: 20 additions & 6 deletions src/core/assistant-message/NativeToolCallParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,18 @@ export class NativeToolCallParser {
}
break

case "edit":
case "search_and_replace":
if (partialArgs.path !== undefined || partialArgs.operations !== undefined) {
if (
partialArgs.file_path !== undefined ||
partialArgs.old_string !== undefined ||
partialArgs.new_string !== undefined
) {
nativeArgs = {
path: partialArgs.path,
operations: partialArgs.operations,
file_path: partialArgs.file_path,
old_string: partialArgs.old_string,
new_string: partialArgs.new_string,
replace_all: this.coerceOptionalBoolean(partialArgs.replace_all),
}
}
break
Expand Down Expand Up @@ -806,11 +813,18 @@ export class NativeToolCallParser {
}
break

case "edit":
case "search_and_replace":
if (args.path !== undefined && args.operations !== undefined && Array.isArray(args.operations)) {
if (
args.file_path !== undefined &&
args.old_string !== undefined &&
args.new_string !== undefined
) {
nativeArgs = {
path: args.path,
operations: args.operations,
file_path: args.file_path,
old_string: args.old_string,
new_string: args.new_string,
replace_all: this.coerceOptionalBoolean(args.replace_all),
} as NativeArgsFor<TName>
}
break
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,54 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
expect(toolResult.is_error).toBe(true)
expect(toolResult.content).toContain("due to user rejecting a previous tool")
})

it("should not treat <editor> as XML tool markup", async () => {
mockTask.assistantMessageContent = [
{
type: "text",
content: "Please open the <editor> panel and continue.",
partial: false,
},
]

await presentAssistantMessage(mockTask)

expect(mockTask.say).toHaveBeenCalledWith(
"text",
"Please open the <editor> panel and continue.",
undefined,
false,
)
expect(mockTask.say).not.toHaveBeenCalledWith(
"error",
expect.stringContaining("XML tool calls are no longer supported"),
)
expect(mockTask.didAlreadyUseTool).toBe(false)
expect(mockTask.consecutiveMistakeCount).toBe(0)
})

it("should not treat <editable> as XML tool markup", async () => {
mockTask.assistantMessageContent = [
{
type: "text",
content: "Use an <editable> region in the docs example.",
partial: false,
},
]

await presentAssistantMessage(mockTask)

expect(mockTask.say).toHaveBeenCalledWith(
"text",
"Use an <editable> region in the docs example.",
undefined,
false,
)
expect(mockTask.say).not.toHaveBeenCalledWith(
"error",
expect.stringContaining("XML tool calls are no longer supported"),
)
expect(mockTask.didAlreadyUseTool).toBe(false)
expect(mockTask.consecutiveMistakeCount).toBe(0)
})
})
30 changes: 26 additions & 4 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { listFilesTool } from "../tools/ListFilesTool"
import { readFileTool } from "../tools/ReadFileTool"
import { readCommandOutputTool } from "../tools/ReadCommandOutputTool"
import { writeToFileTool } from "../tools/WriteToFileTool"
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
import { editTool } from "../tools/EditTool"
import { searchReplaceTool } from "../tools/SearchReplaceTool"
import { editFileTool } from "../tools/EditFileTool"
import { applyPatchTool } from "../tools/ApplyPatchTool"
Expand Down Expand Up @@ -357,8 +357,9 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name} for '${block.params.regex}'${
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
}]`
case "edit":
case "search_and_replace":
return `[${block.name} for '${block.params.path}']`
return `[${block.name} for '${block.params.file_path}']`
case "search_replace":
return `[${block.name} for '${block.params.file_path}']`
case "edit_file":
Expand Down Expand Up @@ -739,9 +740,10 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult,
})
break
case "edit":
case "search_and_replace":
await checkpointSaveAndMark(cline)
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
await editTool.handle(cline, block as ToolUse<"edit">, {
askApproval,
handleError,
pushToolResult,
Expand Down Expand Up @@ -1046,6 +1048,25 @@ function containsXmlToolMarkup(text: string): boolean {
// Avoid regex so we don't keep legacy XML parsing artifacts around.
// Note: This is a best-effort safeguard; tool_use blocks without an id are rejected elsewhere.

const isTagBoundary = (char: string | undefined): boolean => {
if (char === undefined) {
return true
}
return char === ">" || char === "/" || char === " " || char === "\n" || char === "\r" || char === "\t"
}

const hasTagReference = (haystack: string, prefix: string): boolean => {
let index = haystack.indexOf(prefix)
while (index !== -1) {
const nextChar = haystack[index + prefix.length]
if (isTagBoundary(nextChar)) {
return true
}
index = haystack.indexOf(prefix, index + 1)
}
return false
}

// First, strip out content inside markdown code fences to avoid false positives
// when users paste documentation or examples containing tool tag references.
// This handles both fenced code blocks (```) and inline code (`).
Expand Down Expand Up @@ -1073,6 +1094,7 @@ function containsXmlToolMarkup(text: string): boolean {
"new_task",
"read_command_output",
"read_file",
"edit",
"search_and_replace",
"search_files",
"search_replace",
Expand All @@ -1082,5 +1104,5 @@ function containsXmlToolMarkup(text: string): boolean {
"write_to_file",
] as const

return toolNames.some((name) => lower.includes(`<${name}`) || lower.includes(`</${name}`))
return toolNames.some((name) => hasTagReference(lower, `<${name}`) || hasTagReference(lower, `</${name}`))
}
48 changes: 48 additions & 0 deletions src/core/prompts/tools/native-tools/edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type OpenAI from "openai"

const EDIT_DESCRIPTION = `Performs exact string replacements in files.

Usage:
- You must use your \`Read\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`

const edit = {
type: "function",
function: {
name: "edit",
description: EDIT_DESCRIPTION,
parameters: {
type: "object",
properties: {
file_path: {
type: "string",
description: "The path of the file to edit (relative to the working directory)",
},
old_string: {
type: "string",
description:
"The exact text to find in the file. Must match exactly, including all whitespace, indentation, and line endings.",
},
new_string: {
type: "string",
description:
"The replacement text that will replace old_string. Must include all necessary whitespace and indentation.",
},
replace_all: {
type: "boolean",
description:
"When true, replaces ALL occurrences of old_string in the file. When false (default), only replaces the first occurrence and errors if multiple matches exist.",
default: false,
},
},
required: ["file_path", "old_string", "new_string"],
additionalProperties: false,
},
},
} satisfies OpenAI.Chat.ChatCompletionTool

export default edit
4 changes: 2 additions & 2 deletions src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import askFollowupQuestion from "./ask_followup_question"
import attemptCompletion from "./attempt_completion"
import browserAction from "./browser_action"
import codebaseSearch from "./codebase_search"
import editTool from "./edit"
import executeCommand from "./execute_command"
import generateImage from "./generate_image"
import listFiles from "./list_files"
Expand All @@ -14,7 +15,6 @@ import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command"
import skill from "./skill"
import searchAndReplace from "./search_and_replace"
import searchReplace from "./search_replace"
import edit_file from "./edit_file"
import searchFiles from "./search_files"
Expand Down Expand Up @@ -63,9 +63,9 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
createReadFileTool(readFileOptions),
runSlashCommand,
skill,
searchAndReplace,
searchReplace,
edit_file,
editTool,
searchFiles,
switchMode,
updateTodoList,
Expand Down
44 changes: 0 additions & 44 deletions src/core/prompts/tools/native-tools/search_and_replace.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3537,7 +3537,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const input = toolUse.nativeArgs || toolUse.params

// Use originalName (alias) if present for API history consistency.
// When tool aliases are used (e.g., "edit_file" -> "search_and_replace"),
// When tool aliases are used (e.g., "edit_file" -> "search_and_replace" -> "edit" (current canonical name)),
// we want the alias name in the conversation history to match what the model
// was told the tool was named, preventing confusion in multi-turn conversations.
const toolNameForHistory = toolUse.originalName ?? toolUse.name
Expand Down
Loading
Loading