Skip to main content

rig_tools/
delete.rs

1//! The `delete` tool.
2
3use 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/// Deletes a file; a directory is refused. Publishes the file it deleted
18/// ([`FilesChanged`]): its [resolved](Workspace) path, and how many
19/// lines it had.
20#[derive(Clone, Debug)]
21pub struct Delete {
22    /// Where the file is deleted.
23    pub workspace: Arc<dyn Workspace>,
24    /// The directory a relative path is deleted in; `None` is the
25    /// workspace's working directory.
26    pub base: Option<PathBuf>,
27    /// Whether a path that resolves outside `base` is refused, before
28    /// anything is deleted.
29    pub confine: bool,
30}
31
32impl Default for Delete {
33    /// Deletes this machine's files, from this process's working directory.
34    fn default() -> Self {
35        Self {
36            workspace: Arc::new(Local),
37            base: None,
38            confine: false,
39        }
40    }
41}
42
43/// Arguments of [`Delete`].
44#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
45#[serde(deny_unknown_fields)]
46pub struct DeleteArgs {
47    /// The file to delete.
48    pub path: String,
49}
50
51impl DeleteArgs {
52    /// Delete `path`.
53    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;