1use std::sync::Arc;
11
12use async_trait::async_trait;
13use futures::future::BoxFuture;
14use futures::FutureExt;
15use rpi_agent::agent_tool::AgentTool;
16use rpi_agent::error::AgentError;
17use rpi_agent::types::{AgentToolResult, TextContentOrImage, ToolResultPartial};
18use rpi_ai::types::{ConstrainedSamplingConfig, ConstrainedStrictness, Tool};
19use schemars::JsonSchema;
20use serde::Deserialize;
21use tokio_util::sync::CancellationToken;
22
23use crate::env::FileContent;
24use crate::error::{FileError, FileErrorCode};
25use crate::file_mutation_queue::{with_file_mutation_queue, MutatingEnv};
26use crate::path_utils::resolve_tool_path;
27use crate::tools::tool_context::ExecutionToolContext;
28
29#[derive(Debug, Clone, JsonSchema, Deserialize)]
31pub struct WriteInput {
32 pub path: String,
34 pub content: String,
36}
37
38pub struct WriteTool {
40 schema: Tool,
41 env: Arc<dyn MutatingEnv>,
42}
43
44impl WriteTool {
45 fn schema() -> Tool {
46 let params = schemars::schema_for!(WriteInput);
47 Tool {
48 name: "write".to_string(),
49 description: "Write content to a file. Creates the file if it doesn't exist, \
50 overwrites if it does. Automatically creates parent directories."
51 .to_string(),
52 parameters: rpi_ai::types::Schema::new(
53 serde_json::to_value(params).unwrap_or_default(),
54 ),
55 constrained_sampling: Some(ConstrainedSamplingConfig::JsonSchema {
56 strict: ConstrainedStrictness::Prefer,
57 }),
58 }
59 }
60}
61
62pub fn create_write_tool(context: &ExecutionToolContext) -> Arc<dyn AgentTool> {
65 let env = context
66 .mutating_env()
67 .expect("write tool requires a mutation-capable ExecutionToolContext")
68 .clone();
69 Arc::new(WriteTool {
70 schema: WriteTool::schema(),
71 env,
72 })
73}
74
75#[async_trait]
76impl AgentTool for WriteTool {
77 fn schema(&self) -> &Tool {
78 &self.schema
79 }
80 fn label(&self) -> &str {
81 "write"
82 }
83
84 async fn execute(
85 &self,
86 _tool_call_id: &str,
87 params: serde_json::Value,
88 signal: CancellationToken,
89 _on_update: Arc<dyn Fn(ToolResultPartial) + Send + Sync>,
90 ) -> Result<AgentToolResult, AgentError> {
91 let input: WriteInput = serde_json::from_value(params)
92 .map_err(|e| AgentError::Validation(format!("write input invalid: {e}")))?;
93
94 let abs = resolve_tool_path(self.env.as_env(), &input.path, Some(&signal))
95 .await
96 .map_err(file_err_to_agent)?;
97 let abs_str = abs.clone();
98 let path_for_msg = input.path.clone();
99
100 let content = input.content;
103 let env = self.env.clone();
104 let env_ref = env.clone();
108 let abs_for_closure = abs_str.clone();
109 let result: Result<(), FileError> = with_file_mutation_queue(
110 &*env_ref,
111 &abs_str,
112 &signal,
113 move |cancel: &CancellationToken| -> BoxFuture<'_, Result<(), FileError>> {
114 let env = env.clone();
115 let content = content.clone();
116 let abs_str = abs_for_closure.clone();
117 async move {
118 check_abort(cancel)?;
119 env.as_env()
120 .write_file(&abs_str, FileContent::Text(content), Some(cancel))
121 .await?;
122 check_abort(cancel)?;
123 Ok(())
124 }
125 .boxed()
126 },
127 )
128 .await;
129
130 result.map_err(file_err_to_agent)?;
131
132 Ok(AgentToolResult {
133 content: vec![TextContentOrImage::text(format!(
134 "Successfully wrote to {path_for_msg}"
135 ))],
136 details: serde_json::Value::Null,
137 ..Default::default()
138 })
139 }
140}
141
142fn check_abort(cancel: &CancellationToken) -> Result<(), FileError> {
145 if cancel.is_cancelled() {
146 return Err(FileError::new(FileErrorCode::Aborted, "Operation aborted"));
147 }
148 Ok(())
149}
150
151fn file_err_to_agent(e: FileError) -> AgentError {
152 AgentError::Tool(e.to_string())
153}