Agent FS
agent_fs gives each agent its own version-controlled file system, backed by a git repository. Every write can be committed automatically, so the agent's files have a full history. The agent can diff, restore, revert, branch, tag and stash, just like a developer working with git.
Typical uses:
- a durable, auditable workspace for an agent's notes, plans, generated code or documents;
- letting an agent experiment on a branch and merge only what works;
- rolling a file back to the version that was known to be good.
Installation
agent_fs is pure Python with no dependencies; it shells out to the git command-line tool, which must be installed. Requires Python ≥ 3.10.
The package lives in agent-memories/fs. Its pyproject.toml refers to a README.md that is not in the folder and uses an unusual build backend, so pip install may fail. In that case, put the folder on PYTHONPATH:
export PYTHONPATH=$PYTHONPATH:/path/to/agent-memories/fs
from agent_fs import AgentFS
Creating a File System
fs = AgentFS("/var/lib/agents", subject_id="planner-agent",
author_name="Planner Agent", author_email="planner@agents.local")
- The root is
<path>/<subject_id>(here/var/lib/agents/planner-agent). It is created if missing. - On first use, a git repository is initialised there with an empty initial commit, so
HEADis always valid. - All paths passed to the API are relative to that root. A path that resolves outside the root (for example
../other-agent/x) raisesAgentFSError.
Working with Files
sha = fs.write("notes/plan.md", "# Plan\n- step 1\n") # writes and commits
fs.write("notes/plan.md", "# Plan\n- step 1\n- step 2\n", message="add step 2")
fs.write("data/blob.bin", b"\x00\x01", auto_commit=False) # write without committing
fs.read("notes/plan.md") # str (UTF-8)
fs.read_bytes("data/blob.bin") # bytes
fs.copy("notes/plan.md", "notes/plan-v2.md")
fs.move("notes/plan-v2.md", "archive/plan-v2.md")
fs.delete("archive/plan-v2.md")
fs.mkdir("reports/2026")
fs.exists("notes/plan.md") # True
fs.is_dir("reports") # True
fs.list() # top-level entries (excluding .git)
fs.list("notes", recursive=True) # depth-first listing
write, delete, move and copy commit by default, returning the new commit hash. The default messages are write: <path>, delete: <path>, move: <src> -> <dst> and copy: <src> -> <dst>. Pass auto_commit=False to batch several changes into one commit:
fs.write("a.txt", "A", auto_commit=False)
fs.write("b.txt", "B", auto_commit=False)
fs.stage() # git add -A (or fs.stage("a.txt", "b.txt"))
fs.commit("add a and b")
Missing files raise AgentFSFileNotFoundError.
History and Diffs
fs.head() # current commit hash
for c in fs.log(limit=5): # newest first
print(c.short_hash, c.date, c.message, c.files_changed)
fs.log(file_path="notes/plan.md") # commits touching one file
fs.log(branch="experiment")
fs.status() # [StatusEntry(path, staged, unstaged), ...]
fs.diff() # unstaged changes
fs.diff(staged=True) # staged changes
fs.diff(from_ref="HEAD~1") # HEAD~1 vs HEAD
fs.diff(from_ref="main", to_ref="experiment", file_path="notes/plan.md")
CommitInfo has hash, short_hash, author, email, date, message and files_changed.
Restoring and Undoing
old = fs.read_at("notes/plan.md", "HEAD~2") # content at any commit/branch/tag
raw = fs.read_bytes_at("data/blob.bin", "v1")
fs.restore("notes/plan.md", "HEAD~2") # write the old content back (and commit)
fs.revert(commit_hash) # new commit undoing a commit
fs.cherry_pick(commit_hash) # apply a commit onto the current branch
fs.checkout("HEAD~1", file_path="notes/plan.md") # restore one file in the working tree
Branches, Merges and Tags
fs.create_branch("experiment") # from HEAD (or from_ref="v1")
fs.checkout("experiment")
fs.write("notes/plan.md", "# Experimental plan\n")
fs.checkout("master") # the default branch name depends on your git config
fs.merge("experiment", message="merge experiment")
fs.delete_branch("experiment") # force=True for unmerged branches
fs.branches() # ["experiment", "master"]
fs.current_branch()
fs.checkout("hotfix", create=True) # create and switch
fs.tag("v1") # lightweight tag
fs.tag("v2", message="known-good plan") # annotated tag
fs.tags() # newest first
fs.delete_tag("v1")
merge raises MergeConflictError on conflicts. Branch errors raise BranchError, and an unknown ref passed to checkout raises RefNotFoundError.
Stash
fs.write("notes/wip.md", "half done", auto_commit=False)
fs.stage("notes/wip.md") # new files must be staged; git stash ignores untracked files
fs.stash("work in progress")
fs.stash_list() # [{"index": 0, "ref": "stash@{0}", "message": "On master: work in progress"}]
fs.stash_apply(0) # apply, keep the entry
fs.stash_pop(0) # apply and remove
fs.stash_drop(0) # remove without applying
Repository Summary
fs.info()
# {"root": "...", "branch": "master", "head": "...", "branches": [...], "tags": [...], "status": [...]}
Exceptions
All errors derive from AgentFSError:
| Exception | Raised when |
|---|---|
AgentFSFileNotFoundError |
A file to read, delete, move or copy does not exist |
CommitError |
A commit, revert or cherry-pick fails |
BranchError |
Creating or deleting a branch fails |
MergeConflictError |
A merge has conflicts |
RefNotFoundError |
A ref or path at a ref does not exist |
AgentFSError |
Any other git failure, or a path that escapes the root |
API Reference
| Area | Methods |
|---|---|
| Files | read, read_bytes, write, delete, move, copy, mkdir, exists, is_dir, list |
| Commits | stage, unstage, commit, head, status, log, diff |
| Branches | branches, current_branch, create_branch, delete_branch, checkout, merge |
| Tags | tag, tags, delete_tag |
| Versions | read_at, read_bytes_at, restore, revert, cherry_pick |
| Stash | stash, stash_pop, stash_apply, stash_drop, stash_list |
| Info | info |