1use crate::types::*;
22use async_trait::async_trait;
23use base64::Engine;
24use std::path::Path;
25
26const MAX_IMAGE_SIZE_BYTES: u64 = 20 * 1024 * 1024;
28
29fn is_image_file(path: &Path) -> bool {
48 matches!(
49 path.extension()
50 .and_then(|e| e.to_str())
51 .map(|e| e.to_lowercase())
52 .as_deref(),
53 Some("jpg" | "jpeg" | "png" | "webp" | "gif" | "bmp")
54 )
55}
56
57fn get_image_mime_type(path: &Path) -> Option<&'static str> {
67 match path
68 .extension()
69 .and_then(|e| e.to_str())
70 .map(|e| e.to_lowercase())
71 .as_deref()
72 {
73 Some("jpg" | "jpeg") => Some("image/jpeg"),
74 Some("png") => Some("image/png"),
75 Some("webp") => Some("image/webp"),
76 Some("gif") => Some("image/gif"),
77 Some("bmp") => Some("image/bmp"),
78 _ => None,
79 }
80}
81
82pub struct ReadFileTool {
84 pub max_bytes: usize,
86 pub allowed_paths: Vec<String>,
88}
89
90impl Default for ReadFileTool {
91 fn default() -> Self {
92 Self {
93 max_bytes: 1024 * 1024, allowed_paths: Vec::new(),
95 }
96 }
97}
98
99impl ReadFileTool {
100 pub fn new() -> Self {
101 Self::default()
102 }
103}
104
105#[async_trait]
106impl AgentTool for ReadFileTool {
107 fn name(&self) -> &str {
108 "read_file"
109 }
110
111 fn label(&self) -> &str {
112 "Read File"
113 }
114
115 fn description(&self) -> &str {
116 "Read a file's contents. Supports text files with optional offset/limit, and image files (jpg, png, webp, gif, bmp) which are returned as base64-encoded images."
117 }
118
119 fn parameters_schema(&self) -> serde_json::Value {
120 serde_json::json!({
121 "type": "object",
122 "properties": {
123 "path": {
124 "type": "string",
125 "description": "File path to read"
126 },
127 "offset": {
128 "type": "integer",
129 "description": "Starting line number (1-indexed, optional)"
130 },
131 "limit": {
132 "type": "integer",
133 "description": "Maximum number of lines to return (optional)"
134 }
135 },
136 "required": ["path"]
137 })
138 }
139
140 async fn execute(
141 &self,
142 params: serde_json::Value, ctx: ToolContext, ) -> Result<ToolResult, ToolError> {
145 let path = params["path"]
146 .as_str()
147 .ok_or_else(|| ToolError::InvalidArgs("missing 'path' parameter".into()))?;
148
149 if ctx.cancel.is_cancelled() {
150 return Err(ToolError::Cancelled);
151 }
152
153 let metadata = tokio::fs::metadata(path)
155 .await
156 .map_err(|e| ToolError::Failed(format!("Cannot access {}: {}", path, e)))?;
157
158 let file_path = Path::new(path);
160 if is_image_file(file_path) {
161 if metadata.len() > MAX_IMAGE_SIZE_BYTES {
162 return Err(ToolError::Failed(format!(
163 "Image too large ({}MB, max 20MB)",
164 metadata.len() / (1024 * 1024)
165 )));
166 }
167 let mime_type = get_image_mime_type(file_path)
168 .ok_or_else(|| ToolError::Failed("Unknown image format".into()))?;
169 let bytes = tokio::fs::read(path)
170 .await
171 .map_err(|e| ToolError::Failed(format!("Cannot read {}: {}", path, e)))?;
172 let data = base64::engine::general_purpose::STANDARD.encode(&bytes);
173 return Ok(ToolResult {
174 content: vec![Content::Image {
175 data,
176 mime_type: mime_type.to_string(),
177 }],
178 details: serde_json::json!({ "path": path, "bytes": bytes.len() }),
179 child_loop_id: None,
180 });
181 }
182
183 if metadata.len() as usize > self.max_bytes {
185 return Err(ToolError::Failed(format!(
186 "File too large ({} bytes, max {}). Use offset/limit for partial reads.",
187 metadata.len(),
188 self.max_bytes
189 )));
190 }
191
192 let content = tokio::fs::read_to_string(path)
193 .await
194 .map_err(|e| ToolError::Failed(format!("Cannot read {}: {}", path, e)))?;
195
196 let offset = params["offset"].as_u64().map(|v| v.max(1) as usize);
197 let limit = params["limit"].as_u64().map(|v| v as usize);
198
199 let lines: Vec<&str> = content.lines().collect();
201 let total = lines.len();
202
203 let (start, end) = match (offset, limit) {
204 (Some(off), Some(lim)) => {
205 let s = (off - 1).min(total);
206 (s, (s + lim).min(total))
207 }
208 (Some(off), None) => {
209 let s = (off - 1).min(total);
210 (s, total)
211 }
212 (None, Some(lim)) => (0, lim.min(total)),
213 (None, None) => (0, total),
214 };
215
216 let numbered: Vec<String> = lines[start..end]
217 .iter()
218 .enumerate()
219 .map(|(i, line)| format!("{:>4} | {}", start + i + 1, line))
220 .collect();
221
222 let header = if start > 0 || end < total {
223 format!("[Lines {}-{} of {}]", start + 1, end, total)
224 } else {
225 format!("[{} lines]", total)
226 };
227
228 let output = format!("{}\n{}", header, numbered.join("\n"));
229
230 Ok(ToolResult {
231 content: vec![Content::Text { text: output }],
232 details: serde_json::json!({ "path": path }),
233 child_loop_id: None,
234 })
235 }
236}
237
238pub struct WriteFileTool;
242
243impl Default for WriteFileTool {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl WriteFileTool {
250 pub fn new() -> Self {
251 Self
252 }
253}
254
255#[async_trait]
256impl AgentTool for WriteFileTool {
257 fn name(&self) -> &str {
258 "write_file"
259 }
260
261 fn label(&self) -> &str {
262 "Write File"
263 }
264
265 fn description(&self) -> &str {
266 "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Creates parent directories automatically."
267 }
268
269 fn parameters_schema(&self) -> serde_json::Value {
270 serde_json::json!({
271 "type": "object",
272 "properties": {
273 "path": {
274 "type": "string",
275 "description": "File path to write"
276 },
277 "content": {
278 "type": "string",
279 "description": "Content to write to the file"
280 }
281 },
282 "required": ["path", "content"]
283 })
284 }
285
286 async fn execute(
287 &self,
288 params: serde_json::Value, ctx: ToolContext, ) -> Result<ToolResult, ToolError> {
291 let path = params["path"]
292 .as_str()
293 .ok_or_else(|| ToolError::InvalidArgs("missing 'path' parameter".into()))?;
294 let content = params["content"]
295 .as_str()
296 .ok_or_else(|| ToolError::InvalidArgs("missing 'content' parameter".into()))?;
297
298 if ctx.cancel.is_cancelled() {
299 return Err(ToolError::Cancelled);
300 }
301
302 if let Some(parent) = std::path::Path::new(path).parent() {
304 if !parent.exists() {
305 tokio::fs::create_dir_all(parent)
306 .await
307 .map_err(|e| ToolError::Failed(format!("Cannot create directory: {}", e)))?;
308 }
309 }
310
311 tokio::fs::write(path, content)
312 .await
313 .map_err(|e| ToolError::Failed(format!("Cannot write {}: {}", path, e)))?;
314
315 let bytes = content.len();
316 Ok(ToolResult {
317 content: vec![Content::Text {
318 text: format!("Wrote {} bytes to {}", bytes, path),
319 }],
320 details: serde_json::json!({ "path": path, "bytes": bytes }),
321 child_loop_id: None,
322 })
323 }
324}