1use crate::error::Result;
4use crate::session_file::{
5 FileInfo, FileStat, GrepMatch, GrepOptions, GrepSearchResult, InitialFile, SessionFile,
6};
7use crate::typed_id::{SessionId, WorkspaceId};
8use async_trait::async_trait;
9use std::sync::Arc;
10
11#[async_trait]
19pub trait SessionFileSystem: Send + Sync {
20 fn display_root(&self) -> String {
26 crate::session_path::WORKSPACE_PREFIX.to_string()
27 }
28
29 fn display_path(&self, path: &str) -> String {
35 crate::session_path::to_display_path(path)
36 }
37
38 fn resolve_path(&self, input: &str) -> String {
52 crate::session_path::to_session_path(input)
53 }
54
55 fn is_mount_resolver(&self) -> bool;
60
61 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
63
64 async fn write_file(
66 &self,
67 session_id: SessionId,
68 path: &str,
69 content: &str,
70 encoding: &str,
71 ) -> Result<SessionFile>;
72
73 async fn write_file_if_content_matches(
78 &self,
79 session_id: SessionId,
80 path: &str,
81 expected_content: &str,
82 expected_encoding: &str,
83 content: &str,
84 encoding: &str,
85 ) -> Result<Option<SessionFile>> {
86 let Some(existing) = self.read_file(session_id, path).await? else {
87 return Ok(None);
88 };
89
90 if existing.is_directory {
91 return Ok(None);
92 }
93
94 let current_content = existing.content.unwrap_or_default();
95 if current_content != expected_content || existing.encoding != expected_encoding {
96 return Ok(None);
97 }
98
99 self.write_file(session_id, path, content, encoding)
100 .await
101 .map(Some)
102 }
103
104 async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
106 -> Result<bool>;
107
108 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
110
111 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
113
114 async fn grep_files(
121 &self,
122 session_id: SessionId,
123 pattern: &str,
124 path_pattern: Option<&str>,
125 ) -> Result<Vec<GrepMatch>>;
126
127 async fn grep_files_with_options(
133 &self,
134 session_id: SessionId,
135 pattern: &str,
136 options: &GrepOptions,
137 ) -> Result<GrepSearchResult> {
138 if options.before_context != 0 || options.after_context != 0 {
139 return Err(crate::error::AgentLoopError::tool(
140 "this file store does not support grep context",
141 ));
142 }
143 let all = self
144 .grep_files(session_id, pattern, options.path_pattern.as_deref())
145 .await?;
146 Ok(crate::session_file::bound_grep_matches(all, options))
147 }
148
149 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
151
152 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
154 if file.is_readonly {
155 return Err(crate::error::AgentLoopError::store(
156 "read-only initial files require a SessionFileSystem-specific seed implementation",
157 ));
158 }
159 self.write_file(session_id, &file.path, &file.content, &file.encoding)
160 .await?;
161 Ok(())
162 }
163}
164
165pub struct WorkspaceScopedFileSystem {
175 inner: Arc<dyn SessionFileSystem>,
176 key: SessionId,
177}
178
179impl WorkspaceScopedFileSystem {
180 pub fn wrap(
182 inner: Arc<dyn SessionFileSystem>,
183 workspace_id: WorkspaceId,
184 ) -> Arc<dyn SessionFileSystem> {
185 Arc::new(Self {
186 inner,
187 key: SessionId::from_uuid(workspace_id.uuid()),
188 })
189 }
190}
191
192#[async_trait]
193impl SessionFileSystem for WorkspaceScopedFileSystem {
194 async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
195 self.inner.read_file(self.key, path).await
196 }
197 async fn write_file(
198 &self,
199 _session_id: SessionId,
200 path: &str,
201 content: &str,
202 encoding: &str,
203 ) -> Result<SessionFile> {
204 self.inner
205 .write_file(self.key, path, content, encoding)
206 .await
207 }
208 async fn write_file_if_content_matches(
209 &self,
210 _session_id: SessionId,
211 path: &str,
212 expected_content: &str,
213 expected_encoding: &str,
214 content: &str,
215 encoding: &str,
216 ) -> Result<Option<SessionFile>> {
217 self.inner
218 .write_file_if_content_matches(
219 self.key,
220 path,
221 expected_content,
222 expected_encoding,
223 content,
224 encoding,
225 )
226 .await
227 }
228 async fn delete_file(
229 &self,
230 _session_id: SessionId,
231 path: &str,
232 recursive: bool,
233 ) -> Result<bool> {
234 self.inner.delete_file(self.key, path, recursive).await
235 }
236 async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
237 self.inner.list_directory(self.key, path).await
238 }
239 async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
240 self.inner.stat_file(self.key, path).await
241 }
242 async fn grep_files(
243 &self,
244 _session_id: SessionId,
245 pattern: &str,
246 path_pattern: Option<&str>,
247 ) -> Result<Vec<GrepMatch>> {
248 self.inner.grep_files(self.key, pattern, path_pattern).await
249 }
250 async fn grep_files_with_options(
251 &self,
252 _session_id: SessionId,
253 pattern: &str,
254 options: &GrepOptions,
255 ) -> Result<GrepSearchResult> {
256 self.inner
257 .grep_files_with_options(self.key, pattern, options)
258 .await
259 }
260 async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
261 self.inner.create_directory(self.key, path).await
262 }
263 async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
264 self.inner.seed_initial_file(self.key, file).await
265 }
266
267 fn display_root(&self) -> String {
268 self.inner.display_root()
269 }
270
271 fn display_path(&self, path: &str) -> String {
272 self.inner.display_path(path)
273 }
274
275 fn resolve_path(&self, input: &str) -> String {
276 self.inner.resolve_path(input)
277 }
278
279 fn is_mount_resolver(&self) -> bool {
280 self.inner.is_mount_resolver()
281 }
282}
283
284#[async_trait]
285impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
286 fn display_root(&self) -> String {
287 (**self).display_root()
288 }
289
290 fn display_path(&self, path: &str) -> String {
291 (**self).display_path(path)
292 }
293
294 fn resolve_path(&self, input: &str) -> String {
295 (**self).resolve_path(input)
296 }
297
298 fn is_mount_resolver(&self) -> bool {
299 (**self).is_mount_resolver()
300 }
301
302 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
303 (**self).read_file(session_id, path).await
304 }
305
306 async fn write_file(
307 &self,
308 session_id: SessionId,
309 path: &str,
310 content: &str,
311 encoding: &str,
312 ) -> Result<SessionFile> {
313 (**self)
314 .write_file(session_id, path, content, encoding)
315 .await
316 }
317
318 async fn write_file_if_content_matches(
319 &self,
320 session_id: SessionId,
321 path: &str,
322 expected_content: &str,
323 expected_encoding: &str,
324 content: &str,
325 encoding: &str,
326 ) -> Result<Option<SessionFile>> {
327 (**self)
328 .write_file_if_content_matches(
329 session_id,
330 path,
331 expected_content,
332 expected_encoding,
333 content,
334 encoding,
335 )
336 .await
337 }
338
339 async fn delete_file(
340 &self,
341 session_id: SessionId,
342 path: &str,
343 recursive: bool,
344 ) -> Result<bool> {
345 (**self).delete_file(session_id, path, recursive).await
346 }
347
348 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
349 (**self).list_directory(session_id, path).await
350 }
351
352 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
353 (**self).stat_file(session_id, path).await
354 }
355
356 async fn grep_files(
357 &self,
358 session_id: SessionId,
359 pattern: &str,
360 path_pattern: Option<&str>,
361 ) -> Result<Vec<GrepMatch>> {
362 (**self).grep_files(session_id, pattern, path_pattern).await
363 }
364
365 async fn grep_files_with_options(
366 &self,
367 session_id: SessionId,
368 pattern: &str,
369 options: &GrepOptions,
370 ) -> Result<GrepSearchResult> {
371 (**self)
372 .grep_files_with_options(session_id, pattern, options)
373 .await
374 }
375
376 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
377 (**self).create_directory(session_id, path).await
378 }
379
380 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
381 (**self).seed_initial_file(session_id, file).await
382 }
383}