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
index f1d05a1..68ba15e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5418,7 +5418,7 @@ version = "0.3.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c236db19d4cea01cf48b584838fcc898745514c8e01c1c0cad0cba976cabefb"
dependencies = [
- "clap 4.6.0",
+ "clap 4.6.1",
"log",
"reqwest 0.12.28",
"serde",
src/chat.rs
+3
-1
index 99c2108..b09acaf 100644
--- a/src/chat.rs
+++ b/src/chat.rs
@@ -17,7 +17,9 @@ use std::sync::mpsc as std_mpsc;
use anyhow::Result;
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use futures::StreamExt;
-use onde::inference::{ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult};
+use onde::inference::{
+ ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult,
+};
use ratatui::{
Frame,
layout::{Constraint, Layout, Position},
src/main.rs
+4
-3
index e850bed..f14d992 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -66,9 +66,10 @@ You help developers build, debug, and ship software on the smbCloud platform.
Keep answers short. Write idiomatic code. \
Fix root causes, not symptoms.
-You have access to tools that let you read files, list directories, and search \
-code. Use them proactively to understand the codebase before answering questions \
-or writing code. Always ground your answers in the actual code.
+You have access to tools that let you read files, list directories, search \
+code, create new files, edit existing files, and delete files. Use them \
+proactively to understand the codebase before answering questions or writing \
+code. Always ground your answers in the actual code.
Be direct and brief. Write clean, idiomatic code. When debugging, go for the \
root cause, not the symptom. Correct beats clever.";
src/tools.rs
+123
-2
index 241521d..33cd8a0 100644
--- a/src/tools.rs
+++ b/src/tools.rs
@@ -1,4 +1,4 @@
-//! Tool definitions and execution for the siGit coding agent.
+//! Tool definitions and execution for the siGit Code.
//!
//! Each tool has:
//! - A schema (JSON Schema) that describes its parameters for the LLM
@@ -16,6 +16,7 @@
//!
//! - `create_file` — create a new file (fails if it already exists)
//! - `edit_file` — replace an exact old-text span with new text in an existing file
+//! - `delete_file` — delete a file or empty directory at the given path
use regex::Regex;
use serde_json::{Value, json};
@@ -147,6 +148,23 @@ pub fn all_tools() -> Vec<AgentTool> {
"additionalProperties": false
}),
},
+ AgentTool {
+ name: "delete_file",
+ description: "Delete a file or empty directory at the given path. \
+ Refuses to delete non-empty directories to prevent accidental data loss. \
+ Use read_file or list_directory first to confirm the target.",
+ parameters_schema: json!({
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "description": "Absolute or relative path to the file or empty directory to delete."
+ }
+ },
+ "required": ["path"],
+ "additionalProperties": false
+ }),
+ },
]
}
@@ -163,6 +181,7 @@ pub fn execute_tool(name: &str, arguments: &str) -> String {
"search_files" => exec_search_files(arguments),
"create_file" => exec_create_file(arguments),
"edit_file" => exec_edit_file(arguments),
+ "delete_file" => exec_delete_file(arguments),
_ => format!("Unknown tool: {name}"),
}
}
@@ -499,6 +518,45 @@ fn exec_edit_file(arguments: &str) -> String {
}
}
+// ── delete_file ──────────────────────────────────────────────────────────────
+
+/// Delete a file or empty directory at the given path.
+///
+/// Refuses to remove non-empty directories to guard against accidental
+/// recursive deletes.
+fn exec_delete_file(arguments: &str) -> String {
+ let args: Value = match serde_json::from_str(arguments) {
+ Ok(v) => v,
+ Err(err) => return format!("Error: failed to parse arguments: {err}"),
+ };
+
+ let path_str = match args.get("path").and_then(Value::as_str) {
+ Some(p) => p,
+ None => return "Error: missing required parameter \"path\"".to_string(),
+ };
+
+ let path = Path::new(path_str);
+
+ if !path.exists() {
+ return format!("Error: path does not exist: {path_str}");
+ }
+
+ if path.is_dir() {
+ match fs::remove_dir(path) {
+ Ok(()) => format!("Deleted empty directory: {path_str}"),
+ Err(err) => format!(
+ "Error: could not delete directory: {err}. \
+ Only empty directories can be deleted."
+ ),
+ }
+ } else {
+ match fs::remove_file(path) {
+ Ok(()) => format!("Deleted file: {path_str}"),
+ Err(err) => format!("Error: could not delete file: {err}"),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -613,12 +671,13 @@ mod tests {
#[test]
fn test_all_tools_count() {
let tools = all_tools();
- assert_eq!(tools.len(), 5);
+ assert_eq!(tools.len(), 6);
assert_eq!(tools[0].name, "read_file");
assert_eq!(tools[1].name, "list_directory");
assert_eq!(tools[2].name, "search_files");
assert_eq!(tools[3].name, "create_file");
assert_eq!(tools[4].name, "edit_file");
+ assert_eq!(tools[5].name, "delete_file");
}
#[test]
@@ -778,4 +837,66 @@ mod tests {
let _ = fs::remove_dir_all(&dir);
}
+
+ // ── delete_file tests ────────────────────────────────────────────────
+
+ #[test]
+ fn test_delete_file_missing_path() {
+ let result = exec_delete_file("{}");
+ assert!(
+ result.contains("missing required parameter"),
+ "got: {result}"
+ );
+ }
+
+ #[test]
+ fn test_delete_file_nonexistent() {
+ let result = exec_delete_file(r#"{"path": "/tmp/sigit_test_no_such_file_xyz"}"#);
+ assert!(result.contains("does not exist"), "got: {result}");
+ }
+
+ #[test]
+ fn test_delete_file_success() {
+ let dir = std::env::temp_dir().join("sigit_test_delete_file");
+ let _ = fs::remove_dir_all(&dir);
+ fs::create_dir_all(&dir).unwrap();
+
+ let file_path = dir.join("to_delete.txt");
+ fs::write(&file_path, "bye").unwrap();
+ assert!(file_path.exists());
+
+ let args = format!(r#"{{"path": "{}"}}"#, file_path.display());
+ let result = exec_delete_file(&args);
+ assert!(result.contains("Deleted file"), "got: {result}");
+ assert!(!file_path.exists());
+
+ let _ = fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn test_delete_empty_directory() {
+ let dir = std::env::temp_dir().join("sigit_test_delete_empty_dir");
+ let _ = fs::remove_dir_all(&dir);
+ fs::create_dir_all(&dir).unwrap();
+
+ let args = format!(r#"{{"path": "{}"}}"#, dir.display());
+ let result = exec_delete_file(&args);
+ assert!(result.contains("Deleted empty directory"), "got: {result}");
+ assert!(!dir.exists());
+ }
+
+ #[test]
+ fn test_delete_nonempty_directory() {
+ let dir = std::env::temp_dir().join("sigit_test_delete_nonempty_dir");
+ let _ = fs::remove_dir_all(&dir);
+ fs::create_dir_all(&dir).unwrap();
+ fs::write(dir.join("child.txt"), "content").unwrap();
+
+ let args = format!(r#"{{"path": "{}"}}"#, dir.display());
+ let result = exec_delete_file(&args);
+ assert!(result.contains("Error"), "got: {result}");
+ assert!(dir.exists(), "directory should not have been deleted");
+
+ let _ = fs::remove_dir_all(&dir);
+ }
}