Skip to content

docs: stateful agent session demo — the sandbox is a value#45

Merged
ivarvong merged 5 commits into
mainfrom
docs/multi-repo-grep-example
Jul 2, 2026
Merged

docs: stateful agent session demo — the sandbox is a value#45
ivarvong merged 5 commits into
mainfrom
docs/multi-repo-grep-example

Conversation

@ivarvong

@ivarvong ivarvong commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Adds examples/agent_session.exs: a runnable, deterministic, ~60-line example of driving a just_bash session as a value, with git-backed mounts.

elixir examples/agent_session.exs

No helpers or wrapper modules — every executable line is what a caller writes. Four public repositories (vfs, exgit, just_bash, pyex) are cloned over git smart HTTP v2 in pure Elixir — no git binary, no checkout on disk, no container — and mounted into one session. Every exec is written as

{%{exit_code: 0} = result, session} = JustBash.exec(session, cmd)

which executes, threads the session forward, and asserts the exit code in a single match. On failure the MatchError carries the full Result, stderr included.

What it demonstrates

  • Programmatic mount and umount. Each loop iteration mounts one more repo at /repos/<name> and re-runs the same grep -rln 'defimpl VFS.Mountable, for:' /repos/*/lib. The match list grows with the mount table (2 → 3 → 4 → 6 files); the glob expands across mount roots like ordinary directories. Backends appear exactly once each, as a constructor at the mount line. umount at the end removes pyex from the same grep.
  • State threads through the value. The grep is redirected into /work/matches.txt; the next exec reads it back with wc -l. One command's writes are the next command's input, carried entirely by the returned session.
  • Checkpoint and rollback are variable bindings. checkpoint = session, then the live session removes the file — cat exits 1, asserted — while the checkpoint still reads it — cat exits 0, asserted. No snapshot API exists because none is needed.

The transcript is deterministic — consecutive runs are byte-identical for a given set of repo HEADs — and every command is exit-asserted, so running the example is a smoke test of the stack.

How the libraries interact

just_bash never references exgit, and exgit never references just_bash; both depend on vfs's ten-operation VFS.Mountable protocol. The example's mount line crosses all three layers.

A session's filesystem is a %VFS{} mount table — JustBash.mount/3 is a one-line delegation:

# just_bash — lib/just_bash.ex
def mount(%JustBash{fs: fs} = bash, mountpoint, backend) do
  %{bash | fs: VFS.mount(fs, mountpoint, backend)}
end

When the agent's grep reads a file, just_bash's commands go through its FS facade, which delegates to vfs:

# just_bash — lib/just_bash/fs/fs.ex
@doc "See `VFS.read_file/2`."
defdelegate read_file(fs, path), to: VFS

vfs resolves the longest mount prefix, strips it, and dispatches to whichever backend owns the path. Every operation returns the possibly-updated backend, which vfs threads back into the table — that is how lazy-clone caches survive:

# vfs — lib/vfs/mountable.ex: the contract every backend implements
@spec stream_read(t, path, keyword) ::
        {:ok, Enumerable.t(binary), t} | {:error, VFS.Error.t()}
def stream_read(impl, path, opts)

exgit's entire integration is one protocol implementation — paths in, blobs out of the in-memory object store, workspace state threaded back:

# exgit — lib/exgit/workspace/vfs.ex
defimpl VFS.Mountable, for: Exgit.Workspace do
  def stream_read(%Workspace{} = ws, path, opts) do
    p = strip_leading(VFS.Path.normalize(path))

    case Workspace.read(ws, p) do
      {:ok, data, ws} -> {:ok, slice(data, opts), ws}
      {:error, reason} -> {:error, error_for(reason, path)}
    end
  end
end

So grep -rln … /repos/*/lib resolves as: just_bash's grep command → JustBash.FS → vfs mount resolution → Exgit.Workspace's Mountable impl → git blobs. Swap the backend and nothing above the protocol changes — pyex's S3 and overlay filesystems, which this grep finds, mount the same way.

Relevance to agent platforms (one repo per session)

  • Session start is Exgit.clone(url) + JustBash.mount/3: an isolated bash environment with the repository at HEAD, fetched straight into memory. No clone on disk, no container image, no shared checkout to coordinate over.
  • Isolation is structural. Each session is an immutable value; concurrent sessions cannot interfere by construction. Checkpoint, fork, and replay are struct bindings — the example exercises this directly.
  • Session end is umount, or letting the value go out of scope. Cleanup is garbage collection.
  • Agents speak bash. grep, wc, redirects, pipes, and globs work uniformly across every mount — the tool surface presented to the model is the one it is already trained on, while the host keeps typed control over what is mounted where.
  • Large repos don't require full fetches. Exgit.clone(url, filter: {:blob, :none}) starts a session from refs plus trees; blobs fetch lazily on first read, and that cache threads through the session value like all other state.
  • One protocol for every backend. The grep finds six VFS.Mountable implementations across the mounted repos — including exgit's own workspace impl and pyex's S3 and overlay filesystems. Anything implementing the protocol mounts into the same tree the agent is already searching.

Also adds examples/**/*.exs to the formatter inputs so the example stays format-checked in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H2mmV55riKhWgzBS8G2oMT

ivarvong and others added 3 commits July 2, 2026 09:42
A runnable, deterministic example of the just_bash + vfs + exgit stack
as an agent sandbox: four public GitHub repos cloned in pure Elixir
(no git binary, in-memory), mounted programmatically one per loop
iteration, searched with a single grep whose scope grows with the
mount table, then shrunk again with umount. Closes with Exgit.FS.grep
streaming matches host-side off the unmounted clone.

Also adds examples/**/*.exs to the formatter inputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2mmV55riKhWgzBS8G2oMT
…ndary

A nonzero exit from any command now raises, so the deterministic
transcript genuinely doubles as a smoke test. The lazy-clone comment
notes that Exgit.FS.grep requires an eager clone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2mmV55riKhWgzBS8G2oMT
The Shell helper hid the load-bearing line of the whole stack.
Every exec is now written the way a real caller writes it:
{%{exit_code: 0} = result, bash} = JustBash.exec(bash, cmd) —
execute, destructure, and crash-assert in one match, with the full
Result visible in the MatchError on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2mmV55riKhWgzBS8G2oMT
@ivarvong ivarvong changed the title docs: multi-repo grep demo — git mounts as agent session sandboxes docs: stateful agent session demo — the sandbox is a value Jul 2, 2026
ivarvong and others added 2 commits July 2, 2026 12:48
Rename multi_repo_grep.exs -> agent_session.exs and restructure into
four beats: build the world (mount-per-loop, growing grep), state
threading (redirect one exec's output, read it in the next), fork and
roll back (checkpoint = session, rm in the live session, checkpoint
asserted intact), shrink the world (umount). Drops the host-side
Exgit.FS.grep coda — backends now appear only at mount lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2mmV55riKhWgzBS8G2oMT
@ivarvong ivarvong merged commit 2f1e2c2 into main Jul 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant