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 RuntimeArtifactFileSystem(pub Arc<dyn SessionFileSystem>);
193
194impl crate::ToolContext {
197 pub fn runtime_artifact_file_store(&self) -> Option<Arc<dyn SessionFileSystem>> {
206 match self
207 .extensions
208 .get::<crate::session_files::RuntimeArtifactFileSystem>()
209 {
210 Some(artifacts) => Some(crate::mount_fs::scoped_prompt_file_store(
211 artifacts.0.clone(),
212 self.workspace_id,
213 )),
214 None => self.file_store.clone(),
215 }
216 }
217}
218
219pub struct WorkspaceScopedFileSystem {
229 inner: Arc<dyn SessionFileSystem>,
230 key: SessionId,
231}
232
233impl WorkspaceScopedFileSystem {
234 pub fn wrap(
236 inner: Arc<dyn SessionFileSystem>,
237 workspace_id: WorkspaceId,
238 ) -> Arc<dyn SessionFileSystem> {
239 Arc::new(Self {
240 inner,
241 key: SessionId::from_uuid(workspace_id.uuid()),
242 })
243 }
244}
245
246#[async_trait]
247impl SessionFileSystem for WorkspaceScopedFileSystem {
248 async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
249 self.inner.read_file(self.key, path).await
250 }
251 async fn write_file(
252 &self,
253 _session_id: SessionId,
254 path: &str,
255 content: &str,
256 encoding: &str,
257 ) -> Result<SessionFile> {
258 self.inner
259 .write_file(self.key, path, content, encoding)
260 .await
261 }
262 async fn write_file_if_content_matches(
263 &self,
264 _session_id: SessionId,
265 path: &str,
266 expected_content: &str,
267 expected_encoding: &str,
268 content: &str,
269 encoding: &str,
270 ) -> Result<Option<SessionFile>> {
271 self.inner
272 .write_file_if_content_matches(
273 self.key,
274 path,
275 expected_content,
276 expected_encoding,
277 content,
278 encoding,
279 )
280 .await
281 }
282 async fn delete_file(
283 &self,
284 _session_id: SessionId,
285 path: &str,
286 recursive: bool,
287 ) -> Result<bool> {
288 self.inner.delete_file(self.key, path, recursive).await
289 }
290 async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
291 self.inner.list_directory(self.key, path).await
292 }
293 async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
294 self.inner.stat_file(self.key, path).await
295 }
296 async fn grep_files(
297 &self,
298 _session_id: SessionId,
299 pattern: &str,
300 path_pattern: Option<&str>,
301 ) -> Result<Vec<GrepMatch>> {
302 self.inner.grep_files(self.key, pattern, path_pattern).await
303 }
304 async fn grep_files_with_options(
305 &self,
306 _session_id: SessionId,
307 pattern: &str,
308 options: &GrepOptions,
309 ) -> Result<GrepSearchResult> {
310 self.inner
311 .grep_files_with_options(self.key, pattern, options)
312 .await
313 }
314 async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
315 self.inner.create_directory(self.key, path).await
316 }
317 async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
318 self.inner.seed_initial_file(self.key, file).await
319 }
320
321 fn display_root(&self) -> String {
322 self.inner.display_root()
323 }
324
325 fn display_path(&self, path: &str) -> String {
326 self.inner.display_path(path)
327 }
328
329 fn resolve_path(&self, input: &str) -> String {
330 self.inner.resolve_path(input)
331 }
332
333 fn is_mount_resolver(&self) -> bool {
334 self.inner.is_mount_resolver()
335 }
336
337 fn host_path(&self, path: &str) -> Option<std::path::PathBuf> {
338 self.inner.host_path(path)
339 }
340}
341
342#[async_trait]
343impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
344 fn display_root(&self) -> String {
345 (**self).display_root()
346 }
347
348 fn display_path(&self, path: &str) -> String {
349 (**self).display_path(path)
350 }
351
352 fn resolve_path(&self, input: &str) -> String {
353 (**self).resolve_path(input)
354 }
355
356 fn is_mount_resolver(&self) -> bool {
357 (**self).is_mount_resolver()
358 }
359
360 fn host_path(&self, path: &str) -> Option<std::path::PathBuf> {
361 (**self).host_path(path)
362 }
363
364 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
365 (**self).read_file(session_id, path).await
366 }
367
368 async fn write_file(
369 &self,
370 session_id: SessionId,
371 path: &str,
372 content: &str,
373 encoding: &str,
374 ) -> Result<SessionFile> {
375 (**self)
376 .write_file(session_id, path, content, encoding)
377 .await
378 }
379
380 async fn write_file_if_content_matches(
381 &self,
382 session_id: SessionId,
383 path: &str,
384 expected_content: &str,
385 expected_encoding: &str,
386 content: &str,
387 encoding: &str,
388 ) -> Result<Option<SessionFile>> {
389 (**self)
390 .write_file_if_content_matches(
391 session_id,
392 path,
393 expected_content,
394 expected_encoding,
395 content,
396 encoding,
397 )
398 .await
399 }
400
401 async fn delete_file(
402 &self,
403 session_id: SessionId,
404 path: &str,
405 recursive: bool,
406 ) -> Result<bool> {
407 (**self).delete_file(session_id, path, recursive).await
408 }
409
410 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
411 (**self).list_directory(session_id, path).await
412 }
413
414 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
415 (**self).stat_file(session_id, path).await
416 }
417
418 async fn grep_files(
419 &self,
420 session_id: SessionId,
421 pattern: &str,
422 path_pattern: Option<&str>,
423 ) -> Result<Vec<GrepMatch>> {
424 (**self).grep_files(session_id, pattern, path_pattern).await
425 }
426
427 async fn grep_files_with_options(
428 &self,
429 session_id: SessionId,
430 pattern: &str,
431 options: &GrepOptions,
432 ) -> Result<GrepSearchResult> {
433 (**self)
434 .grep_files_with_options(session_id, pattern, options)
435 .await
436 }
437
438 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
439 (**self).create_directory(session_id, path).await
440 }
441
442 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
443 (**self).seed_initial_file(session_id, file).await
444 }
445}