llm_coding_tools_rig/absolute/
write.rs

1//! Write file tool using [`AbsolutePathResolver`].
2
3use llm_coding_tools_core::operations::write_file;
4use llm_coding_tools_core::path::AbsolutePathResolver;
5use llm_coding_tools_core::tool_names;
6use llm_coding_tools_core::{ToolContext, ToolError};
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use schemars::{schema_for, JsonSchema};
10use serde::Deserialize;
11
12/// Arguments for the write tool.
13#[derive(Debug, Clone, Deserialize, JsonSchema)]
14pub struct WriteToolArgs {
15    /// Absolute path for the file to write.
16    pub file_path: String,
17    /// Content to write to the file.
18    pub content: String,
19}
20
21/// Tool for writing content to files.
22#[derive(Debug, Clone, Default)]
23pub struct WriteTool;
24
25impl WriteTool {
26    /// Creates a new write tool instance.
27    #[inline]
28    pub fn new() -> Self {
29        Self
30    }
31}
32
33impl Tool for WriteTool {
34    const NAME: &'static str = tool_names::WRITE;
35
36    type Error = ToolError;
37    type Args = WriteToolArgs;
38    type Output = String;
39
40    async fn definition(&self, _prompt: String) -> ToolDefinition {
41        ToolDefinition {
42            name: <Self as Tool>::NAME.to_string(),
43            description: "Write content to a file, creating parent directories if needed. \
44                           Overwrites existing files."
45                .to_string(),
46            parameters: serde_json::to_value(schema_for!(WriteToolArgs))
47                .expect("schema generation should not fail"),
48        }
49    }
50
51    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
52        let resolver = AbsolutePathResolver;
53        write_file(&resolver, &args.file_path, &args.content).await
54    }
55}
56
57impl ToolContext for WriteTool {
58    const NAME: &'static str = tool_names::WRITE;
59
60    fn context(&self) -> &'static str {
61        llm_coding_tools_core::context::WRITE_ABSOLUTE
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use tempfile::TempDir;
69
70    #[tokio::test]
71    async fn writes_new_file() {
72        let temp = TempDir::new().unwrap();
73        let file_path = temp.path().join("new.txt");
74        let tool = WriteTool::new();
75        let result = tool
76            .call(WriteToolArgs {
77                file_path: file_path.to_string_lossy().to_string(),
78                content: "hello".to_string(),
79            })
80            .await
81            .unwrap();
82        assert!(result.contains("5 bytes"));
83    }
84
85    #[tokio::test]
86    async fn rejects_relative_path() {
87        let tool = WriteTool::new();
88        let result = tool
89            .call(WriteToolArgs {
90                file_path: "relative/path.txt".to_string(),
91                content: "content".to_string(),
92            })
93            .await;
94        assert!(matches!(result, Err(ToolError::InvalidPath(_))));
95    }
96}