a3s-code-core 3.2.0

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
//! Behavioural conformance suite for workspace backend implementations.
//!
//! This module captures the **implicit invariants** that every
//! [`WorkspaceFileSystem`] and [`WorkspaceFileSystemExt`] implementation must
//! satisfy. Until this suite landed those invariants only lived in commits,
//! comments, and the maintainer's head — new backends would have to
//! reverse-engineer them from existing implementations.
//!
//! # How to use
//!
//! When you add a new backend (S3, GCS, container, browser, ...):
//!
//! ```ignore
//! #[tokio::test]
//! async fn my_backend_conforms_to_filesystem() {
//!     let fs: Arc<dyn WorkspaceFileSystem> = Arc::new(MyBackend::new(...));
//!     conformance::assert_filesystem_conformance(fs, "MyBackend").await;
//! }
//! ```
//!
//! When the suite grows (new invariant discovered, e.g. an S3 production
//! incident), every backend running it gets the regression test for free —
//! that is the point.
//!
//! # What is covered
//!
//! * [`assert_filesystem_conformance`] — the required
//!   [`WorkspaceFileSystem`] surface: read-after-write round-trips, errors
//!   on nonexistent reads / list_dirs, listing returns directory entries,
//!   write overwrites existing content, write creates parent path
//!   components.
//! * [`assert_filesystem_ext_conformance`] — the optional
//!   [`WorkspaceFileSystemExt`] CAS surface: version round-trip,
//!   compare-and-swap success on matching version, downcastable
//!   [`WorkspaceVersionConflict`] on mismatch, empty-version rejection.
//!
//! Out of scope (intentionally) for the first cut:
//!
//! * `WorkspaceCommandRunner` — shell semantics differ enough across
//!   sandboxes that a single contract is hard to write.
//! * `WorkspaceSearch` — glob and regex behaviour are inherently
//!   implementation-defined; conformance tests would over-constrain.
//! * `WorkspaceGit*` — implementation behaviour is too coupled to the
//!   underlying git engine to capture in a single trait contract.
//!
//! # Reference backend
//!
//! [`InMemoryFileSystem`] is a tiny `HashMap`-backed implementation that
//! exists for two purposes: (1) it runs the conformance suite against
//! itself in this module, proving the contract is internally consistent;
//! (2) it serves as the smallest possible reference for what a backend
//! that satisfies the suite has to look like. Real production backends
//! (`LocalWorkspaceBackend`, `S3WorkspaceBackend`) layer real I/O on top
//! of the same observable semantics.

use super::{
    WorkspaceDirEntry, WorkspaceError, WorkspaceFileSystem, WorkspaceFileSystemExt, WorkspacePath,
    WorkspaceResult, WorkspaceVersionConflict, WorkspaceWriteOutcome,
};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// ============================================================================
// Conformance assertions for WorkspaceFileSystem
// ============================================================================

/// Run the full required-surface conformance suite against a backend.
///
/// On failure, the panic message includes `ctx` so test output names the
/// backend under test even when several backends share the same harness.
pub(crate) async fn assert_filesystem_conformance(fs: Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    fs_read_after_write_roundtrip(&fs, ctx).await;
    fs_read_nonexistent_errors(&fs, ctx).await;
    fs_write_overwrites_existing(&fs, ctx).await;
    fs_write_creates_parent_components(&fs, ctx).await;
    fs_list_dir_root_succeeds(&fs, ctx).await;
    fs_list_dir_after_write_sees_the_entry(&fs, ctx).await;
    fs_list_dir_nonexistent_errors(&fs, ctx).await;
}

/// Run the optional CAS-surface conformance suite against a backend that
/// implements both [`WorkspaceFileSystem`] and [`WorkspaceFileSystemExt`].
///
/// Backends that don't implement `Ext` just don't call this.
pub(crate) async fn assert_filesystem_ext_conformance(
    fs: Arc<dyn WorkspaceFileSystem>,
    ext: Arc<dyn WorkspaceFileSystemExt>,
    ctx: &str,
) {
    ext_version_token_is_non_empty(&fs, &ext, ctx).await;
    ext_write_with_matching_version_succeeds(&fs, &ext, ctx).await;
    ext_write_with_stale_version_yields_conflict(&fs, &ext, ctx).await;
    ext_empty_expected_version_is_rejected(&ext, ctx).await;
}

// ---- individual invariants ----

async fn fs_read_after_write_roundtrip(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/roundtrip.txt");
    fs.write_text(&path, "hello world")
        .await
        .unwrap_or_else(|e| panic!("[{ctx}] write_text failed: {e}"));
    let content = fs
        .read_text(&path)
        .await
        .unwrap_or_else(|e| panic!("[{ctx}] read_text after write failed: {e}"));
    assert_eq!(
        content, "hello world",
        "[{ctx}] read_text must return exactly what write_text wrote"
    );
}

async fn fs_read_nonexistent_errors(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/definitely-not-there.txt");
    let result = fs.read_text(&path).await;
    assert!(
        result.is_err(),
        "[{ctx}] read_text on a path that was never written must error, got Ok({:?})",
        result.ok()
    );
}

async fn fs_write_overwrites_existing(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/overwrite.txt");
    fs.write_text(&path, "v1").await.unwrap();
    fs.write_text(&path, "v2").await.unwrap();
    let content = fs.read_text(&path).await.unwrap();
    assert_eq!(
        content, "v2",
        "[{ctx}] second write_text must overwrite first"
    );
}

async fn fs_write_creates_parent_components(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/deep/nested/path/file.txt");
    fs.write_text(&path, "deep").await.unwrap_or_else(|e| {
        panic!("[{ctx}] write_text must create missing parent components: {e}")
    });
    let content = fs.read_text(&path).await.unwrap();
    assert_eq!(content, "deep", "[{ctx}] write_text round-trip at depth");
}

async fn fs_list_dir_root_succeeds(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let result = fs.list_dir(&WorkspacePath::root()).await;
    assert!(
        result.is_ok(),
        "[{ctx}] list_dir on the workspace root must always succeed (may be empty), got {:?}",
        result
    );
}

async fn fs_list_dir_after_write_sees_the_entry(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    // Use a fresh subdirectory so we know exactly what should appear.
    let dir = WorkspacePath::from_normalized("conformance/listing-test");
    let file = WorkspacePath::from_normalized("conformance/listing-test/visible.txt");
    fs.write_text(&file, "hello").await.unwrap();

    let entries = fs
        .list_dir(&dir)
        .await
        .unwrap_or_else(|e| panic!("[{ctx}] list_dir after write failed: {e}"));
    let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
    assert!(
        names.contains(&"visible.txt"),
        "[{ctx}] list_dir must include just-written entry; got {names:?}"
    );
}

async fn fs_list_dir_nonexistent_errors(fs: &Arc<dyn WorkspaceFileSystem>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/never-created");
    let result = fs.list_dir(&path).await;
    assert!(
        result.is_err(),
        "[{ctx}] list_dir on a nonexistent path must error, got Ok({:?}); \
         a silent empty result masks user typos",
        result.ok()
    );
}

async fn ext_version_token_is_non_empty(
    fs: &Arc<dyn WorkspaceFileSystem>,
    ext: &Arc<dyn WorkspaceFileSystemExt>,
    ctx: &str,
) {
    let path = WorkspacePath::from_normalized("conformance/cas-version.txt");
    fs.write_text(&path, "seed").await.unwrap();
    let (content, version) = ext
        .read_text_with_version(&path)
        .await
        .unwrap_or_else(|e| panic!("[{ctx}] read_text_with_version failed: {e}"));
    assert_eq!(content, "seed");
    assert!(
        !version.is_empty(),
        "[{ctx}] read_text_with_version must return a non-empty opaque token"
    );
}

async fn ext_write_with_matching_version_succeeds(
    fs: &Arc<dyn WorkspaceFileSystem>,
    ext: &Arc<dyn WorkspaceFileSystemExt>,
    ctx: &str,
) {
    let path = WorkspacePath::from_normalized("conformance/cas-match.txt");
    fs.write_text(&path, "v0").await.unwrap();
    let (_, version) = ext.read_text_with_version(&path).await.unwrap();
    ext.write_text_if_version(&path, "v1", &version)
        .await
        .unwrap_or_else(|e| {
            panic!("[{ctx}] write_text_if_version with matching version must succeed: {e}")
        });
    assert_eq!(fs.read_text(&path).await.unwrap(), "v1");
}

async fn ext_write_with_stale_version_yields_conflict(
    fs: &Arc<dyn WorkspaceFileSystem>,
    ext: &Arc<dyn WorkspaceFileSystemExt>,
    ctx: &str,
) {
    let path = WorkspacePath::from_normalized("conformance/cas-conflict.txt");
    fs.write_text(&path, "alpha").await.unwrap();
    let (_, stale_version) = ext.read_text_with_version(&path).await.unwrap();

    // Simulate a concurrent writer that bumps the version.
    fs.write_text(&path, "concurrent").await.unwrap();

    let err = ext
        .write_text_if_version(&path, "beta", &stale_version)
        .await
        .expect_err(&format!(
            "[{ctx}] write_text_if_version with stale version must reject"
        ));
    assert!(
        matches!(err, WorkspaceError::VersionConflict(_)),
        "[{ctx}] CAS rejection must produce WorkspaceError::VersionConflict; got: {err:?}"
    );
}

async fn ext_empty_expected_version_is_rejected(ext: &Arc<dyn WorkspaceFileSystemExt>, ctx: &str) {
    let path = WorkspacePath::from_normalized("conformance/cas-empty-ver.txt");
    let err = ext
        .write_text_if_version(&path, "anything", "")
        .await
        .expect_err(&format!(
            "[{ctx}] write_text_if_version with empty expected version must reject"
        ));
    // Don't require a specific message — just that it's a clear failure.
    let _ = err;
}

// ============================================================================
// Reference backend: InMemoryFileSystem
// ============================================================================

/// A `HashMap<String, (content, version)>`-backed reference implementation.
///
/// Purpose:
/// * **Self-test the conformance suite.** The suite must pass against an
///   ideally-behaving backend; if it fails here, the contract itself is
///   inconsistent.
/// * **Document the shape.** A few hundred lines of obvious code show new
///   backend authors what the trait surface "wants".
/// * **Mock for higher-level tests.** Helper modules can wire this into a
///   `WorkspaceServices` to exercise tool code paths without touching disk
///   or network.
///
/// The file map and the version counter live behind a single mutex so the
/// CAS write is genuinely atomic — a separate counter mutex would allow
/// a concurrent writer to slip in between the version check and the
/// version bump, defeating the conformance test for stale-version
/// rejection.
struct InMemoryState {
    files: HashMap<String, (String, String)>,
    counter: u64,
}

pub(crate) struct InMemoryFileSystem {
    state: Mutex<InMemoryState>,
}

impl InMemoryFileSystem {
    pub(crate) fn new() -> Self {
        Self {
            state: Mutex::new(InMemoryState {
                files: HashMap::new(),
                counter: 0,
            }),
        }
    }

    fn bump_version(state: &mut InMemoryState) -> String {
        state.counter += 1;
        format!("v{}", state.counter)
    }
}

impl Default for InMemoryFileSystem {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl WorkspaceFileSystem for InMemoryFileSystem {
    async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String> {
        self.state
            .lock()
            .unwrap()
            .files
            .get(path.as_str())
            .map(|(c, _)| c.clone())
            .ok_or_else(|| WorkspaceError::NotFound {
                path: path.as_str().to_string(),
            })
    }

    async fn write_text(
        &self,
        path: &WorkspacePath,
        content: &str,
    ) -> WorkspaceResult<WorkspaceWriteOutcome> {
        let mut state = self.state.lock().unwrap();
        let version = Self::bump_version(&mut state);
        state
            .files
            .insert(path.as_str().to_string(), (content.to_string(), version));
        Ok(WorkspaceWriteOutcome {
            bytes: content.len(),
            lines: content.lines().count(),
        })
    }

    async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>> {
        // Synthesize a directory view from the flat key space. For a
        // requested directory at `path`, anything stored under
        // `<path>/<rest>` shows up. Mid-path components become Directory
        // entries; direct children become File entries.
        let prefix: String = if path.is_root() {
            String::new()
        } else {
            format!("{}/", path.as_str())
        };
        let state = self.state.lock().unwrap();
        let mut seen_dirs: std::collections::HashSet<String> = std::collections::HashSet::new();
        let mut entries: Vec<WorkspaceDirEntry> = Vec::new();
        let mut any = false;
        for (key, (content, _)) in state.files.iter() {
            let Some(rest) = key.strip_prefix(&prefix) else {
                continue;
            };
            if rest.is_empty() {
                continue;
            }
            any = true;
            if let Some((dir_name, _)) = rest.split_once('/') {
                if seen_dirs.insert(dir_name.to_string()) {
                    entries.push(WorkspaceDirEntry {
                        name: dir_name.to_string(),
                        kind: super::WorkspaceFileType::Directory,
                        size: 0,
                    });
                }
            } else {
                entries.push(WorkspaceDirEntry {
                    name: rest.to_string(),
                    kind: super::WorkspaceFileType::File,
                    size: content.len() as u64,
                });
            }
        }
        if !path.is_root() && !any {
            return Err(WorkspaceError::NotFound {
                path: path.as_str().to_string(),
            });
        }
        entries.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(entries)
    }
}

#[async_trait]
impl WorkspaceFileSystemExt for InMemoryFileSystem {
    async fn read_text_with_version(
        &self,
        path: &WorkspacePath,
    ) -> WorkspaceResult<(String, String)> {
        self.state
            .lock()
            .unwrap()
            .files
            .get(path.as_str())
            .cloned()
            .ok_or_else(|| WorkspaceError::NotFound {
                path: path.as_str().to_string(),
            })
    }

    async fn write_text_if_version(
        &self,
        path: &WorkspacePath,
        content: &str,
        expected_version: &str,
    ) -> WorkspaceResult<WorkspaceWriteOutcome> {
        if expected_version.is_empty() {
            return Err(WorkspaceError::InvalidArgument {
                message: "expected_version must not be empty".to_string(),
            });
        }
        // Hold the single mutex across the entire compare-and-swap so a
        // concurrent writer cannot slip between the version check and the
        // version bump.
        let mut state = self.state.lock().unwrap();
        let actual = state.files.get(path.as_str()).map(|(_, v)| v.clone());
        match actual {
            Some(actual) if actual == expected_version => {
                let new_version = Self::bump_version(&mut state);
                state.files.insert(
                    path.as_str().to_string(),
                    (content.to_string(), new_version),
                );
                Ok(WorkspaceWriteOutcome {
                    bytes: content.len(),
                    lines: content.lines().count(),
                })
            }
            Some(actual) => Err(WorkspaceError::VersionConflict(WorkspaceVersionConflict {
                path: path.as_str().to_string(),
                expected: expected_version.to_string(),
                actual: Some(actual),
            })),
            None => Err(WorkspaceError::NotFound {
                path: path.as_str().to_string(),
            }),
        }
    }
}

// ============================================================================
// Self-test: the conformance suite must pass against the in-memory backend.
// And the local backend must too, proving the contract is implementable
// over real I/O.
// ============================================================================

#[cfg(test)]
mod self_tests {
    use super::*;
    use crate::workspace::LocalWorkspaceBackend;

    #[tokio::test]
    async fn in_memory_backend_satisfies_filesystem_conformance() {
        let fs: Arc<dyn WorkspaceFileSystem> = Arc::new(InMemoryFileSystem::new());
        assert_filesystem_conformance(fs, "InMemoryFileSystem").await;
    }

    #[tokio::test]
    async fn in_memory_backend_satisfies_filesystem_ext_conformance() {
        let backend = Arc::new(InMemoryFileSystem::new());
        let fs: Arc<dyn WorkspaceFileSystem> = backend.clone();
        let ext: Arc<dyn WorkspaceFileSystemExt> = backend;
        assert_filesystem_ext_conformance(fs, ext, "InMemoryFileSystem").await;
    }

    #[tokio::test]
    async fn local_backend_satisfies_filesystem_conformance() {
        let temp = tempfile::tempdir().unwrap();
        let fs: Arc<dyn WorkspaceFileSystem> =
            Arc::new(LocalWorkspaceBackend::new(temp.path().to_path_buf()));
        assert_filesystem_conformance(fs, "LocalWorkspaceBackend").await;
    }
}