Skip to main content

opi_coding_agent/tool/
write.rs

1use std::future::Future;
2use std::path::PathBuf;
3use std::pin::Pin;
4
5use opi_agent::tool::{ExecutionMode, Tool, ToolError, ToolResult};
6use opi_ai::message::{OutputContent, ToolDef};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use tokio_util::sync::CancellationToken;
10
11#[derive(Debug, Deserialize, JsonSchema)]
12pub struct WriteArgs {
13    /// Relative path within workspace to write.
14    pub path: String,
15    /// Content to write.
16    pub content: String,
17}
18
19pub struct WriteTool {
20    workspace_root: PathBuf,
21    schema: serde_json::Value,
22}
23
24impl WriteTool {
25    pub fn new(workspace_root: PathBuf) -> Self {
26        let schema = schemars::schema_for!(WriteArgs);
27        Self {
28            workspace_root,
29            schema: serde_json::to_value(&schema).unwrap_or_default(),
30        }
31    }
32}
33
34impl Tool for WriteTool {
35    fn definition(&self) -> ToolDef {
36        ToolDef {
37            name: "write".into(),
38            description: "Create or replace a file with the given content.".into(),
39            input_schema: self.schema.clone(),
40        }
41    }
42
43    fn execute(
44        &self,
45        _call_id: &str,
46        arguments: serde_json::Value,
47        _signal: CancellationToken,
48        _on_update: Option<opi_agent::tool::UpdateCallback>,
49    ) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send>> {
50        let args: WriteArgs = match serde_json::from_value(arguments) {
51            Ok(a) => a,
52            Err(e) => {
53                return Box::pin(async move {
54                    Ok(ToolResult {
55                        content: vec![OutputContent::Text {
56                            text: format!("invalid arguments: {e}"),
57                        }],
58                        details: None,
59                        is_error: true,
60                        terminate: false,
61                    })
62                });
63            }
64        };
65        let resolved_path = match super::resolve_tool_path(
66            &self.workspace_root,
67            &args.path,
68            super::PathPolicy::WorkspaceOnly,
69        ) {
70            Ok(p) => p,
71            Err(msg) => {
72                return Box::pin(async move {
73                    Ok(ToolResult {
74                        content: vec![OutputContent::Text { text: msg }],
75                        details: None,
76                        is_error: true,
77                        terminate: false,
78                    })
79                });
80            }
81        };
82        let file_path = resolved_path.path;
83        let inside_workspace = resolved_path.inside_workspace;
84        let workspace_root = self.workspace_root.clone();
85        let path_for_display = args.path.clone();
86        Box::pin(async move {
87            // Create parent directories if needed
88            if let Some(parent) = file_path.parent()
89                && let Err(e) = tokio::fs::create_dir_all(parent).await
90            {
91                return Ok(ToolResult {
92                    content: vec![OutputContent::Text {
93                        text: format!("failed to create directories: {e}"),
94                    }],
95                    details: None,
96                    is_error: true,
97                    terminate: false,
98                });
99            }
100
101            if let Err(e) = tokio::fs::write(&file_path, &args.content).await {
102                return Ok(ToolResult {
103                    content: vec![OutputContent::Text {
104                        text: format!("failed to write {}: {e}", file_path.display()),
105                    }],
106                    details: None,
107                    is_error: true,
108                    terminate: false,
109                });
110            }
111
112            let details = serde_json::json!({
113                "workspace_root": workspace_root.to_string_lossy(),
114                "path": path_for_display,
115                "resolved_path": file_path.to_string_lossy(),
116                "inside_workspace": inside_workspace,
117            });
118
119            Ok(ToolResult {
120                content: vec![OutputContent::Text {
121                    text: format!("wrote {}", path_for_display),
122                }],
123                details: Some(details),
124                is_error: false,
125                terminate: false,
126            })
127        })
128    }
129
130    fn execution_mode(&self) -> ExecutionMode {
131        ExecutionMode::Sequential
132    }
133}