Skip to main content

lash_tools/files/
write.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::Path;
4
5use lash_core::{ToolCall, ToolDefinition, ToolResult, ToolScheduling};
6
7use lash_tool_support::{
8    StaticToolExecute, StaticToolProvider, ToolDefinitionLashlangExt, display_relative,
9    execute_typed_tool_result, non_empty_string, resolve_under, run_blocking,
10};
11
12const WRITE_DESCRIPTION: &str = "Write content to a file. Creates the file if it does not exist, overwrites if it does. Automatically creates parent directories. Use write only for new files or complete rewrites.";
13
14#[derive(Default)]
15pub struct Write;
16
17pub fn write_provider() -> StaticToolProvider<Write> {
18    StaticToolProvider::new(vec![write_tool_definition()], Write)
19}
20
21#[derive(Clone, Debug, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23struct WriteArgs {
24    /// Path to the file to write (relative or absolute).
25    path: String,
26    /// Content to write to the file.
27    content: String,
28}
29
30#[derive(Clone, Debug, Serialize, JsonSchema)]
31struct WriteOutput {
32    summary: String,
33    path: String,
34    bytes: usize,
35}
36
37#[async_trait::async_trait]
38impl StaticToolExecute for Write {
39    async fn execute(&self, call: ToolCall<'_>) -> ToolResult {
40        execute_typed_tool_result::<WriteArgs, _, _>(call.args, |args| async move {
41            if let Err(err) = non_empty_string(&args.path, "path") {
42                return err;
43            }
44            run_blocking(move || write_file(args)).await
45        })
46        .await
47    }
48}
49
50fn write_tool_definition() -> ToolDefinition {
51    ToolDefinition::typed::<WriteArgs, WriteOutput>("tool:write", "write", WRITE_DESCRIPTION)
52        .with_examples(vec![
53            r#"await files.write({ path: "hello.txt", content: "hello\n" })?"#.into(),
54            r#"await files.write({ path: "src/main.rs", content: "fn main() {}\n" })?"#.into(),
55        ])
56        .with_lashlang_binding(lash_tool_support::lashlang_binding(
57            ["files"],
58            "write",
59            &["write_file"],
60        ))
61        .with_scheduling(ToolScheduling::Serial)
62}
63
64fn write_file(args: WriteArgs) -> ToolResult {
65    let cwd = match std::env::current_dir() {
66        Ok(cwd) => cwd,
67        Err(err) => return ToolResult::err_fmt(format_args!("Failed to determine cwd: {err}")),
68    };
69    let absolute_path = resolve_under(&cwd, Path::new(&args.path));
70    if let Some(parent) = absolute_path.parent()
71        && let Err(err) = std::fs::create_dir_all(parent)
72    {
73        return ToolResult::err_fmt(format_args!("Could not write file: {}. {err}.", args.path));
74    }
75    if let Err(err) = std::fs::write(&absolute_path, &args.content) {
76        return ToolResult::err_fmt(format_args!("Could not write file: {}. {err}.", args.path));
77    }
78
79    let display_path = display_relative(&cwd, &absolute_path);
80    let bytes = args.content.len();
81    lash_tool_support::typed_tool_ok(WriteOutput {
82        summary: format!("Successfully wrote {bytes} bytes to {display_path}."),
83        path: args.path,
84        bytes,
85    })
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use serde_json::json;
92    use tempfile::TempDir;
93
94    fn run_write(dir: &TempDir, path: &str, content: &str) -> ToolResult {
95        let path = dir.path().join(path).to_string_lossy().to_string();
96        write_file(WriteArgs {
97            path,
98            content: content.to_string(),
99        })
100    }
101
102    #[test]
103    fn write_contract_documents_pi_shape() {
104        let definition = write_tool_definition();
105        let rendered = definition.compact_contract().render_signature();
106
107        assert!(rendered.contains("path"), "{rendered}");
108        assert!(rendered.contains("content"), "{rendered}");
109        assert!(
110            definition
111                .manifest()
112                .description
113                .contains("Creates the file if it does not exist")
114        );
115    }
116
117    #[test]
118    fn write_creates_parent_directories_and_file() {
119        let dir = TempDir::new().unwrap();
120
121        let result = run_write(&dir, "nested/hello.txt", "hello\n");
122
123        assert!(result.is_success(), "{}", result.value_for_projection());
124        assert_eq!(
125            std::fs::read_to_string(dir.path().join("nested/hello.txt")).unwrap(),
126            "hello\n"
127        );
128        assert_eq!(result.value_for_projection()["bytes"], json!(6));
129    }
130
131    #[test]
132    fn write_overwrites_existing_file() {
133        let dir = TempDir::new().unwrap();
134        std::fs::write(dir.path().join("hello.txt"), "old\n").unwrap();
135
136        let result = run_write(&dir, "hello.txt", "new\n");
137
138        assert!(result.is_success(), "{}", result.value_for_projection());
139        assert_eq!(
140            std::fs::read_to_string(dir.path().join("hello.txt")).unwrap(),
141            "new\n"
142        );
143    }
144}