Skip to main content

agent_engine/tools/
read.rs

1use super::{expand_path, Tool, ToolContext};
2use crate::{Result, RuntimeError};
3use serde_json::{json, Value};
4
5pub struct ReadTool;
6
7#[async_trait::async_trait]
8impl Tool for ReadTool {
9    fn effect(&self) -> crate::tools::catalog::ToolEffect {
10        crate::tools::catalog::ToolEffect::ReadOnly
11    }
12
13    fn origin(&self) -> crate::tools::ToolOrigin {
14        crate::tools::ToolOrigin::Builtin
15    }
16
17    fn name(&self) -> &str {
18        "read"
19    }
20
21    fn description(&self) -> &str {
22        "Read the contents of a file. Returns lines with line numbers. Reads up to 500 lines by default. For large files, use offset and limit to read in sections."
23    }
24
25    fn parameters(&self) -> Value {
26        json!({
27            "type": "object",
28            "properties": {
29                "path": {
30                    "type": "string",
31                    "description": "Path to the file to read"
32                },
33                "offset": {
34                    "type": "integer",
35                    "description": "Line number to start reading from (0-indexed, default: 0)"
36                },
37                "limit": {
38                    "type": "integer",
39                    "description": "Maximum number of lines to read (default: all lines)"
40                }
41            },
42            "required": ["path"]
43        })
44    }
45
46    async fn execute(&self, params: Value, _ctx: ToolContext) -> Result<String> {
47        let raw_path = params["path"]
48            .as_str()
49            .ok_or_else(|| RuntimeError::Tool("Missing path parameter".to_string()))?;
50        let path = expand_path(raw_path);
51
52        // Read raw bytes first to detect binary files
53        let bytes = tokio::fs::read(&path).await.map_err(|e| {
54            RuntimeError::Tool(format!("Failed to read file '{}': {}", path.display(), e))
55        })?;
56
57        let content = match String::from_utf8(bytes) {
58            Ok(s) => s,
59            Err(_) => return Err(RuntimeError::Tool(format!(
60                "File '{}' appears to be binary (not valid UTF-8). Use `bash` with `xxd` or `file` to inspect binary files.",
61                path.display()
62            ))),
63        };
64
65        let lines: Vec<&str> = content.lines().collect();
66        let total_lines = lines.len();
67
68        let offset = params["offset"].as_u64().unwrap_or(0) as usize;
69        let limit = params["limit"]
70            .as_u64()
71            .map(|l| l as usize)
72            .unwrap_or(500.min(total_lines));
73
74        let start = offset.min(total_lines);
75        let end = (start + limit).min(total_lines);
76
77        let mut result = String::new();
78        for (i, line) in lines[start..end].iter().enumerate() {
79            result.push_str(&format!("{}\t{}\n", start + i + 1, line));
80        }
81
82        if total_lines > end {
83            result.push_str(&format!("\n... ({} more lines)", total_lines - end));
84        }
85
86        Ok(result)
87    }
88}
89#[cfg(test)]
90mod tests {
91    use super::super::test_helpers::create_tool_context;
92    use super::*;
93    use crate::tools::Tool;
94    use serde_json::json;
95
96    #[test]
97    fn test_read_tool_schema() {
98        let tool = ReadTool;
99        assert_eq!(tool.name(), "read");
100        assert!(!tool.description().is_empty());
101
102        let params = tool.parameters();
103        assert_eq!(params["type"], "object");
104        assert!(params["properties"].is_object());
105        assert!(params["required"].is_array());
106    }
107
108    #[tokio::test]
109    async fn test_read_tool_execution() {
110        let temp_dir = std::env::temp_dir();
111        let test_file = temp_dir.join("read_tool_test.txt");
112
113        // Create temp file with known content
114        let content = "line 1\nline 2\nline 3\nline 4\nline 5";
115        std::fs::write(&test_file, content).unwrap();
116
117        let tool = ReadTool;
118        let ctx = create_tool_context();
119
120        // Test basic read
121        let params = json!({
122            "path": test_file.to_string_lossy()
123        });
124        let result = tool.execute(params, ctx).await.unwrap();
125
126        // Verify line numbers and content
127        assert!(result.contains("1\tline 1"));
128        assert!(result.contains("2\tline 2"));
129        assert!(result.contains("5\tline 5"));
130
131        // Test with offset and limit
132        let ctx = create_tool_context();
133        let params = json!({
134            "path": test_file.to_string_lossy(),
135            "offset": 2,
136            "limit": 2
137        });
138        let result = tool.execute(params, ctx).await.unwrap();
139
140        assert!(result.contains("3\tline 3"));
141        assert!(result.contains("4\tline 4"));
142        assert!(!result.contains("1\tline 1"));
143        assert!(!result.contains("5\tline 5"));
144
145        // Cleanup
146        let _ = std::fs::remove_file(&test_file);
147    }
148
149    #[tokio::test]
150    async fn test_read_tool_offset() {
151        let temp_dir = std::env::temp_dir();
152        let test_file = temp_dir.join("test_read_tool_offset.txt");
153
154        // Write 10 lines
155        let lines = (1..=10).map(|i| format!("line {}", i)).collect::<Vec<_>>();
156        let content = lines.join("\n");
157        std::fs::write(&test_file, &content).unwrap();
158
159        let tool = ReadTool;
160        let ctx = create_tool_context();
161
162        // Read with offset=5 (0-indexed, so starts at line 6)
163        let params = json!({
164            "path": test_file.to_string_lossy(),
165            "offset": 5
166        });
167
168        let result = tool.execute(params, ctx).await.unwrap();
169
170        // First line shown should be line 6 (1-indexed in output)
171        assert!(result.contains("6\tline 6"));
172        // Should not contain earlier lines
173        assert!(!result.contains("1\tline 1"));
174        assert!(!result.contains("5\tline 5"));
175
176        // Cleanup
177        let _ = std::fs::remove_file(&test_file);
178    }
179}