llm_coding_tools_serdesai/absolute/
write.rs

1//! Write file tool using [`AbsolutePathResolver`].
2
3use async_trait::async_trait;
4use llm_coding_tools_core::operations::write_file;
5use llm_coding_tools_core::path::AbsolutePathResolver;
6use llm_coding_tools_core::tool_names;
7use llm_coding_tools_core::{ToolContext, ToolOutput};
8use serde::Deserialize;
9use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult};
10
11use crate::convert::to_serdes_result;
12
13/// Internal args for JSON deserialization.
14#[derive(Debug, Deserialize)]
15struct WriteArgs {
16    /// Absolute path to the file.
17    file_path: String,
18    /// Content to write to the file.
19    content: String,
20}
21
22/// Tool for writing content to files.
23///
24/// Creates parent directories if needed and overwrites existing files.
25#[derive(Debug, Clone, Default)]
26pub struct WriteTool;
27
28impl WriteTool {
29    /// Creates a new write tool instance.
30    #[inline]
31    pub fn new() -> Self {
32        Self
33    }
34}
35
36#[async_trait]
37impl<Deps: Send + Sync> Tool<Deps> for WriteTool {
38    fn definition(&self) -> ToolDefinition {
39        let schema = SchemaBuilder::new()
40            .string("file_path", "Absolute path to the file", true)
41            .string("content", "Content to write to the file", true)
42            .build()
43            .expect("schema build should not fail");
44
45        ToolDefinition::new(
46            tool_names::WRITE,
47            "Write content to a file, creating parent directories if needed. Overwrites existing files.",
48        )
49        .with_parameters(schema)
50    }
51
52    async fn call(&self, _ctx: &RunContext<Deps>, args: serde_json::Value) -> ToolResult {
53        let args: WriteArgs = serde_json::from_value(args)
54            .map_err(|e| ToolError::validation_error(tool_names::WRITE, None, e.to_string()))?;
55
56        let resolver = AbsolutePathResolver;
57        let result = write_file(&resolver, &args.file_path, &args.content).await;
58
59        // Convert String result to ToolOutput for consistent error handling
60        to_serdes_result(tool_names::WRITE, result.map(ToolOutput::new))
61    }
62}
63
64impl ToolContext for WriteTool {
65    const NAME: &'static str = tool_names::WRITE;
66
67    fn context(&self) -> &'static str {
68        llm_coding_tools_core::context::WRITE_ABSOLUTE
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use serde_json::json;
76    use serdes_ai::tools::RunContext;
77    use tempfile::TempDir;
78
79    fn mock_ctx() -> RunContext<()> {
80        RunContext::new((), "test-model")
81    }
82
83    #[tokio::test]
84    async fn writes_file() {
85        let temp = TempDir::new().unwrap();
86        let file_path = temp.path().join("new.txt");
87        let tool = WriteTool::new();
88
89        let result = tool
90            .call(
91                &mock_ctx(),
92                json!({
93                    "file_path": file_path.to_string_lossy(),
94                    "content": "hello world"
95                }),
96            )
97            .await
98            .unwrap();
99
100        let text = result.as_text().unwrap();
101        assert!(text.contains("11 bytes"));
102        assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "hello world");
103    }
104}