Skip to main content

swink_agent/tools/
write_file.rs

1//! Built-in tool for writing content to a file.
2
3use schemars::JsonSchema;
4use serde::Deserialize;
5use serde_json::Value;
6use tokio_util::sync::CancellationToken;
7
8use crate::tool::{AgentTool, AgentToolResult, ToolFuture, validated_schema_for};
9use crate::types::ContentBlock;
10
11/// Built-in tool that writes content to a file, creating parent directories as
12/// needed.
13pub struct WriteFileTool {
14    schema: Value,
15}
16
17impl WriteFileTool {
18    /// Create a new `WriteFileTool`.
19    #[must_use]
20    pub fn new() -> Self {
21        Self {
22            schema: validated_schema_for::<Params>(),
23        }
24    }
25}
26
27impl Default for WriteFileTool {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33#[derive(Deserialize, JsonSchema)]
34#[schemars(deny_unknown_fields)]
35struct Params {
36    /// Absolute path to write.
37    path: String,
38    /// Content to write to the file.
39    content: String,
40}
41
42#[allow(clippy::unnecessary_literal_bound)]
43impl AgentTool for WriteFileTool {
44    fn name(&self) -> &str {
45        "write_file"
46    }
47
48    fn label(&self) -> &str {
49        "Write File"
50    }
51
52    fn description(&self) -> &str {
53        "Write content to a file, creating parent directories if needed."
54    }
55
56    fn parameters_schema(&self) -> &Value {
57        &self.schema
58    }
59
60    fn requires_approval(&self) -> bool {
61        true
62    }
63
64    fn execute(
65        &self,
66        _tool_call_id: &str,
67        params: Value,
68        cancellation_token: CancellationToken,
69        _on_update: Option<Box<dyn Fn(AgentToolResult) + Send + Sync>>,
70        _state: std::sync::Arc<std::sync::RwLock<crate::SessionState>>,
71        _credential: Option<crate::credential::ResolvedCredential>,
72    ) -> ToolFuture<'_> {
73        Box::pin(async move {
74            let parsed: Params = match serde_json::from_value(params) {
75                Ok(p) => p,
76                Err(e) => return AgentToolResult::error(format!("invalid parameters: {e}")),
77            };
78
79            if cancellation_token.is_cancelled() {
80                return AgentToolResult::error("cancelled");
81            }
82
83            let path = std::path::Path::new(&parsed.path);
84
85            // Read existing content for diff (empty string if file doesn't exist)
86            let old_content = tokio::fs::read_to_string(path).await.unwrap_or_default();
87
88            if let Some(parent) = path.parent()
89                && let Err(e) = tokio::fs::create_dir_all(parent).await
90            {
91                return AgentToolResult::error(format!(
92                    "failed to create parent directories for {}: {e}",
93                    parsed.path
94                ));
95            }
96
97            let bytes_written = parsed.content.len();
98            match tokio::fs::write(path, &parsed.content).await {
99                Ok(()) => AgentToolResult {
100                    content: vec![ContentBlock::Text {
101                        text: format!(
102                            "Successfully wrote {bytes_written} bytes to {}",
103                            parsed.path
104                        ),
105                    }],
106                    details: serde_json::json!({
107                        "path": parsed.path,
108                        "bytes_written": bytes_written,
109                        "is_new_file": old_content.is_empty(),
110                        "old_content": old_content,
111                        "new_content": parsed.content,
112                    }),
113                    is_error: false,
114                    transfer_signal: None,
115                },
116                Err(e) => {
117                    AgentToolResult::error(format!("failed to write file {}: {e}", parsed.path))
118                }
119            }
120        })
121    }
122}