Add delete_file tool to allow deleting files and empty directories
Includes implementation, schema, and tests for the new delete_file tool. Updates agent prompt and documentation to mention delete_file capability.
Seto Elkahfi committed
Apr 24, 2026 at 01:28 UTC
c276617a2db8b09658a95285ce6b0cd462ae5623
4 files changed
+131
-7
Cargo.lock
+1
-1
@@ -5418,7 +5418,7 @@ version = "0.3.35"
5418
source = "registry+https://github.com/rust-lang/crates.io-index"
5419
checksum = "1c236db19d4cea01cf48b584838fcc898745514c8e01c1c0cad0cba976cabefb"
5420
dependencies = [
5421
- "clap 4.6.0",
5421
+ "clap 4.6.1",
5422
"log",
5423
"reqwest 0.12.28",
5424
"serde",
src/chat.rs
+3
-1
@@ -17,7 +17,9 @@ use std::sync::mpsc as std_mpsc;
17
use anyhow::Result;
18
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
19
use futures::StreamExt;
20
-use onde::inference::{ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult};
20
+use onde::inference::{
21
+ ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult,
22
+};
23
use ratatui::{
24
Frame,
25
layout::{Constraint, Layout, Position},
src/main.rs
+4
-3
@@ -66,9 +66,10 @@ You help developers build, debug, and ship software on the smbCloud platform.
66
Keep answers short. Write idiomatic code. \
67
Fix root causes, not symptoms.
68
69
-You have access to tools that let you read files, list directories, and search \
70
-code. Use them proactively to understand the codebase before answering questions \
71
-or writing code. Always ground your answers in the actual code.
69
+You have access to tools that let you read files, list directories, search \
70
+code, create new files, edit existing files, and delete files. Use them \
71
+proactively to understand the codebase before answering questions or writing \
72
+code. Always ground your answers in the actual code.
73
74
Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
75
root cause, not the symptom. Correct beats clever.";
src/tools.rs
+123
-2
@@ -1,4 +1,4 @@
1
-//! Tool definitions and execution for the siGit coding agent.
1
+//! Tool definitions and execution for the siGit Code.
2
//!
3
//! Each tool has:
4
//! - A schema (JSON Schema) that describes its parameters for the LLM
@@ -16,6 +16,7 @@
16
//!
17
//! - `create_file` — create a new file (fails if it already exists)
18
//! - `edit_file` — replace an exact old-text span with new text in an existing file
19
+//! - `delete_file` — delete a file or empty directory at the given path
20
21
use regex::Regex;
22
use serde_json::{Value, json};
@@ -147,6 +148,23 @@ pub fn all_tools() -> Vec<AgentTool> {
148
"additionalProperties": false
149
}),
150
},
151
+ AgentTool {
152
+ name: "delete_file",
153
+ description: "Delete a file or empty directory at the given path. \
154
+ Refuses to delete non-empty directories to prevent accidental data loss. \
155
+ Use read_file or list_directory first to confirm the target.",
156
+ parameters_schema: json!({
157
+ "type": "object",
158
+ "properties": {
159
+ "path": {
160
+ "type": "string",
161
+ "description": "Absolute or relative path to the file or empty directory to delete."
162
+ }
163
+ },
164
+ "required": ["path"],
165
+ "additionalProperties": false
166
+ }),
167
+ },
168
]
169
}
170
@@ -163,6 +181,7 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
181
"search_files" => exec_search_files(arguments),
182
"create_file" => exec_create_file(arguments),
183
"edit_file" => exec_edit_file(arguments),
184
+ "delete_file" => exec_delete_file(arguments),
185
_ => format!("Unknown tool: {name}"),
186
}
187
}
@@ -499,6 +518,45 @@ fn exec_edit_file(arguments: &str) -> String {
518
}
519
}
520
521
+// ── delete_file ──────────────────────────────────────────────────────────────
522
+
523
+/// Delete a file or empty directory at the given path.
524
+///
525
+/// Refuses to remove non-empty directories to guard against accidental
526
+/// recursive deletes.
527
+fn exec_delete_file(arguments: &str) -> String {
528
+ let args: Value = match serde_json::from_str(arguments) {
529
+ Ok(v) => v,
530
+ Err(err) => return format!("Error: failed to parse arguments: {err}"),
531
+ };
532
+
533
+ let path_str = match args.get("path").and_then(Value::as_str) {
534
+ Some(p) => p,
535
+ None => return "Error: missing required parameter \"path\"".to_string(),
536
+ };
537
+
538
+ let path = Path::new(path_str);
539
+
540
+ if !path.exists() {
541
+ return format!("Error: path does not exist: {path_str}");
542
+ }
543
+
544
+ if path.is_dir() {
545
+ match fs::remove_dir(path) {
546
+ Ok(()) => format!("Deleted empty directory: {path_str}"),
547
+ Err(err) => format!(
548
+ "Error: could not delete directory: {err}. \
549
+ Only empty directories can be deleted."
550
+ ),
551
+ }
552
+ } else {
553
+ match fs::remove_file(path) {
554
+ Ok(()) => format!("Deleted file: {path_str}"),
555
+ Err(err) => format!("Error: could not delete file: {err}"),
556
+ }
557
+ }
558
+}
559
+
560
#[cfg(test)]
561
mod tests {
562
use super::*;
@@ -613,12 +671,13 @@ mod tests {
671
#[test]
672
fn test_all_tools_count() {
673
let tools = all_tools();
616
- assert_eq!(tools.len(), 5);
674
+ assert_eq!(tools.len(), 6);
675
assert_eq!(tools[0].name, "read_file");
676
assert_eq!(tools[1].name, "list_directory");
677
assert_eq!(tools[2].name, "search_files");
678
assert_eq!(tools[3].name, "create_file");
679
assert_eq!(tools[4].name, "edit_file");
680
+ assert_eq!(tools[5].name, "delete_file");
681
}
682
683
#[test]
@@ -778,4 +837,66 @@ mod tests {
837
838
let _ = fs::remove_dir_all(&dir);
839
}
840
+
841
+ // ── delete_file tests ────────────────────────────────────────────────
842
+
843
+ #[test]
844
+ fn test_delete_file_missing_path() {
845
+ let result = exec_delete_file("{}");
846
+ assert!(
847
+ result.contains("missing required parameter"),
848
+ "got: {result}"
849
+ );
850
+ }
851
+
852
+ #[test]
853
+ fn test_delete_file_nonexistent() {
854
+ let result = exec_delete_file(r#"{"path": "/tmp/sigit_test_no_such_file_xyz"}"#);
855
+ assert!(result.contains("does not exist"), "got: {result}");
856
+ }
857
+
858
+ #[test]
859
+ fn test_delete_file_success() {
860
+ let dir = std::env::temp_dir().join("sigit_test_delete_file");
861
+ let _ = fs::remove_dir_all(&dir);
862
+ fs::create_dir_all(&dir).unwrap();
863
+
864
+ let file_path = dir.join("to_delete.txt");
865
+ fs::write(&file_path, "bye").unwrap();
866
+ assert!(file_path.exists());
867
+
868
+ let args = format!(r#"{{"path": "{}"}}"#, file_path.display());
869
+ let result = exec_delete_file(&args);
870
+ assert!(result.contains("Deleted file"), "got: {result}");
871
+ assert!(!file_path.exists());
872
+
873
+ let _ = fs::remove_dir_all(&dir);
874
+ }
875
+
876
+ #[test]
877
+ fn test_delete_empty_directory() {
878
+ let dir = std::env::temp_dir().join("sigit_test_delete_empty_dir");
879
+ let _ = fs::remove_dir_all(&dir);
880
+ fs::create_dir_all(&dir).unwrap();
881
+
882
+ let args = format!(r#"{{"path": "{}"}}"#, dir.display());
883
+ let result = exec_delete_file(&args);
884
+ assert!(result.contains("Deleted empty directory"), "got: {result}");
885
+ assert!(!dir.exists());
886
+ }
887
+
888
+ #[test]
889
+ fn test_delete_nonempty_directory() {
890
+ let dir = std::env::temp_dir().join("sigit_test_delete_nonempty_dir");
891
+ let _ = fs::remove_dir_all(&dir);
892
+ fs::create_dir_all(&dir).unwrap();
893
+ fs::write(dir.join("child.txt"), "content").unwrap();
894
+
895
+ let args = format!(r#"{{"path": "{}"}}"#, dir.display());
896
+ let result = exec_delete_file(&args);
897
+ assert!(result.contains("Error"), "got: {result}");
898
+ assert!(dir.exists(), "directory should not have been deleted");
899
+
900
+ let _ = fs::remove_dir_all(&dir);
901
+ }
902
}