Skip to main content

bamboo_tools/tools/
get_file_info.rs

1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolClass, ToolCtx, ToolError, ToolOutcome, ToolResult};
3use serde_json::json;
4use tokio::fs;
5
6/// Return metadata for a file or directory.
7pub struct GetFileInfoTool;
8
9impl GetFileInfoTool {
10    pub fn new() -> Self {
11        Self
12    }
13}
14
15impl Default for GetFileInfoTool {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21#[async_trait]
22impl Tool for GetFileInfoTool {
23    fn name(&self) -> &str {
24        "GetFileInfo"
25    }
26
27    fn description(&self) -> &str {
28        "Get file metadata such as type, size, modification time, and existence without loading file contents."
29    }
30
31    fn classify(&self, _args: &serde_json::Value) -> ToolClass {
32        ToolClass::READONLY_PARALLEL
33    }
34
35    fn parameters_schema(&self) -> serde_json::Value {
36        json!({
37            "type": "object",
38            "properties": {
39                "path": {
40                    "type": "string",
41                    "description": "Absolute or relative path"
42                }
43            },
44            "required": ["path"],
45            "additionalProperties": false
46        })
47    }
48
49    async fn invoke(
50        &self,
51        args: serde_json::Value,
52        _ctx: ToolCtx,
53    ) -> Result<ToolOutcome, ToolError> {
54        let path = args
55            .get("path")
56            .and_then(|v| v.as_str())
57            .ok_or_else(|| ToolError::InvalidArguments("Missing 'path' parameter".to_string()))?;
58
59        if path.trim().is_empty() {
60            return Err(ToolError::InvalidArguments(
61                "path must be a non-empty string".to_string(),
62            ));
63        }
64
65        let metadata = match fs::metadata(path).await {
66            Ok(metadata) => metadata,
67            Err(error) => {
68                // When the path simply does not exist, return a structured
69                // `exists: false` response so this tool subsumes FileExists.
70                if error.kind() == std::io::ErrorKind::NotFound {
71                    return Ok(ToolOutcome::Completed(ToolResult {
72                        success: true,
73                        result: json!({
74                            "path": path,
75                            "exists": false
76                        })
77                        .to_string(),
78                        display_preference: Some("json".to_string()),
79                        images: Vec::new(),
80                    }));
81                }
82                return Ok(ToolOutcome::Completed(ToolResult {
83                    success: false,
84                    result: format!("Failed to read metadata for '{path}': {error}"),
85                    display_preference: Some("error".to_string()),
86                    images: Vec::new(),
87                }));
88            }
89        };
90
91        let modified_unix = metadata
92            .modified()
93            .ok()
94            .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
95            .map(|duration| duration.as_secs());
96
97        Ok(ToolOutcome::Completed(ToolResult {
98            success: true,
99            result: json!({
100                "path": path,
101                "exists": true,
102                "is_file": metadata.is_file(),
103                "is_dir": metadata.is_dir(),
104                "size_bytes": metadata.len(),
105                "modified_unix": modified_unix
106            })
107            .to_string(),
108            display_preference: Some("json".to_string()),
109            images: Vec::new(),
110        }))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[tokio::test]
119    async fn get_file_info_returns_metadata_for_existing_file() {
120        let dir = tempfile::tempdir().unwrap();
121        let file_path = dir.path().join("demo.txt");
122        tokio::fs::write(&file_path, "hello").await.unwrap();
123
124        let tool = GetFileInfoTool::new();
125        let out = tool
126            .invoke(
127                json!({"path": file_path.to_string_lossy()}),
128                ToolCtx::none("t"),
129            )
130            .await
131            .unwrap();
132        let ToolOutcome::Completed(result) = out else {
133            panic!("expected Completed")
134        };
135
136        assert!(result.success);
137        assert!(result.result.contains("\"is_file\":true"));
138        assert!(result.result.contains("\"exists\":true"));
139    }
140
141    #[tokio::test]
142    async fn get_file_info_returns_exists_false_for_missing_path() {
143        let tool = GetFileInfoTool::new();
144        let out = tool
145            .invoke(
146                json!({"path": "/tmp/bamboo-file-info-missing-xyz-98765"}),
147                ToolCtx::none("t"),
148            )
149            .await
150            .unwrap();
151        let ToolOutcome::Completed(result) = out else {
152            panic!("expected Completed")
153        };
154
155        assert!(result.success);
156        let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
157        assert_eq!(payload["exists"], false);
158    }
159}