Skip to main content

sgr_agent_tools/
delete.rs

1//! DeleteTool — delete one or more files (batch delete support).
2
3use std::sync::Arc;
4
5use schemars::JsonSchema;
6use serde::Deserialize;
7use serde_json::Value;
8use sgr_agent_core::agent_tool::{Tool, ToolError, ToolOutput, parse_args};
9use sgr_agent_core::context::AgentContext;
10use sgr_agent_core::schema::json_schema_for;
11
12use crate::backend::FileBackend;
13
14pub struct DeleteTool<B: FileBackend>(pub Arc<B>);
15
16#[derive(Deserialize, JsonSchema)]
17struct DeleteArgs {
18    /// File path to delete (use this for single file)
19    #[serde(default)]
20    path: Option<String>,
21    /// Multiple file paths to delete in one call (preferred for bulk cleanup)
22    #[serde(default)]
23    paths: Option<Vec<String>>,
24}
25
26#[async_trait::async_trait]
27impl<B: FileBackend> Tool for DeleteTool<B> {
28    fn name(&self) -> &str {
29        "delete"
30    }
31    fn description(&self) -> &str {
32        "Delete one or more files. Pass `path` for a single file, or `paths` (array) to delete many files at once."
33    }
34    fn parameters_schema(&self) -> Value {
35        json_schema_for::<DeleteArgs>()
36    }
37    async fn execute(&self, args: Value, _ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
38        let a: DeleteArgs = parse_args(&args)?;
39
40        let targets: Vec<String> = match (a.path, a.paths) {
41            (_, Some(ps)) if !ps.is_empty() => ps,
42            (Some(p), _) => vec![p],
43            _ => {
44                return Err(ToolError::InvalidArgs(
45                    "provide `path` (string) or `paths` (array)".into(),
46                ));
47            }
48        };
49
50        let mut results: Vec<String> = Vec::with_capacity(targets.len());
51        let mut errors: Vec<String> = Vec::new();
52
53        for path in &targets {
54            match self.0.delete(path).await {
55                Ok(()) => results.push(format!("Deleted {}", path)),
56                Err(e) => errors.push(format!("FAILED {}: {}", path, e)),
57            }
58        }
59
60        let mut out = results.join("\n");
61        if !errors.is_empty() {
62            if !out.is_empty() {
63                out.push('\n');
64            }
65            out.push_str(&errors.join("\n"));
66        }
67        if out.is_empty() {
68            out = "No files deleted".to_string();
69        }
70        Ok(ToolOutput::text(out))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::mock_fs::MockFs;
78    use sgr_agent_core::agent_tool::Tool;
79
80    #[tokio::test]
81    async fn test_single_delete() {
82        let fs = Arc::new(MockFs::new());
83        fs.add_file("tmp.txt", "bye");
84        let tool = DeleteTool(fs.clone());
85        let mut ctx = AgentContext::new();
86        let result = tool
87            .execute(serde_json::json!({"path": "tmp.txt"}), &mut ctx)
88            .await
89            .unwrap();
90        assert!(result.content.contains("Deleted tmp.txt"));
91        assert!(!fs.exists("tmp.txt"));
92    }
93
94    #[tokio::test]
95    async fn test_batch_delete() {
96        let fs = Arc::new(MockFs::new());
97        fs.add_file("a.txt", "1");
98        fs.add_file("b.txt", "2");
99        let tool = DeleteTool(fs.clone());
100        let mut ctx = AgentContext::new();
101        let result = tool
102            .execute(serde_json::json!({"paths": ["a.txt", "b.txt"]}), &mut ctx)
103            .await
104            .unwrap();
105        assert!(result.content.contains("Deleted a.txt"));
106        assert!(result.content.contains("Deleted b.txt"));
107        assert!(!fs.exists("a.txt"));
108        assert!(!fs.exists("b.txt"));
109    }
110
111    #[tokio::test]
112    async fn test_delete_nonexistent() {
113        let fs = Arc::new(MockFs::new());
114        let tool = DeleteTool(fs.clone());
115        let mut ctx = AgentContext::new();
116        let result = tool
117            .execute(serde_json::json!({"path": "ghost.txt"}), &mut ctx)
118            .await
119            .unwrap();
120        assert!(result.content.contains("FAILED ghost.txt"));
121    }
122}