swink_agent/tools/
write_file.rs1use 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
11pub struct WriteFileTool {
14 schema: Value,
15}
16
17impl WriteFileTool {
18 #[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 path: String,
38 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 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}