1use async_trait::async_trait;
2use serde_json::{json, Value};
3use tokio::fs;
4
5use crate::types::{AgentTool, AgentToolResult};
6
7pub struct ReadTool;
8
9#[async_trait]
10impl AgentTool for ReadTool {
11 fn name(&self) -> &str {
12 "read"
13 }
14 fn description(&self) -> &str {
15 "Read the contents of a file from disk. Returns text content with optional line numbers."
16 }
17 fn parameters(&self) -> Value {
18 json!({
19 "type": "object",
20 "properties": {
21 "path": {"type": "string", "description": "Absolute or relative path to the file"},
22 "offset": {"type": "integer", "description": "Line offset (1-based), optional"},
23 "limit": {"type": "integer", "description": "Max number of lines, optional"}
24 },
25 "required": ["path"]
26 })
27 }
28 async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
29 let path = args
30 .get("path")
31 .and_then(|v| v.as_str())
32 .ok_or("missing 'path'")?;
33 let offset = args
34 .get("offset")
35 .and_then(|v| v.as_u64())
36 .map(|v| v as usize);
37 let limit = args
38 .get("limit")
39 .and_then(|v| v.as_u64())
40 .map(|v| v as usize);
41
42 let text = fs::read_to_string(path)
43 .await
44 .map_err(|e| format!("read {path}: {e}"))?;
45 let lines: Vec<&str> = text.lines().collect();
46 let start = offset.map(|o| o.saturating_sub(1)).unwrap_or(0);
47 let end = limit
48 .map(|l| std::cmp::min(start + l, lines.len()))
49 .unwrap_or(lines.len());
50
51 let mut buf = String::new();
52 for (i, line) in lines[start..end].iter().enumerate() {
53 buf.push_str(&format!("{:>5}\t{}\n", start + i + 1, line));
54 }
55 Ok(AgentToolResult::text(buf))
56 }
57}