Skip to main content

ai_agent/tools/
read.rs

1use crate::types::*;
2use std::fs;
3
4pub struct FileReadTool;
5
6impl FileReadTool {
7    pub fn new() -> Self {
8        Self
9    }
10
11    pub fn name(&self) -> &str {
12        "Read"
13    }
14
15    pub fn description(&self) -> &str {
16        "Read files from filesystem"
17    }
18
19    pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
20        "Read".to_string()
21    }
22
23    pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
24        input.and_then(|inp| inp["file_path"].as_str().map(String::from))
25    }
26
27    pub fn render_tool_result_message(
28        &self,
29        content: &serde_json::Value,
30    ) -> Option<String> {
31        let text = content["content"].as_str()?;
32        let line_count = text.lines().count();
33        Some(format!("{} {} {}", line_count, if line_count == 1 { "line" } else { "lines" }, "read"))
34    }
35
36    pub fn input_schema(&self) -> ToolInputSchema {
37        ToolInputSchema {
38            schema_type: "object".to_string(),
39            properties: serde_json::json!({
40                "file_path": {
41                    "type": "string",
42                    "description": "The absolute path to the file to read"
43                }
44            }),
45            required: Some(vec!["file_path".to_string()]),
46        }
47    }
48
49    pub async fn execute(
50        &self,
51        input: serde_json::Value,
52        context: &ToolContext,
53    ) -> Result<ToolResult, crate::error::AgentError> {
54        let path = input["file_path"]
55            .as_str()
56            .ok_or_else(|| crate::error::AgentError::Tool("file_path is required".to_string()))?;
57
58        // Resolve relative paths using cwd from context
59        let path_buf = std::path::PathBuf::from(path);
60
61        // If path is absolute and doesn't exist, try to find it relative to cwd
62        let final_path = if path_buf.is_absolute() && !path_buf.exists() {
63            if let Some(filename) = path_buf.file_name() {
64                std::path::Path::new(&context.cwd).join(filename)
65            } else {
66                std::path::Path::new(&context.cwd).join(path)
67            }
68        } else if path_buf.is_relative() {
69            std::path::Path::new(&context.cwd).join(path)
70        } else {
71            path_buf
72        };
73
74        let content =
75            fs::read_to_string(&final_path).map_err(|e| crate::error::AgentError::Io(e))?;
76
77        // Extract file extension for token estimation
78        let ext = final_path
79            .extension()
80            .and_then(|e| e.to_str())
81            .unwrap_or("");
82
83        // Validate content token budget (two-phase: rough estimate first, then API)
84        // If rough estimate is under max/4, this returns immediately without API call
85        let model = std::env::var("AI_MODEL")
86            .ok()
87            .unwrap_or_else(|| crate::utils::model::get_main_loop_model());
88        if let Err(e) = crate::services::validate_content_tokens(
89            &content, ext, None, None, None, &model,
90        )
91        .await
92        {
93            return Err(crate::error::AgentError::Tool(e.to_string()));
94        }
95
96        Ok(ToolResult {
97            result_type: "text".to_string(),
98            tool_use_id: "".to_string(),
99            content,
100            is_error: None,
101            was_persisted: None,
102        })
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_file_read_tool_name() {
112        let tool = FileReadTool::new();
113        assert_eq!(tool.name(), "Read");
114    }
115
116    #[test]
117    fn test_file_read_tool_description_contains_read() {
118        let tool = FileReadTool::new();
119        assert!(tool.description().to_lowercase().contains("read"));
120    }
121
122    #[test]
123    fn test_file_read_tool_has_path_in_schema() {
124        let tool = FileReadTool::new();
125        let schema = tool.input_schema();
126        assert!(schema.properties.get("file_path").is_some());
127    }
128
129    #[tokio::test]
130    async fn test_file_read_tool_execute_reads_file() {
131        // Create a temp file to read
132        let temp_dir = std::env::temp_dir();
133        let temp_file = temp_dir.join("test_read_file.txt");
134        std::fs::write(&temp_file, "Hello, World!").unwrap();
135
136        let tool = FileReadTool::new();
137        let input = serde_json::json!({
138            "file_path": temp_file.to_str().unwrap()
139        });
140        let context = ToolContext::default();
141
142        let result = tool.execute(input, &context).await;
143        assert!(result.is_ok());
144        let tool_result = result.unwrap();
145        assert!(tool_result.content.contains("Hello, World!"));
146
147        // Cleanup
148        std::fs::remove_file(temp_file).ok();
149    }
150
151    #[tokio::test]
152    async fn test_file_read_tool_returns_error_for_nonexistent_file() {
153        let tool = FileReadTool::new();
154        let input = serde_json::json!({
155            "file_path": "/nonexistent/file/that/does/not/exist.txt"
156        });
157        let context = ToolContext::default();
158
159        let result = tool.execute(input, &context).await;
160        assert!(result.is_err());
161    }
162}