···4455# Tools
6677-| tool | description | params=[default] |
88-| ---------- | ----------------------------------- | --------------------------------------------------- |
99-| `list` | list all sessions | `dir=[pwd]` constraint, `active=[null]` |
1010-| `call` | send a request or message | `session=[new]`, `elaborate=[true]`, `fork=[false]` |
1111-| `response` | multitool for queues responses, get | `get=[top]`, `list=[false]` |
77+| tool | description | params=[default] |
88+| -------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
99+| `list-session` | list all sessions | `dir=[pwd]` constraint, `active=[null]` |
1010+| `send-message` | create or update a message | `session=[new]`, `elaborate=[false]`, `fork=[false]`, `iterate=false`, `id=[uuid]`, `draft=[false]`, `reply=[]` |
1111+| `buffer` | list or accept buffered messages for this agent | `accept=[false]`, `replies=[null]`, session=[this]` |
1212+1313+## Messages
1414+1515+- `id` a unique id, auto generated as id if not provided
1616+- `created_at` (nanoseconds), time of submission
1717+- `updated_at` (nanoseconds), time last updated
1818+- `from` source session id
1919+- `to` destination session id
2020+- `in-reply-to` a parent message id, used for responses and other threads.
2121+- `draft`, boolean indicating message is not ready to be sent
2222+- `buffered`, boolean indicating message is buffered and must be accepted first
2323+- `accepted_at`, time when message is accepted
2424+- `accepted_by`, the session that actually accepted the message, typically null (unless not the `to`)
2525+- `initial`, the user's initial message
2626+- `message`, message in it's ready form
2727+2828+### Session
2929+3030+- `new` create a new session
3131+- `fork` forks the current session
3232+- `[id]`, send message to a session identified by this id. can be a substring of the session but must match only one session id. will be expanded.
3333+3434+#### Fork
3535+3636+Call can accept a `fork` parameter (must not have a session set or a `session=fork` parameter). A fork of the session is created, and the message is sent to that agent. Fork can be: true (for most recent message id), a number (number of messages back to fork from).
3737+3838+### Buffering
3939+4040+Messages sent (without a reply field) to an existing session (not `new` or `fork` sessions) are not automatically received by the other agent, to allow the user to control the timing of messages. The `buffered` tool can list messages (if no accept parameter set), or be used to accept messages. `accept=true` or `oldest` will take the oldest message off the queue. `newest` will take the most recent. a number will select the message by id, and if that fails to match, by that accept as an ordinal number. Lists of messages are sent with an instruction to treat this as a list to be shown to the user, and to not process the messages now.
4141+4242+## Draft
4343+4444+A message can be built as a draft. This allows messages to be iterated on and improved over time, before being sent (by changing the state to buffered or ready).
4545+4646+## Storage
4747+4848+By default, a `~/.local/share/opencode/storage/call-response` directory will be used to store a [Turso AgentFS](https://github.com/tursodatabase/agentfs) sqlite-compatible database for each agent that is sent messages. These will be created on the fly as necessary.
4949+5050+Messages are stored as JSON files in agentfs, by their id.
5151+5252+# Tool Calling Tracing
5353+5454+`opencode-call-response` also includes a plugin that can be used to write agentfs tool call logs!
5555+5656+# Configuration
5757+5858+configuration is a toml file or json.
5959+6060+- storage directory. default `~/.local/share/opencode/storage/call-response`.
6161+- tools enable, array of tools. default true (all tools).
6262+- tracing enable. default true.
6363+- parameter defaults ought be broadly configurable.
6464+- turso cdc disable (enabled by default)
6565+6666+# Future Work
6767+6868+- configuration to constrain parameters shown/available, allowing additional lockdown. examples: restrict to only current directory sessions, restrict to only active sessions.
6969+- `elaborate` to allow for an initial prompt from an agent to receive additional processing. probably via a fork? before being marked ready.
7070+- explore other addressing modes for `fork`
7171+- a raw file mode, without agentfs.
+1057
doc/discovery/agentfs-api.md
···11+# AgentFS API Discovery
22+33+> An in-depth exploration of AgentFS database schema, library API, and usage patterns
44+55+## Overview
66+77+AgentFS is a filesystem explicitly designed for AI agents, built on top of SQLite (via libSQL/Turso). It provides three core storage abstractions:
88+99+1. **Virtual Filesystem** - POSIX-like filesystem with files, directories, symlinks, and special files
1010+2. **Key-Value Store** - Simple JSON-serialized key-value storage for agent state
1111+3. **Tool Call Audit Trail** - Insert-only log of tool/function invocations for debugging and compliance
1212+1313+**Key Design Philosophy**: Everything is stored in a single SQLite database file, enabling:
1414+- **Auditability** - Complete history queryable via SQL
1515+- **Reproducibility** - `cp agent.db snapshot.db` captures exact state
1616+- **Portability** - Single file can be moved, versioned, or deployed anywhere
1717+1818+## Can AgentFS be used without FUSE?
1919+2020+**Yes!** AgentFS provides full-featured SDKs for Rust, Python, and TypeScript that operate directly on the SQLite database. The FUSE/NFS mount is optional and primarily useful for:
2121+- Running existing tools that expect a POSIX filesystem
2222+- Interactive shell sessions (`agentfs run`)
2323+- Debugging and inspection
2424+2525+For programmatic access, **use the SDKs directly** - they're faster, more reliable, and give you finer control.
2626+2727+---
2828+2929+## Database Schema (Version 0.4)
3030+3131+### Schema Versioning
3232+3333+AgentFS uses schema versioning to handle migrations. Current version is stored in `fs_config`:
3434+3535+```sql
3636+SELECT value FROM fs_config WHERE key = 'schema_version';
3737+-- Returns: '0.4'
3838+```
3939+4040+Version history:
4141+- **0.0**: Base schema (fs_inode, fs_dentry, fs_data, fs_symlink, fs_config, kv_store, tool_calls)
4242+- **0.2**: Added `nlink` column to `fs_inode` for hard link counts
4343+- **0.4**: Added nanosecond precision timestamps (`atime_nsec`, `mtime_nsec`, `ctime_nsec`) and `rdev` for special files
4444+4545+---
4646+4747+### 1. Virtual Filesystem Tables
4848+4949+#### `fs_config` - Filesystem Configuration
5050+5151+Immutable configuration set at filesystem creation.
5252+5353+```sql
5454+CREATE TABLE fs_config (
5555+ key TEXT PRIMARY KEY,
5656+ value TEXT NOT NULL
5757+)
5858+```
5959+6060+**Required Keys**:
6161+| Key | Value | Description |
6262+|-----|--------|-------------|
6363+| `chunk_size` | '4096' | Size of data chunks in bytes (default: 4096) |
6464+| `schema_version` | '0.4' | Current schema version |
6565+6666+**Note**: `chunk_size` is immutable after creation. All chunks except the last chunk of a file must be exactly `chunk_size` bytes.
6767+6868+---
6969+7070+#### `fs_inode` - File and Directory Metadata
7171+7272+Stores file/directory metadata using a Unix-like inode design.
7373+7474+```sql
7575+CREATE TABLE fs_inode (
7676+ ino INTEGER PRIMARY KEY AUTOINCREMENT,
7777+ mode INTEGER NOT NULL,
7878+ nlink INTEGER NOT NULL DEFAULT 0,
7979+ uid INTEGER NOT NULL DEFAULT 0,
8080+ gid INTEGER NOT NULL DEFAULT 0,
8181+ size INTEGER NOT NULL DEFAULT 0,
8282+ atime INTEGER NOT NULL,
8383+ mtime INTEGER NOT NULL,
8484+ ctime INTEGER NOT NULL,
8585+ rdev INTEGER NOT NULL DEFAULT 0,
8686+ atime_nsec INTEGER NOT NULL DEFAULT 0,
8787+ mtime_nsec INTEGER NOT NULL DEFAULT 0,
8888+ ctime_nsec INTEGER NOT NULL DEFAULT 0
8989+)
9090+```
9191+9292+**Fields**:
9393+- `ino` - Unique inode number (root is always ino=1)
9494+- `mode` - Unix mode bits combining file type and permissions (see Mode Encoding below)
9595+- `nlink` - Hard link count (0 for unlinked files being deleted, 2+ for directories)
9696+- `uid` / `gid` - Owner user/group IDs
9797+- `size` - File size in bytes (0 for directories)
9898+- `atime` / `mtime` / `ctime` - Access/modification/change time (Unix epoch, seconds)
9999+- `atime_nsec` / `mtime_nsec` / `ctime_nsec` - Nanosecond components of timestamps
100100+- `rdev` - Device number for character/block devices (major/minor encoded)
101101+102102+**Mode Encoding** (32-bit):
103103+104104+```
105105+File type (upper 4 bits):
106106+ 0o170000 - File type mask (S_IFMT)
107107+ 0o100000 - Regular file (S_IFREG)
108108+ 0o040000 - Directory (S_IFDIR)
109109+ 0o120000 - Symbolic link (S_IFLNK)
110110+ 0o010000 - FIFO/named pipe (S_IFIFO)
111111+ 0o020000 - Character device (S_IFCHR)
112112+ 0o060000 - Block device (S_IFBLK)
113113+ 0o140000 - Socket (S_IFSOCK)
114114+115115+Permissions (lower 12 bits):
116116+ 0o000777 - Permission bits (rwxrwxrwx)
117117+118118+Example modes:
119119+ 0o100644 - Regular file, rw-r--r--
120120+ 0o040755 - Directory, rwxr-xr-x
121121+ 0o120777 - Symlink, rwxrwxrwx
122122+```
123123+124124+**Special Inodes**:
125125+- Inode 1 is always the root directory
126126+127127+**Consistency Rules**:
128128+- Root inode (ino=1) MUST always exist
129129+- Directories MUST have mode with S_IFDIR bit set
130130+- Regular files MUST have mode with S_IFREG bit set
131131+- File size MUST match total size of all `fs_data` chunks
132132+133133+---
134134+135135+#### `fs_dentry` - Directory Entries
136136+137137+Maps filenames to inodes (the filesystem namespace).
138138+139139+```sql
140140+CREATE TABLE fs_dentry (
141141+ id INTEGER PRIMARY KEY AUTOINCREMENT,
142142+ name TEXT NOT NULL,
143143+ parent_ino INTEGER NOT NULL,
144144+ ino INTEGER NOT NULL,
145145+ UNIQUE(parent_ino, name)
146146+)
147147+148148+CREATE INDEX idx_fs_dentry_parent ON fs_dentry(parent_ino, name)
149149+```
150150+151151+**Fields**:
152152+- `name` - Basename (filename or directory name)
153153+- `parent_ino` - Parent directory inode number
154154+- `ino` - Inode this entry points to
155155+156156+**Notes**:
157157+- Multiple dentries MAY point to the same inode (hard links)
158158+- Root directory (ino=1) has no dentry (no parent)
159159+- Link count in `fs_inode.nlink` tracks hard links
160160+161161+**Path Resolution Algorithm**:
162162+```sql
163163+-- To resolve "/a/b/c":
164164+1. Start at ino=1 (root)
165165+2. SELECT ino FROM fs_dentry WHERE parent_ino=1 AND name='a'
166166+3. SELECT ino FROM fs_dentry WHERE parent_ino=<result> AND name='b'
167167+4. SELECT ino FROM fs_dentry WHERE parent_ino=<result> AND name='c'
168168+```
169169+170170+---
171171+172172+#### `fs_data` - File Content Chunks
173173+174174+Stores file content in fixed-size chunks.
175175+176176+```sql
177177+CREATE TABLE fs_data (
178178+ ino INTEGER NOT NULL,
179179+ chunk_index INTEGER NOT NULL,
180180+ data BLOB NOT NULL,
181181+ PRIMARY KEY (ino, chunk_index)
182182+)
183183+```
184184+185185+**Fields**:
186186+- `ino` - Inode number
187187+- `chunk_index` - Zero-based chunk index (chunk 0 = bytes 0 to chunk_size-1)
188188+- `data` - Binary content (BLOB), exactly `chunk_size` bytes except last chunk
189189+190190+**Chunk Access**:
191191+```sql
192192+-- Read entire file
193193+SELECT data FROM fs_data WHERE ino=? ORDER BY chunk_index ASC;
194194+195195+-- Read bytes 1000-5000 (with chunk_size=4096)
196196+SELECT chunk_index, data FROM fs_data
197197+WHERE ino=? AND chunk_index >= 0 AND chunk_index <= 1
198198+ORDER BY chunk_index ASC;
199199+-- Returns chunk 0 (bytes 0-4095) and chunk 1 (bytes 4096-8191)
200200+-- Extract bytes 1000-4095 from chunk 0
201201+-- Extract bytes 0-904 from chunk 1
202202+```
203203+204204+**Byte Offset Calculation**:
205205+- `chunk_index = byte_offset / chunk_size`
206206+- `offset_in_chunk = byte_offset % chunk_size`
207207+- `byte_offset = chunk_index * chunk_size + offset_in_chunk`
208208+209209+**Consistency Rules**:
210210+- Directories MUST NOT have data chunks
211211+- All chunks except the last chunk MUST be exactly `chunk_size` bytes
212212+- Last chunk MAY be smaller than `chunk_size`
213213+214214+---
215215+216216+#### `fs_symlink` - Symbolic Link Targets
217217+218218+```sql
219219+CREATE TABLE fs_symlink (
220220+ ino PRIMARY KEY,
221221+ target TEXT NOT NULL
222222+)
223223+```
224224+225225+**Fields**:
226226+- `ino` - Inode number of the symlink (must have S_IFLNK mode)
227227+- `target` - Target path (may be absolute or relative)
228228+229229+**Note**: Symlink resolution (following symlinks) is implementation-defined. The SDKs implement depth-limited following (max 40 levels).
230230+231231+---
232232+233233+### 2. Overlay Filesystem Tables (Optional)
234234+235235+The overlay filesystem provides copy-on-write semantics, layering a writable delta on top of a read-only base.
236236+237237+#### `fs_whiteout` - Deletion Markers
238238+239239+Tracks deleted paths to prevent base layer visibility.
240240+241241+```sql
242242+CREATE TABLE fs_whiteout (
243243+ path TEXT PRIMARY KEY,
244244+ parent_path TEXT NOT NULL,
245245+ created_at INTEGER NOT NULL
246246+)
247247+248248+CREATE INDEX idx_fs_whiteout_parent ON fs_whiteout(parent_path)
249249+```
250250+251251+**Fields**:
252252+- `path` - Normalized absolute path that has been deleted
253253+- `parent_path` - Parent directory path (for efficient child lookups)
254254+- `created_at` - Deletion timestamp (Unix epoch, seconds)
255255+256256+**Purpose**: When deleting `/a.txt` that exists in base layer, insert whiteout. On lookup, if whiteout exists, return "not found" instead of falling through to base.
257257+258258+**Whiteout Creation**:
259259+```sql
260260+INSERT INTO fs_whiteout (path, parent_path, created_at)
261261+VALUES (?, ?, ?)
262262+ON CONFLICT(path) DO UPDATE SET created_at = excluded.created_at
263263+```
264264+265265+---
266266+267267+#### `fs_origin` - Copy-Up Origin Tracking
268268+269269+Maintains inode consistency across layers for FUSE cache compatibility.
270270+271271+```sql
272272+CREATE TABLE fs_origin (
273273+ delta_ino INTEGER PRIMARY KEY,
274274+ base_ino INTEGER NOT NULL
275275+)
276276+```
277277+278278+**Fields**:
279279+- `delta_ino` - Inode number in delta layer
280280+- `base_ino` - Original inode number from base layer
281281+282282+**Purpose**: When copying a file from base to delta (copy-up), preserve the original base inode number. This prevents ENOENT errors when FUSE kernel caches inodes.
283283+284284+---
285285+286286+#### `fs_overlay_config` - Overlay Configuration
287287+288288+```sql
289289+CREATE TABLE fs_overlay_config (
290290+ key TEXT PRIMARY KEY,
291291+ value TEXT NOT NULL
292292+)
293293+```
294294+295295+**Required Keys**:
296296+| Key | Value | Description |
297297+|-----|--------|-------------|
298298+| `base_path` | '/absolute/path' | Canonical path to base directory |
299299+300300+---
301301+302302+### 3. Key-Value Store Table
303303+304304+#### `kv_store` - Agent State Storage
305305+306306+```sql
307307+CREATE TABLE kv_store (
308308+ key TEXT PRIMARY KEY,
309309+ value TEXT NOT NULL,
310310+ created_at INTEGER DEFAULT (unixepoch()),
311311+ updated_at INTEGER DEFAULT (unixepoch())
312312+)
313313+314314+CREATE INDEX idx_kv_store_created_at ON kv_store(created_at)
315315+```
316316+317317+**Fields**:
318318+- `key` - Unique key identifier (any naming convention supported, e.g., `user:123`, `session:state`)
319319+- `value` - JSON-serialized value
320320+- `created_at` / `updated_at` - Timestamps (Unix epoch, seconds)
321321+322322+**Upsert Pattern**:
323323+```sql
324324+INSERT INTO kv_store (key, value, updated_at)
325325+VALUES (?, ?, unixepoch())
326326+ON CONFLICT(key) DO UPDATE SET
327327+ value = excluded.value,
328328+ updated_at = unixepoch()
329329+```
330330+331331+**Use Cases**:
332332+- Agent preferences and configuration
333333+- Session state and context
334334+- Conversation history (optional extension)
335335+- Structured data that doesn't fit filesystem model
336336+337337+---
338338+339339+### 4. Tool Call Audit Trail Table
340340+341341+#### `tool_calls` - Tool Invocation Log
342342+343343+Insert-only audit log for debugging and compliance.
344344+345345+```sql
346346+CREATE TABLE tool_calls (
347347+ id INTEGER PRIMARY KEY AUTOINCREMENT,
348348+ name TEXT NOT NULL,
349349+ parameters TEXT,
350350+ result TEXT,
351351+ error TEXT,
352352+ status TEXT NOT NULL DEFAULT 'pending',
353353+ started_at INTEGER NOT NULL,
354354+ completed_at INTEGER,
355355+ duration_ms INTEGER
356356+)
357357+358358+CREATE INDEX idx_tool_calls_name ON tool_calls(name)
359359+CREATE INDEX idx_tool_calls_started_at ON tool_calls(started_at)
360360+```
361361+362362+**Fields**:
363363+- `id` - Unique tool call identifier
364364+- `name` - Tool name (e.g., 'read_file', 'web_search', 'execute_code')
365365+- `parameters` - JSON-serialized input parameters (NULL if no parameters)
366366+- `result` - JSON-serialized result (NULL if error)
367367+- `error` - Error message (NULL if success)
368368+- `status` - 'pending', 'success', or 'error'
369369+- `started_at` - Invocation timestamp (Unix epoch, seconds)
370370+- `completed_at` - Completion timestamp (NULL if pending)
371371+- `duration_ms` - Execution duration in milliseconds (NULL if pending)
372372+373373+**Consistency Rules**:
374374+1. Exactly one of `result` or `error` should be non-NULL (mutual exclusion)
375375+2. `completed_at` MUST always be set (no NULL values on completion)
376376+3. `duration_ms` MUST equal `(completed_at - started_at) * 1000`
377377+4. Parameters and results MUST be valid JSON strings when present
378378+5. Records MUST NOT be updated or deleted (insert-only)
379379+380380+**Performance Analysis Query**:
381381+```sql
382382+SELECT
383383+ name,
384384+ COUNT(*) as total_calls,
385385+ SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as successful,
386386+ SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as failed,
387387+ AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms ELSE 0 END) as avg_duration_ms
388388+FROM tool_calls
389389+GROUP BY name
390390+ORDER BY total_calls DESC
391391+```
392392+393393+---
394394+395395+## SDK API Reference
396396+397397+### Rust SDK (`agentfs_sdk` crate)
398398+399399+**Installation**:
400400+```toml
401401+[dependencies]
402402+agentfs-sdk = "latest"
403403+```
404404+405405+**Opening an AgentFS Database**:
406406+407407+```rust
408408+use agentfs_sdk::{AgentFS, AgentFSOptions};
409409+410410+// Persistent storage with identifier
411411+let agent = AgentFS::open(AgentFSOptions::with_id("my-agent")).await?;
412412+// Creates: .agentfs/my-agent.db
413413+414414+// Ephemeral in-memory database
415415+let agent = AgentFS::open(AgentFSOptions::ephemeral()).await?;
416416+417417+// Custom database path
418418+let agent = AgentFS::open(AgentFSOptions::with_path("./data/mydb.db")).await?;
419419+420420+// With overlay filesystem (copy-on-write)
421421+let agent = AgentFS::open(
422422+ AgentFSOptions::with_id("my-overlay")
423423+ .with_base("/path/to/project")
424424+).await?;
425425+```
426426+427427+**Filesystem Operations**:
428428+429429+```rust
430430+use agentfs_sdk::{DEFAULT_DIR_MODE, DEFAULT_FILE_MODE};
431431+432432+// Create directory
433433+agent.fs.mkdir("/documents", 0, 0).await?;
434434+435435+// Write file (creates parent directories automatically)
436436+agent.fs.write_file("/documents/readme.txt", b"Hello, world!").await?;
437437+438438+// Read file
439439+let content = agent.fs.read_file("/documents/readme.txt").await?;
440440+441441+// Get file stats
442442+let stats = agent.fs.stat("/documents/readme.txt").await?;
443443+if let Some(s) = stats {
444444+ println!("Size: {} bytes", s.size);
445445+ println!("Is file: {}", s.is_file());
446446+}
447447+448448+// List directory
449449+let entries = agent.fs.readdir("/documents").await?;
450450+if let Some(files) = entries {
451451+ for file in files {
452452+ println!(" {}", file);
453453+ }
454454+}
455455+456456+// Create file with specific mode
457457+let (stats, file) = agent.fs.create_file(
458458+ "/data/test.txt",
459459+ DEFAULT_FILE_MODE,
460460+ 0,
461461+ 0,
462462+).await?;
463463+file.pwrite(0, b"Hello").await?;
464464+465465+// Delete file
466466+agent.fs.unlink("/documents/readme.txt").await?;
467467+468468+// Remove directory (recursive)
469469+agent.fs.rm("/documents", true, false).await?;
470470+471471+// Rename/move
472472+agent.fs.rename("/old.txt", "/new.txt").await?;
473473+474474+// Copy file
475475+agent.fs.copy_file("/source.txt", "/dest.txt").await?;
476476+477477+// Check existence
478478+let _ = agent.fs.access("/path/to/file").await?;
479479+```
480480+481481+**Key-Value Operations**:
482482+483483+```rust
484484+use serde_json::json;
485485+486486+// Set a value
487487+agent.kv.set("user:preferences", &json!({"theme": "dark"})).await?;
488488+489489+// Get a value
490490+let prefs: Option<serde_json::Value> = agent.kv.get("user:preferences").await?;
491491+492492+// Delete a value
493493+agent.kv.delete("user:preferences").await?;
494494+495495+// List all keys
496496+let keys = agent.kv.keys().await?;
497497+for key in keys {
498498+ println!("{}", key);
499499+}
500500+```
501501+502502+**Tool Call Tracking**:
503503+504504+```rust
505505+use serde_json::json;
506506+507507+// Start a tool call
508508+let call_id = agent.tools.start("search", Some(json!({"query": "Rust"}))).await?;
509509+510510+// Mark as successful
511511+agent.tools.success(call_id, Some(json!({"results": ["doc1", "doc2"]})).await?;
512512+513513+// Or mark as failed
514514+agent.tools.error(call_id, "Connection timeout").await?;
515515+516516+// Record a completed call (spec-compliant)
517517+agent.tools.record(
518518+ "search",
519519+ 1234567890,
520520+ 1234567892,
521521+ Some(json!({"query": "Rust"})),
522522+ Some(json!({"results": ["doc1", "doc2"]})),
523523+ None,
524524+).await?;
525525+526526+// Get tool call
527527+let call = agent.tools.get(call_id).await?;
528528+if let Some(c) = call {
529529+ println!("Tool: {}, Status: {}", c.name, c.status);
530530+}
531531+532532+// Get recent tool calls
533533+let recent = agent.tools.recent(Some(10)).await?;
534534+535535+// Get statistics for a tool
536536+let stats = agent.tools.stats_for("search").await?;
537537+if let Some(s) = stats {
538538+ println!("Total: {}, Success: {}, Avg: {:.2}ms",
539539+ s.total_calls, s.successful, s.avg_duration_ms);
540540+}
541541+```
542542+543543+**Low-Level Database Access**:
544544+545545+```rust
546546+// Get a connection for custom queries
547547+let conn = agent.get_connection().await?;
548548+549549+// Execute custom SQL
550550+let mut rows = conn.query("SELECT * FROM fs_inode WHERE ino > ?", (10,)).await?;
551551+while let Some(row) = rows.next().await? {
552552+ // Process row...
553553+}
554554+```
555555+556556+---
557557+558558+### Python SDK (`agentfs-sdk` pip package)
559559+560560+**Installation**:
561561+```bash
562562+pip install agentfs-sdk
563563+```
564564+565565+**Opening an AgentFS Database**:
566566+567567+```python
568568+import asyncio
569569+from agentfs_sdk import AgentFS, AgentFSOptions
570570+571571+async def main():
572572+ # Persistent storage with identifier
573573+ agent = await AgentFS.open(AgentFSOptions(id='my-agent'))
574574+ # Creates: .agentfs/my-agent.db
575575+576576+ # Ephemeral in-memory database
577577+ agent = await AgentFS.open()
578578+579579+ # Custom database path
580580+ agent = await AgentFS.open(AgentFSOptions(path='./data/mydb.db'))
581581+582582+ # Context manager support
583583+ async with await AgentFS.open(AgentFSOptions(id='my-agent')) as agent:
584584+ await agent.kv.set('key', 'value')
585585+ # Automatically closed on exit
586586+```
587587+588588+**Filesystem Operations**:
589589+590590+```python
591591+# Write a file (creates parent directories automatically)
592592+await agent.fs.write_file('/data/config.json', '{"key": "value"}')
593593+594594+# Read a file
595595+content = await agent.fs.read_file('/data/config.json')
596596+597597+# Read as bytes
598598+data = await agent.fs.read_file('/data/image.png', encoding=None)
599599+600600+# List directory
601601+entries = await agent.fs.readdir('/data')
602602+603603+# Get file stats
604604+stats = await agent.fs.stat('/data/config.json')
605605+print(f"Size: {stats.size} bytes")
606606+print(f"Modified: {stats.mtime}")
607607+print(f"Is file: {stats.is_file()}")
608608+609609+# Create directory
610610+await agent.fs.mkdir('/new-folder')
611611+612612+# Delete a file
613613+await agent.fs.unlink('/data/config.json')
614614+615615+# Remove directory (recursive)
616616+await agent.fs.rm('/folder', recursive=True, force=False)
617617+618618+# Rename/move
619619+await agent.fs.rename('/old.txt', '/new.txt')
620620+621621+# Copy file
622622+await agent.fs.copy_file('/source.txt', '/dest.txt')
623623+624624+# Check existence
625625+try:
626626+ await agent.fs.access('/documents/readme.txt')
627627+ print("File exists")
628628+except Exception:
629629+ print("File not found")
630630+```
631631+632632+**Key-Value Operations**:
633633+634634+```python
635635+# Set a value
636636+await agent.kv.set('user:123', {'name': 'Alice', 'age': 30})
637637+638638+# Get a value
639639+user = await agent.kv.get('user:123')
640640+641641+# Delete a value
642642+await agent.kv.delete('user:123')
643643+644644+# List by prefix (custom implementation)
645645+# Note: Python SDK doesn't have built-in prefix listing
646646+# Use direct SQL access for prefix queries:
647647+conn = agent.fs._db
648648+cursor = await conn.execute("SELECT key FROM kv_store WHERE key LIKE ?", ('user:%',))
649649+```
650650+651651+**Tool Call Tracking**:
652652+653653+```python
654654+# Start a tool call
655655+call_id = await agent.tools.start('search', {'query': 'Python'})
656656+657657+# Mark as successful
658658+await agent.tools.success(call_id, {'results': ['result1', 'result2']})
659659+660660+# Or mark as failed
661661+await agent.tools.error(call_id, 'Connection timeout')
662662+663663+# Record a completed call
664664+await agent.tools.record(
665665+ 'search',
666666+ started_at=1234567890,
667667+ completed_at=1234567892,
668668+ parameters={'query': 'Python'},
669669+ result={'results': ['result1', 'result2']}
670670+)
671671+672672+# Query tool calls
673673+calls = await agent.tools.get_by_name('search', limit=10)
674674+recent = await agent.tools.get_recent(since=1234567890)
675675+676676+# Get statistics
677677+stats = await agent.tools.get_stats()
678678+for stat in stats:
679679+ print(f"{stat.name}: {stat.successful}/{stat.total_calls} successful")
680680+```
681681+682682+---
683683+684684+### TypeScript SDK (`agentfs-sdk` npm package)
685685+686686+**Installation**:
687687+```bash
688688+npm install agentfs-sdk
689689+```
690690+691691+**Opening an AgentFS Database**:
692692+693693+```typescript
694694+import { AgentFS } from 'agentfs-sdk';
695695+696696+// Persistent storage with identifier
697697+const agent = await AgentFS.open({ id: 'my-agent' });
698698+// Creates: .agentfs/my-agent.db
699699+700700+// Or use ephemeral in-memory database
701701+const ephemeralAgent = await AgentFS.open();
702702+```
703703+704704+**Filesystem Operations**:
705705+706706+```typescript
707707+// Write files (parent dirs created automatically)
708708+await agent.fs.writeFile('/documents/readme.txt', 'Hello, world!');
709709+await agent.fs.writeFile('/data/config.json', JSON.stringify({ key: 'value' }));
710710+711711+// Write binary data
712712+const imageBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
713713+await agent.fs.writeFile('/images/icon.png', imageBuffer);
714714+715715+// Read files
716716+const content = await agent.fs.readFile('/documents/readme.txt', 'utf-8');
717717+console.log(content); // "Hello, world!"
718718+719719+const binary = await agent.fs.readFile('/images/icon.png');
720720+console.log(binary); // <Buffer 89 50 4e 47>
721721+722722+// Get file statistics
723723+const stats = await agent.fs.stat('/documents/readme.txt');
724724+console.log({
725725+ inode: stats.ino,
726726+ size: stats.size,
727727+ mode: stats.mode.toString(8), // "100644" for regular file
728728+ isFile: stats.isFile(), // true
729729+ isDirectory: stats.isDirectory(), // false
730730+ modified: new Date(stats.mtime * 1000)
731731+});
732732+733733+// List directory contents
734734+const files = await agent.fs.readdir('/documents');
735735+console.log(files); // ['readme.txt']
736736+737737+// Create directories
738738+await agent.fs.mkdir('/new-folder');
739739+740740+// Remove files and directories
741741+await agent.fs.unlink('/documents/readme.txt');
742742+await agent.fs.rmdir('/empty-folder');
743743+await agent.fs.rm('/folder', { recursive: true, force: true });
744744+745745+// Rename/move files
746746+await agent.fs.rename('/old-name.txt', '/new-name.txt');
747747+748748+// Copy files
749749+await agent.fs.copyFile('/source.txt', '/destination.txt');
750750+751751+// Check file access (existence)
752752+try {
753753+ await agent.fs.access('/documents/readme.txt');
754754+ console.log('File exists');
755755+} catch (e) {
756756+ console.log('File does not exist');
757757+}
758758+```
759759+760760+**Key-Value Operations**:
761761+762762+```typescript
763763+// Key-value operations
764764+await agent.kv.set('user:preferences', { theme: 'dark' });
765765+const prefs = await agent.kv.get('user:preferences');
766766+767767+// Tool call tracking
768768+await agent.tools.record(
769769+ 'web_search',
770770+ Date.now() / 1000,
771771+ Date.now() / 1000 + 1.5,
772772+ { query: 'AI' },
773773+ { results: [...] }
774774+);
775775+```
776776+777777+---
778778+779779+## Direct SQL Access
780780+781781+Since AgentFS is just a SQLite database, you can query it directly with any SQLite client:
782782+783783+```bash
784784+# Using sqlite3 CLI
785785+sqlite3 .agentfs/my-agent.db
786786+787787+# List all files with paths (recursive CTE)
788788+WITH RECURSIVE file_tree(ino, path) AS (
789789+ SELECT 1, '/'
790790+ UNION ALL
791791+ SELECT d.ino, path || d.name || '/'
792792+ FROM fs_dentry d
793793+ JOIN file_tree ft ON d.parent_ino = ft.ino
794794+)
795795+SELECT path, i.size, i.mode
796796+FROM file_tree ft
797797+JOIN fs_inode i ON ft.ino = i.ino
798798+WHERE (i.mode & 0o170000) = 0o100000; -- Regular files only
799799+```
800800+801801+---
802802+803803+## Overlay Filesystem (Copy-on-Write)
804804+805805+The overlay filesystem combines a read-only base layer (host filesystem) with a writable delta layer (AgentFS).
806806+807807+**Lookup Semantics**:
808808+1. Check if path exists in delta layer → return delta entry
809809+2. Check if path has a whiteout → return "not found"
810810+3. Check if path exists in base layer → return base entry
811811+4. Return "not found"
812812+813813+**Whiteouts**:
814814+- Created when deleting a file that exists only in base layer
815815+- Mark paths as deleted so base layer doesn't show through
816816+- Example: Delete `/README.md` (exists in base), insert whiteout for `/README.md`
817817+818818+**Copy-Up**:
819819+- When first writing to a base file, it's copied to delta layer
820820+- Origin tracking ensures inode numbers stay consistent (FUSE cache compatibility)
821821+- Example: Read `/src/main.rs` (from base), write to it → copy to delta
822822+823823+**Usage**:
824824+```rust
825825+// Create overlay on top of project directory
826826+let agent = AgentFS::open(
827827+ AgentFSOptions::with_id("my-overlay")
828828+ .with_base("/path/to/project")
829829+).await?;
830830+831831+// All modifications go to delta (my-overlay.db)
832832+// Reads fall through to base filesystem (/path/to/project)
833833+834834+// Get modified paths
835835+let delta_paths = agent.get_delta_paths().await?;
836836+837837+// Get deleted paths (whiteouts)
838838+let whiteouts = agent.get_whiteouts().await?;
839839+```
840840+841841+### Delta-to-Base Commit: **NOT SUPPORTED**
842842+843843+**Important Limitation**: AgentFS does **NOT** provide any built-in functionality to write/commit delta changes back to the base filesystem.
844844+845845+**Design Rationale**:
846846+- The base layer is intentionally **read-only** - this enables:
847847+ - Safe sandboxing of untrusted code
848848+ - Reproducible execution (base never changes)
849849+ - Easy rollback (just discard delta database)
850850+ - Multiple independent sandboxes sharing the same base
851851+852852+**What Does Work**:
853853+- `agentfs diff <id_or_path>` - CLI command to **view** changes (shows Added/Modified/Deleted)
854854+- `get_delta_paths()` - SDK method to list all modified/new files in delta
855855+- `get_whiteouts()` - SDK method to list all deleted paths
856856+857857+**What Does NOT Work**:
858858+- No API to apply/merge delta changes into base
859859+- No `commit` or `sync-to-base` command
860860+- No built-in functionality to write delta back to base
861861+862862+**Workarounds** (if you need to apply delta changes to base):
863863+864864+1. **Manual Copy** - Iterate through delta paths and copy to base:
865865+```bash
866866+# Example shell script to apply changes
867867+agentfs diff my-overlay | while read change_type type_char path; do
868868+ case $change_type in
869869+ A|M) cp ".agentfs/my-overlay.db:$path" "/base/path/$path" ;;
870870+ D) rm "/base/path/$path" ;;
871871+ esac
872872+done
873873+```
874874+875875+2. **Use `agentfs exec`** with overlay disabled:
876876+```bash
877877+# Temporarily disable overlay, copy changes directly
878878+agentfs exec my-agent cp -r /delta/* /base/
879879+```
880880+881881+3. **Database Query** - Extract delta changes and apply via direct filesystem operations:
882882+```sql
883883+-- Get all added/modified files
884884+SELECT d.name, d.parent_ino, i.mode, i.size
885885+FROM fs_dentry d
886886+JOIN fs_inode i ON d.ino = i.ino
887887+WHERE i.ino > 1 AND (i.mode & 0o170000) = 0o100000;
888888+889889+-- Then iterate results and copy to base using your preferred method
890890+```
891891+892892+**Why No Built-in Commit**:
893893+The overlay filesystem follows Linux's `overlayfs` design, which treats the lower (base) layer as immutable. This is intentional for security and reproducibility:
894894+- AI agents can safely experiment without modifying source code
895895+- Multiple agents can run concurrently against the same project
896896+- Easy to "factory reset" by deleting the delta database
897897+- Base layer serves as a canonical, immutable reference
898898+899899+---
900900+901901+## Performance Characteristics
902902+903903+### Chunk-Based Storage
904904+905905+**Advantages**:
906906+- Efficient random access (read only needed chunks)
907907+- Sparse file support (missing chunks return zeros)
908908+- Memory-friendly for large files
909909+910910+**Trade-offs**:
911911+- Small file overhead (minimum one chunk)
912912+- Chunk boundary alignment for partial writes
913913+914914+**Chunk Size Selection**:
915915+- Default: 4096 bytes (typical filesystem block size)
916916+- Larger chunks: Better sequential read performance, more wasted space for small files
917917+- Smaller chunks: Better random access, more overhead for large files
918918+919919+### Caching
920920+921921+The Rust SDK includes an LRU cache for directory entry lookups:
922922+- Cache size: 10,000 entries
923923+- Maps `(parent_ino, name) → child_ino`
924924+- Reduces repeated path resolution queries
925925+- Especially beneficial for repeated directory listings
926926+927927+### Connection Pooling
928928+929929+All SDKs use connection pooling:
930930+- Rust: `ConnectionPool` with configurable size
931931+- Python: Turso connection reuse
932932+- TypeScript: Database connection reuse
933933+934934+---
935935+936936+## Common Patterns
937937+938938+### 1. Agent State Snapshot
939939+940940+```rust
941941+// Snapshot agent state
942942+use std::fs;
943943+fs::copy(".agentfs/my-agent.db", "snapshots/agent-backup.db")?;
944944+945945+// Later restore
946946+fs::copy("snapshots/agent-backup.db", ".agentfs/my-agent.db")?;
947947+```
948948+949949+### 2. Tool Call Analysis
950950+951951+```sql
952952+-- Find slowest tool calls
953953+SELECT name, AVG(duration_ms) as avg_ms
954954+FROM tool_calls
955955+WHERE status = 'success'
956956+GROUP BY name
957957+ORDER BY avg_ms DESC;
958958+959959+-- Find failed tool calls in last hour
960960+SELECT name, error, COUNT(*) as failures
961961+FROM tool_calls
962962+WHERE status = 'error' AND started_at > ?
963963+GROUP BY name, error
964964+ORDER BY failures DESC;
965965+```
966966+967967+### 3. File System Diff
968968+969969+```sql
970970+-- Compare two agent states (with attached databases)
971971+ATTACH DATABASE 'snapshots/before.db' AS before;
972972+973973+SELECT 'added' as change, after_d.name || '/' || after_i.name as path
974974+FROM after.fs_dentry after_d
975975+JOIN after.fs_inode after_i ON after_d.ino = after_i.ino
976976+LEFT JOIN before.fs_dentry before_d ON before_d.name = after_d.name
977977+WHERE before_d.name IS NULL
978978+979979+UNION ALL
980980+981981+SELECT 'removed' as change, before_d.name || '/' || before_i.name as path
982982+FROM before.fs_dentry before_d
983983+JOIN before.fs_inode before_i ON before_d.ino = before_i.ino
984984+LEFT JOIN after.fs_dentry after_d ON after_d.name = before_d.name
985985+WHERE after_d.name IS NULL;
986986+```
987987+988988+---
989989+990990+## Extension Points
991991+992992+The AgentFS specification includes extension points for additional functionality:
993993+994994+### Key-Value Store Extensions
995995+- Namespaced keys with hierarchy support
996996+- Value versioning/history
997997+- TTL (time-to-live) for automatic expiration
998998+- Value size limits and quotas
999999+10001000+### Filesystem Extensions
10011001+- Extended attributes table
10021002+- File ACLs and advanced permissions
10031003+- Quota tracking per user/group
10041004+- Version history and snapshots
10051005+- Content deduplication
10061006+- Compression metadata
10071007+- File checksums/hashes
10081008+10091009+### Tool Call Extensions
10101010+- Session/conversation grouping (`session_id` field)
10111011+- User attribution (`user_id` field)
10121012+- Cost tracking (`cost` field for API calls)
10131013+- Parent/child relationships for nested tool calls
10141014+- Token usage tracking
10151015+- Input/output size metrics
10161016+10171017+**Implementation Guidance**: Extensions SHOULD use separate tables to maintain referential integrity with the core schema.
10181018+10191019+---
10201020+10211021+## Summary
10221022+10231023+**AgentFS provides**:
10241024+- A complete POSIX-like filesystem implementation in SQLite
10251025+- Key-value store for agent state
10261026+- Tool call audit trail for debugging and compliance
10271027+- Overlay filesystem for copy-on-write sandboxing
10281028+- Full-featured SDKs for Rust, Python, and TypeScript
10291029+- Direct SQL access for custom queries
10301030+10311031+**Key Advantages**:
10321032+- Queryable history via SQL
10331033+- Simple snapshot/restore via file copy
10341034+- Portable single-file database
10351035+- Works without FUSE (use SDKs directly)
10361036+- Designed for AI agents (auditability, reproducibility)
10371037+10381038+**When to Use FUSE**:
10391039+- Running existing POSIX tools
10401040+- Interactive shell sessions
10411041+- Debugging filesystem behavior
10421042+10431043+**When to Use SDK**:
10441044+- Programmatic access (most cases)
10451045+- Better performance and control
10461046+- Multi-language support (Rust, Python, TypeScript)
10471047+- No filesystem mount requirements
10481048+10491049+---
10501050+10511051+## References
10521052+10531053+- [AgentFS Repository](https://github.com/tursodatabase/agentfs)
10541054+- [AgentFS Specification](.test-agent/agentfs/SPEC.md)
10551055+- [AgentFS Manual](.test-agent/agentfs/MANUAL.md)
10561056+- [Turso Database](https://github.com/tursodatabase/turso)
10571057+- [Announcement Blog Post](https://turso.tech/blog/agentfs)