llm_coding_tools_rig/absolute/
edit.rs

1//! Edit file tool using [`AbsolutePathResolver`].
2
3use llm_coding_tools_core::operations::edit_file;
4use llm_coding_tools_core::path::AbsolutePathResolver;
5use llm_coding_tools_core::tool_names;
6pub use llm_coding_tools_core::EditError;
7use llm_coding_tools_core::ToolContext;
8use rig::completion::ToolDefinition;
9use rig::tool::Tool;
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12
13/// Arguments for file editing.
14#[derive(Debug, Clone, Deserialize, JsonSchema)]
15pub struct EditArgs {
16    /// Absolute path to the file to modify.
17    pub file_path: String,
18    /// Exact text to find and replace.
19    pub old_string: String,
20    /// Replacement text.
21    pub new_string: String,
22    /// Replace all occurrences (default false).
23    #[serde(default)]
24    pub replace_all: bool,
25}
26
27/// Tool for making exact string replacements in files.
28#[derive(Debug, Clone, Default)]
29pub struct EditTool;
30
31impl EditTool {
32    /// Creates a new edit tool instance.
33    #[inline]
34    pub fn new() -> Self {
35        Self
36    }
37}
38
39impl Tool for EditTool {
40    const NAME: &'static str = tool_names::EDIT;
41
42    type Error = EditError;
43    type Args = EditArgs;
44    type Output = String;
45
46    async fn definition(&self, _prompt: String) -> ToolDefinition {
47        ToolDefinition {
48            name: <Self as Tool>::NAME.to_string(),
49            description: "Makes exact string replacements in files. Use replace_all=true to \
50                           replace all occurrences."
51                .to_string(),
52            parameters: serde_json::to_value(schema_for!(EditArgs))
53                .expect("EditArgs schema generation should not fail"),
54        }
55    }
56
57    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
58        let resolver = AbsolutePathResolver;
59        edit_file(
60            &resolver,
61            &args.file_path,
62            &args.old_string,
63            &args.new_string,
64            args.replace_all,
65        )
66        .await
67    }
68}
69
70impl ToolContext for EditTool {
71    const NAME: &'static str = tool_names::EDIT;
72
73    fn context(&self) -> &'static str {
74        llm_coding_tools_core::context::EDIT_ABSOLUTE
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use llm_coding_tools_core::ToolError;
82    use std::io::Write as _;
83    use tempfile::NamedTempFile;
84
85    #[tokio::test]
86    async fn replaces_single_occurrence() {
87        let mut file = NamedTempFile::new().unwrap();
88        file.write_all(b"hello world").unwrap();
89        file.flush().unwrap();
90        let tool = EditTool::new();
91        let result = tool
92            .call(EditArgs {
93                file_path: file.path().to_string_lossy().to_string(),
94                old_string: "world".to_string(),
95                new_string: "rust".to_string(),
96                replace_all: false,
97            })
98            .await
99            .unwrap();
100        assert!(result.contains("1 occurrence"));
101    }
102
103    #[tokio::test]
104    async fn rejects_relative_path() {
105        let tool = EditTool::new();
106        let result = tool
107            .call(EditArgs {
108                file_path: "relative/path.txt".to_string(),
109                old_string: "old".to_string(),
110                new_string: "new".to_string(),
111                replace_all: false,
112            })
113            .await;
114        assert!(matches!(
115            result,
116            Err(EditError::Tool(ToolError::InvalidPath(_)))
117        ));
118    }
119}