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