1use std::path::PathBuf;
4use std::sync::Arc;
5
6use rig_core::completion::ToolAnnotations;
7use rig_core::tool::{
8 FilesChanged, Scope, Tool, ToolContext, ToolExecutionError, Touches, args_schema,
9};
10use schemars::JsonSchema;
11use serde::Deserialize;
12
13use crate::blocking;
14use crate::fs::{Previous, io_error};
15use crate::workspace::{Local, Stat, Workspace};
16
17#[derive(Clone, Debug)]
21pub struct Delete {
22 pub workspace: Arc<dyn Workspace>,
24 pub base: Option<PathBuf>,
27 pub confine: bool,
30}
31
32impl Default for Delete {
33 fn default() -> Self {
35 Self {
36 workspace: Arc::new(Local),
37 base: None,
38 confine: false,
39 }
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
45#[serde(deny_unknown_fields)]
46pub struct DeleteArgs {
47 pub path: String,
49}
50
51impl DeleteArgs {
52 pub fn new(path: impl Into<String>) -> Self {
54 Self { path: path.into() }
55 }
56}
57
58impl Tool for Delete {
59 const NAME: &'static str = "delete";
60 type Args = DeleteArgs;
61 type Output = String;
62 type Error = ToolExecutionError;
63
64 fn description(&self) -> String {
65 "Delete a file, not with `rm` in `shell`. Directories are refused.".to_owned()
66 }
67
68 fn parameters(&self) -> serde_json::Value {
69 args_schema::<DeleteArgs>()
70 }
71
72 fn annotations(&self) -> ToolAnnotations {
73 ToolAnnotations::default()
74 .with_destructive(true)
75 .with_idempotent(false)
76 .with_open_world(false)
77 }
78
79 fn footprint(&self, args: &DeleteArgs) -> Touches {
80 Touches::Writes(Scope::Paths(vec![
81 self.workspace.resolve(self.base.as_deref(), &args.path),
82 ]))
83 }
84
85 async fn call(
86 &self,
87 context: &mut ToolContext,
88 args: DeleteArgs,
89 ) -> Result<String, ToolExecutionError> {
90 let resolved = self
91 .workspace
92 .place(self.base.as_deref(), self.confine, &args.path)?;
93 let workspace = self.workspace.clone();
94 let change = blocking(move || {
95 let path = resolved.to_string_lossy();
96 match workspace
97 .stat(&resolved)
98 .map_err(|error| io_error(&path, error))?
99 {
100 Stat::File { .. } => {}
101 Stat::Missing => {
102 return Err(ToolExecutionError::not_found(format!(
103 "{path}: no such file"
104 )));
105 }
106 Stat::Dir | Stat::Other => {
107 return Err(ToolExecutionError::invalid_args(format!(
108 "{path} is not a regular file; `delete` removes files only"
109 )));
110 }
111 }
112 let previous = workspace.previous(&resolved).unwrap_or_default();
113 workspace
114 .remove(&resolved)
115 .map_err(|error| io_error(&path, error))?;
116 Ok(Previous::change(Some(previous), &path, None))
117 })
118 .await?;
119 context.insert_result(FilesChanged(vec![change]))?;
120 Ok(format!("Deleted {}.", args.path))
121 }
122}
123
124#[cfg(test)]
125mod tests;