opi_coding_agent/tool/
write.rs1use 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 pub path: String,
15 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 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}