Skip to main content

deepseek/agent/builtin_tools/
edit.rs

1use async_trait::async_trait;
2use serde_json::{json, Value};
3
4use crate::agent::tool::{Tool, ToolDefinition};
5
6
7pub struct EditTool;
8
9#[async_trait]
10impl Tool for EditTool {
11    fn name(&self) -> &str {
12        "Edit"
13    }
14
15    fn definition(&self) -> ToolDefinition {
16        ToolDefinition {
17            name: self.name().to_string(),
18            description: "Replace one occurrence (or all, with `replace_all`) of an exact \
19                          string in a file. Errors if `old_string` is missing or appears \
20                          multiple times unless `replace_all` is set."
21                .into(),
22            parameters: json!({
23                "type": "object",
24                "properties": {
25                    "path":        { "type": "string" },
26                    "old_string":  { "type": "string" },
27                    "new_string":  { "type": "string" },
28                    "replace_all": { "type": "boolean", "default": false }
29                },
30                "required": ["path", "old_string", "new_string"]
31            }),
32        }
33    }
34
35    async fn call_json(&self, args: Value) -> Result<String, String> {
36        let path = args
37            .get("path")
38            .and_then(Value::as_str)
39            .ok_or_else(|| "Edit: missing string `path`".to_string())?;
40        let old = args
41            .get("old_string")
42            .and_then(Value::as_str)
43            .ok_or_else(|| "Edit: missing string `old_string`".to_string())?;
44        let new = args
45            .get("new_string")
46            .and_then(Value::as_str)
47            .ok_or_else(|| "Edit: missing string `new_string`".to_string())?;
48        let replace_all = args
49            .get("replace_all")
50            .and_then(Value::as_bool)
51            .unwrap_or(false);
52
53        let body = tokio::fs::read_to_string(path)
54            .await
55            .map_err(|e| format!("Edit({path}): {e}"))?;
56
57        let count = body.matches(old).count();
58        if count == 0 {
59            return Err(format!("Edit({path}): `old_string` not found"));
60        }
61        if count > 1 && !replace_all {
62            return Err(format!(
63                "Edit({path}): `old_string` matches {count} times; \
64                 set `replace_all=true` or supply more context"
65            ));
66        }
67        let updated = if replace_all {
68            body.replace(old, new)
69        } else {
70            body.replacen(old, new, 1)
71        };
72        tokio::fs::write(path, updated)
73            .await
74            .map_err(|e| format!("Edit({path}): write: {e}"))?;
75        Ok(format!(
76            "Edited {path}: replaced {} occurrence(s)",
77            if replace_all { count } else { 1 }
78        ))
79    }
80}