agent_sdk/
environment.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4
5/// Entry in a directory listing
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct FileEntry {
8    pub name: String,
9    pub path: String,
10    pub is_dir: bool,
11    pub size: Option<u64>,
12}
13
14/// Match result from grep operation
15#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct GrepMatch {
17    pub path: String,
18    pub line_number: usize,
19    pub line_content: String,
20    pub match_start: usize,
21    pub match_end: usize,
22}
23
24/// Result from command execution
25#[derive(Clone, Debug, Serialize, Deserialize)]
26pub struct ExecResult {
27    pub stdout: String,
28    pub stderr: String,
29    pub exit_code: i32,
30}
31
32impl ExecResult {
33    #[must_use]
34    pub const fn success(&self) -> bool {
35        self.exit_code == 0
36    }
37}
38
39/// Environment abstraction for file and command operations.
40///
41/// The SDK's primitive tools (Read, Write, Grep, Glob, Bash) use this trait
42/// to interact with the underlying filesystem or storage backend.
43///
44/// Implementations:
45/// - `LocalFileSystem` - Standard filesystem (provided by SDK)
46/// - `InMemoryFileSystem` - For testing (provided by SDK)
47/// - Custom backends (S3, Git, iCloud, etc.)
48#[async_trait]
49pub trait Environment: Send + Sync {
50    /// Read file contents as UTF-8 string
51    ///
52    /// # Errors
53    /// Returns an error if the file cannot be read.
54    async fn read_file(&self, path: &str) -> Result<String>;
55
56    /// Read file contents as raw bytes
57    ///
58    /// # Errors
59    /// Returns an error if the file cannot be read.
60    async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>>;
61
62    /// Write string content to file (creates or overwrites)
63    ///
64    /// # Errors
65    /// Returns an error if the file cannot be written.
66    async fn write_file(&self, path: &str, content: &str) -> Result<()>;
67
68    /// Write raw bytes to file
69    ///
70    /// # Errors
71    /// Returns an error if the file cannot be written.
72    async fn write_file_bytes(&self, path: &str, content: &[u8]) -> Result<()>;
73
74    /// List directory contents
75    ///
76    /// # Errors
77    /// Returns an error if the directory cannot be read.
78    async fn list_dir(&self, path: &str) -> Result<Vec<FileEntry>>;
79
80    /// Check if path exists
81    ///
82    /// # Errors
83    /// Returns an error if existence cannot be determined.
84    async fn exists(&self, path: &str) -> Result<bool>;
85
86    /// Check if path is a directory
87    ///
88    /// # Errors
89    /// Returns an error if the check fails.
90    async fn is_dir(&self, path: &str) -> Result<bool>;
91
92    /// Check if path is a file
93    ///
94    /// # Errors
95    /// Returns an error if the check fails.
96    async fn is_file(&self, path: &str) -> Result<bool>;
97
98    /// Create directory (including parents)
99    ///
100    /// # Errors
101    /// Returns an error if the directory cannot be created.
102    async fn create_dir(&self, path: &str) -> Result<()>;
103
104    /// Delete file
105    ///
106    /// # Errors
107    /// Returns an error if the file cannot be deleted.
108    async fn delete_file(&self, path: &str) -> Result<()>;
109
110    /// Delete directory (must be empty unless recursive)
111    ///
112    /// # Errors
113    /// Returns an error if the directory cannot be deleted.
114    async fn delete_dir(&self, path: &str, recursive: bool) -> Result<()>;
115
116    /// Search for pattern in files (like ripgrep)
117    ///
118    /// # Errors
119    /// Returns an error if the search fails.
120    async fn grep(&self, pattern: &str, path: &str, recursive: bool) -> Result<Vec<GrepMatch>>;
121
122    /// Find files matching glob pattern
123    ///
124    /// # Errors
125    /// Returns an error if the glob operation fails.
126    async fn glob(&self, pattern: &str) -> Result<Vec<String>>;
127
128    /// Execute a shell command
129    ///
130    /// Not all environments support this. Default implementation returns an error.
131    ///
132    /// # Errors
133    /// Returns an error if command execution is not supported or fails.
134    async fn exec(&self, _command: &str, _timeout_ms: Option<u64>) -> Result<ExecResult> {
135        anyhow::bail!("Command execution not supported in this environment")
136    }
137
138    /// Get the root/working directory for this environment
139    fn root(&self) -> &str;
140
141    /// Resolve a relative path to absolute within this environment
142    fn resolve_path(&self, path: &str) -> String {
143        if path.starts_with('/') {
144            path.to_string()
145        } else {
146            format!("{}/{}", self.root().trim_end_matches('/'), path)
147        }
148    }
149}
150
151/// A null environment that rejects all operations.
152/// Useful as a default when no environment is configured.
153pub struct NullEnvironment;
154
155#[async_trait]
156impl Environment for NullEnvironment {
157    async fn read_file(&self, _path: &str) -> Result<String> {
158        anyhow::bail!("No environment configured")
159    }
160
161    async fn read_file_bytes(&self, _path: &str) -> Result<Vec<u8>> {
162        anyhow::bail!("No environment configured")
163    }
164
165    async fn write_file(&self, _path: &str, _content: &str) -> Result<()> {
166        anyhow::bail!("No environment configured")
167    }
168
169    async fn write_file_bytes(&self, _path: &str, _content: &[u8]) -> Result<()> {
170        anyhow::bail!("No environment configured")
171    }
172
173    async fn list_dir(&self, _path: &str) -> Result<Vec<FileEntry>> {
174        anyhow::bail!("No environment configured")
175    }
176
177    async fn exists(&self, _path: &str) -> Result<bool> {
178        anyhow::bail!("No environment configured")
179    }
180
181    async fn is_dir(&self, _path: &str) -> Result<bool> {
182        anyhow::bail!("No environment configured")
183    }
184
185    async fn is_file(&self, _path: &str) -> Result<bool> {
186        anyhow::bail!("No environment configured")
187    }
188
189    async fn create_dir(&self, _path: &str) -> Result<()> {
190        anyhow::bail!("No environment configured")
191    }
192
193    async fn delete_file(&self, _path: &str) -> Result<()> {
194        anyhow::bail!("No environment configured")
195    }
196
197    async fn delete_dir(&self, _path: &str, _recursive: bool) -> Result<()> {
198        anyhow::bail!("No environment configured")
199    }
200
201    async fn grep(&self, _pattern: &str, _path: &str, _recursive: bool) -> Result<Vec<GrepMatch>> {
202        anyhow::bail!("No environment configured")
203    }
204
205    async fn glob(&self, _pattern: &str) -> Result<Vec<String>> {
206        anyhow::bail!("No environment configured")
207    }
208
209    fn root(&self) -> &'static str {
210        "/"
211    }
212}