adk_devtools/tools/
edit.rs1use std::sync::Arc;
4
5use adk_core::{Result, Tool, ToolContext};
6use async_trait::async_trait;
7use serde_json::{Value, json};
8
9use crate::error::DevToolError;
10use crate::tools::read::require_str;
11use crate::workspace::Workspace;
12
13pub struct EditFileTool {
16 workspace: Workspace,
17}
18
19impl EditFileTool {
20 pub fn new(workspace: Workspace) -> Self {
22 Self { workspace }
23 }
24}
25
26#[async_trait]
27impl Tool for EditFileTool {
28 fn name(&self) -> &str {
29 "edit_file"
30 }
31
32 fn description(&self) -> &str {
33 "Replace an exact string in a file. The file must have been read with read_file \
34 first. By default the target string must occur exactly once; set replace_all=true \
35 to replace every occurrence."
36 }
37
38 fn parameters_schema(&self) -> Option<Value> {
39 Some(json!({
40 "type": "object",
41 "properties": {
42 "path": { "type": "string", "description": "Path relative to the workspace root." },
43 "old_string": { "type": "string", "description": "The exact text to replace." },
44 "new_string": { "type": "string", "description": "The replacement text." },
45 "replace_all": { "type": "boolean", "description": "Replace all occurrences (default false)." }
46 },
47 "required": ["path", "old_string", "new_string"]
48 }))
49 }
50
51 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
52 if !self.workspace.is_writable() {
53 return Err(DevToolError::ReadOnly("edit_file").into());
54 }
55 let path = require_str(&args, "path")?;
56 let old_string = require_str(&args, "old_string")?;
57 let new_string = require_str(&args, "new_string")?;
58 let replace_all = args.get("replace_all").and_then(Value::as_bool).unwrap_or(false);
59
60 let resolved = self.workspace.resolve(&path)?;
61 if !self.workspace.was_read(&resolved) {
62 return Err(DevToolError::NotRead(self.workspace.display(&resolved)).into());
63 }
64
65 let original = tokio::fs::read_to_string(&resolved).await.map_err(DevToolError::from)?;
66 let count = original.matches(&old_string).count();
67 let display = self.workspace.display(&resolved);
68 if count == 0 {
69 return Err(DevToolError::NoMatch(display).into());
70 }
71 if count > 1 && !replace_all {
72 return Err(DevToolError::Ambiguous { path: display, count }.into());
73 }
74
75 let updated = if replace_all {
76 original.replace(&old_string, &new_string)
77 } else {
78 original.replacen(&old_string, &new_string, 1)
79 };
80 self.workspace.write_contained(&resolved, updated.as_bytes()).await?;
81
82 Ok(json!({
83 "path": self.workspace.display(&resolved),
84 "replacements": if replace_all { count } else { 1 },
85 "message": format!(
86 "Replaced {} occurrence(s) in {}",
87 if replace_all { count } else { 1 },
88 self.workspace.display(&resolved)
89 ),
90 }))
91 }
92}