agent_engine/tools/
read.rs1use 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 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 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 let params = json!({
122 "path": test_file.to_string_lossy()
123 });
124 let result = tool.execute(params, ctx).await.unwrap();
125
126 assert!(result.contains("1\tline 1"));
128 assert!(result.contains("2\tline 2"));
129 assert!(result.contains("5\tline 5"));
130
131 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 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 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 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 assert!(result.contains("6\tline 6"));
172 assert!(!result.contains("1\tline 1"));
174 assert!(!result.contains("5\tline 5"));
175
176 let _ = std::fs::remove_file(&test_file);
178 }
179}