Skip to content

Commit 0115f25

Browse files
committed
add edit_file tool for targeted string replacement edits
1 parent 9009a08 commit 0115f25

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

src/tools/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,43 @@ impl ToolExecutor {
389389
let results = code_search.search(pattern, file_type, max_results)?;
390390
Ok(format!("Code search results:\n\n{}", results))
391391
}
392+
ToolName::EditFile => {
393+
let path = input["path"].as_str().ok_or_else(|| {
394+
SofosError::ToolExecution("Missing 'path' parameter".to_string())
395+
})?;
396+
let old_string = input["old_string"].as_str().ok_or_else(|| {
397+
SofosError::ToolExecution("Missing 'old_string' parameter".to_string())
398+
})?;
399+
let new_string = input["new_string"].as_str().ok_or_else(|| {
400+
SofosError::ToolExecution("Missing 'new_string' parameter".to_string())
401+
})?;
402+
let replace_all = input["replace_all"].as_bool().unwrap_or(false);
403+
404+
let original = self.fs_tool.read_file(path)?;
405+
406+
if !original.contains(old_string) {
407+
return Err(SofosError::ToolExecution(format!(
408+
"old_string not found in '{}'. Make sure it matches the file content exactly, \
409+
including whitespace and indentation.",
410+
path
411+
)));
412+
}
413+
414+
let modified = if replace_all {
415+
original.replace(old_string, new_string)
416+
} else {
417+
original.replacen(old_string, new_string, 1)
418+
};
419+
420+
self.fs_tool.write_file(path, &modified)?;
421+
422+
let diff_output = diff::generate_compact_diff(&original, &modified, path);
423+
424+
Ok(format!(
425+
"Successfully edited '{}'\n\nChanges:\n{}",
426+
path, diff_output
427+
))
428+
}
392429
ToolName::MorphEditFile => {
393430
let morph = self.morph_client.as_ref().ok_or_else(|| {
394431
SofosError::ToolExecution(

src/tools/tool_name.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub enum ToolName {
1313
CopyFile,
1414
ExecuteBash,
1515
SearchCode,
16+
EditFile,
1617
MorphEditFile,
1718
WebSearch,
1819
}
@@ -30,6 +31,7 @@ impl ToolName {
3031
ToolName::CopyFile => "copy_file",
3132
ToolName::ExecuteBash => "execute_bash",
3233
ToolName::SearchCode => "search_code",
34+
ToolName::EditFile => "edit_file",
3335
ToolName::MorphEditFile => "morph_edit_file",
3436
ToolName::WebSearch => "web_search",
3537
}
@@ -47,6 +49,7 @@ impl ToolName {
4749
"copy_file" => Ok(ToolName::CopyFile),
4850
"execute_bash" => Ok(ToolName::ExecuteBash),
4951
"search_code" => Ok(ToolName::SearchCode),
52+
"edit_file" => Ok(ToolName::EditFile),
5053
"morph_edit_file" => Ok(ToolName::MorphEditFile),
5154
"web_search" => Ok(ToolName::WebSearch),
5255
_ => Err(SofosError::ToolExecution(format!("Unknown tool: {}", s))),

src/tools/types.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,36 @@ fn copy_file_tool() -> Tool {
198198
}
199199
}
200200

201+
fn edit_file_tool() -> Tool {
202+
Tool::Regular {
203+
name: "edit_file".to_string(),
204+
description: "Make targeted edits to a file by replacing exact string matches. Preferred over write_file for modifying existing files — safer and more efficient since only the changed portion is specified.".to_string(),
205+
input_schema: json!({
206+
"type": "object",
207+
"properties": {
208+
"path": {
209+
"type": "string",
210+
"description": "The relative path to the file (e.g., 'src/main.rs')"
211+
},
212+
"old_string": {
213+
"type": "string",
214+
"description": "The exact text to find and replace. Must match the file content exactly, including whitespace and indentation."
215+
},
216+
"new_string": {
217+
"type": "string",
218+
"description": "The replacement text. Use an empty string to delete the matched text."
219+
},
220+
"replace_all": {
221+
"type": "boolean",
222+
"description": "If true, replace all occurrences. Default: false (replace first match only)."
223+
}
224+
},
225+
"required": ["path", "old_string", "new_string"]
226+
}),
227+
cache_control: None,
228+
}
229+
}
230+
201231
fn morph_edit_file_tool() -> Tool {
202232
Tool::Regular {
203233
name: "morph_edit_file".to_string(),
@@ -230,15 +260,14 @@ pub fn get_all_tools() -> Vec<Tool> {
230260
list_directory_tool(),
231261
read_file_tool(),
232262
write_file_tool(false),
263+
edit_file_tool(),
233264
create_directory_tool(),
234265
delete_file_tool(),
235266
delete_directory_tool(),
236267
move_file_tool(),
237268
copy_file_tool(),
238269
execute_bash_tool(),
239-
// Anthropic web search tool
240270
anthropic_web_search_tool(),
241-
// OpenAI web search tool
242271
openai_web_search_tool(),
243272
]
244273
}
@@ -248,16 +277,15 @@ pub fn get_all_tools_with_morph() -> Vec<Tool> {
248277
list_directory_tool(),
249278
read_file_tool(),
250279
write_file_tool(true),
280+
edit_file_tool(),
251281
create_directory_tool(),
252282
delete_file_tool(),
253283
delete_directory_tool(),
254284
move_file_tool(),
255285
copy_file_tool(),
256286
execute_bash_tool(),
257287
morph_edit_file_tool(),
258-
// Anthropic web search tool
259288
anthropic_web_search_tool(),
260-
// OpenAI web search tool
261289
openai_web_search_tool(),
262290
]
263291
}

0 commit comments

Comments
 (0)