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 fn host_path(&self, _path: &str) -> Option<std::path::PathBuf> {
75 None
76 }
77
78 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
80
81 async fn write_file(
83 &self,
84 session_id: SessionId,
85 path: &str,
86 content: &str,
87 encoding: &str,
88 ) -> Result<SessionFile>;
89
90 async fn write_file_if_content_matches(
95 &self,
96 session_id: SessionId,
97 path: &str,
98 expected_content: &str,
99 expected_encoding: &str,
100 content: &str,
101 encoding: &str,
102 ) -> Result<Option<SessionFile>> {
103 let Some(existing) = self.read_file(session_id, path).await? else {
104 return Ok(None);
105 };
106
107 if existing.is_directory {
108 return Ok(None);
109 }
110
111 let current_content = existing.content.unwrap_or_default();
112 if current_content != expected_content || existing.encoding != expected_encoding {
113 return Ok(None);
114 }
115
116 self.write_file(session_id, path, content, encoding)
117 .await
118 .map(Some)
119 }
120
121 async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
123 -> Result<bool>;
124
125 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
127
128 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
130
131 async fn grep_files(
138 &self,
139 session_id: SessionId,
140 pattern: &str,
141 path_pattern: Option<&str>,
142 ) -> Result<Vec<GrepMatch>>;
143
144 async fn grep_files_with_options(
150 &self,
151 session_id: SessionId,
152 pattern: &str,
153 options: &GrepOptions,
154 ) -> Result<GrepSearchResult> {
155 if options.before_context != 0 || options.after_context != 0 {
156 return Err(crate::error::AgentLoopError::tool(
157 "this file store does not support grep context",
158 ));
159 }
160 let all = self
161 .grep_files(session_id, pattern, options.path_pattern.as_deref())
162 .await?;
163 Ok(crate::session_file::bound_grep_matches(all, options))
164 }
165
166 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
168
169 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
171 if file.is_readonly {
172 return Err(crate::error::AgentLoopError::store(
173 "read-only initial files require a SessionFileSystem-specific seed implementation",
174 ));
175 }
176 self.write_file(session_id, &file.path, &file.content, &file.encoding)
177 .await?;
178 Ok(())
179 }
180}
181
182pub struct WorkspaceScopedFileSystem {
192 inner: Arc<dyn SessionFileSystem>,
193 key: SessionId,
194}
195
196impl WorkspaceScopedFileSystem {
197 pub fn wrap(
199 inner: Arc<dyn SessionFileSystem>,
200 workspace_id: WorkspaceId,
201 ) -> Arc<dyn SessionFileSystem> {
202 Arc::new(Self {
203 inner,
204 key: SessionId::from_uuid(workspace_id.uuid()),
205 })
206 }
207}
208
209#[async_trait]
210impl SessionFileSystem for WorkspaceScopedFileSystem {
211 async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
212 self.inner.read_file(self.key, path).await
213 }
214 async fn write_file(
215 &self,
216 _session_id: SessionId,
217 path: &str,
218 content: &str,
219 encoding: &str,
220 ) -> Result<SessionFile> {
221 self.inner
222 .write_file(self.key, path, content, encoding)
223 .await
224 }
225 async fn write_file_if_content_matches(
226 &self,
227 _session_id: SessionId,
228 path: &str,
229 expected_content: &str,
230 expected_encoding: &str,
231 content: &str,
232 encoding: &str,
233 ) -> Result<Option<SessionFile>> {
234 self.inner
235 .write_file_if_content_matches(
236 self.key,
237 path,
238 expected_content,
239 expected_encoding,
240 content,
241 encoding,
242 )
243 .await
244 }
245 async fn delete_file(
246 &self,
247 _session_id: SessionId,
248 path: &str,
249 recursive: bool,
250 ) -> Result<bool> {
251 self.inner.delete_file(self.key, path, recursive).await
252 }
253 async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
254 self.inner.list_directory(self.key, path).await
255 }
256 async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
257 self.inner.stat_file(self.key, path).await
258 }
259 async fn grep_files(
260 &self,
261 _session_id: SessionId,
262 pattern: &str,
263 path_pattern: Option<&str>,
264 ) -> Result<Vec<GrepMatch>> {
265 self.inner.grep_files(self.key, pattern, path_pattern).await
266 }
267 async fn grep_files_with_options(
268 &self,
269 _session_id: SessionId,
270 pattern: &str,
271 options: &GrepOptions,
272 ) -> Result<GrepSearchResult> {
273 self.inner
274 .grep_files_with_options(self.key, pattern, options)
275 .await
276 }
277 async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
278 self.inner.create_directory(self.key, path).await
279 }
280 async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
281 self.inner.seed_initial_file(self.key, file).await
282 }
283
284 fn display_root(&self) -> String {
285 self.inner.display_root()
286 }
287
288 fn display_path(&self, path: &str) -> String {
289 self.inner.display_path(path)
290 }
291
292 fn resolve_path(&self, input: &str) -> String {
293 self.inner.resolve_path(input)
294 }
295
296 fn is_mount_resolver(&self) -> bool {
297 self.inner.is_mount_resolver()
298 }
299
300 fn host_path(&self, path: &str) -> Option<std::path::PathBuf> {
301 self.inner.host_path(path)
302 }
303}
304
305#[async_trait]
306impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
307 fn display_root(&self) -> String {
308 (**self).display_root()
309 }
310
311 fn display_path(&self, path: &str) -> String {
312 (**self).display_path(path)
313 }
314
315 fn resolve_path(&self, input: &str) -> String {
316 (**self).resolve_path(input)
317 }
318
319 fn is_mount_resolver(&self) -> bool {
320 (**self).is_mount_resolver()
321 }
322
323 fn host_path(&self, path: &str) -> Option<std::path::PathBuf> {
324 (**self).host_path(path)
325 }
326
327 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
328 (**self).read_file(session_id, path).await
329 }
330
331 async fn write_file(
332 &self,
333 session_id: SessionId,
334 path: &str,
335 content: &str,
336 encoding: &str,
337 ) -> Result<SessionFile> {
338 (**self)
339 .write_file(session_id, path, content, encoding)
340 .await
341 }
342
343 async fn write_file_if_content_matches(
344 &self,
345 session_id: SessionId,
346 path: &str,
347 expected_content: &str,
348 expected_encoding: &str,
349 content: &str,
350 encoding: &str,
351 ) -> Result<Option<SessionFile>> {
352 (**self)
353 .write_file_if_content_matches(
354 session_id,
355 path,
356 expected_content,
357 expected_encoding,
358 content,
359 encoding,
360 )
361 .await
362 }
363
364 async fn delete_file(
365 &self,
366 session_id: SessionId,
367 path: &str,
368 recursive: bool,
369 ) -> Result<bool> {
370 (**self).delete_file(session_id, path, recursive).await
371 }
372
373 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
374 (**self).list_directory(session_id, path).await
375 }
376
377 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
378 (**self).stat_file(session_id, path).await
379 }
380
381 async fn grep_files(
382 &self,
383 session_id: SessionId,
384 pattern: &str,
385 path_pattern: Option<&str>,
386 ) -> Result<Vec<GrepMatch>> {
387 (**self).grep_files(session_id, pattern, path_pattern).await
388 }
389
390 async fn grep_files_with_options(
391 &self,
392 session_id: SessionId,
393 pattern: &str,
394 options: &GrepOptions,
395 ) -> Result<GrepSearchResult> {
396 (**self)
397 .grep_files_with_options(session_id, pattern, options)
398 .await
399 }
400
401 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
402 (**self).create_directory(session_id, path).await
403 }
404
405 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
406 (**self).seed_initial_file(session_id, file).await
407 }
408}