Tool to send cross-session opencode messages, including as request-response pattern
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

requirements README, agentfs discovery

rektide d1a3c889 4f4c6d40

+1122 -5
+65 -5
README.md
··· 4 4 5 5 # Tools 6 6 7 - | tool | description | params=[default] | 8 - | ---------- | ----------------------------------- | --------------------------------------------------- | 9 - | `list` | list all sessions | `dir=[pwd]` constraint, `active=[null]` | 10 - | `call` | send a request or message | `session=[new]`, `elaborate=[true]`, `fork=[false]` | 11 - | `response` | multitool for queues responses, get | `get=[top]`, `list=[false]` | 7 + | tool | description | params=[default] | 8 + | -------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | 9 + | `list-session` | list all sessions | `dir=[pwd]` constraint, `active=[null]` | 10 + | `send-message` | create or update a message | `session=[new]`, `elaborate=[false]`, `fork=[false]`, `iterate=false`, `id=[uuid]`, `draft=[false]`, `reply=[]` | 11 + | `buffer` | list or accept buffered messages for this agent | `accept=[false]`, `replies=[null]`, session=[this]` | 12 + 13 + ## Messages 14 + 15 + - `id` a unique id, auto generated as id if not provided 16 + - `created_at` (nanoseconds), time of submission 17 + - `updated_at` (nanoseconds), time last updated 18 + - `from` source session id 19 + - `to` destination session id 20 + - `in-reply-to` a parent message id, used for responses and other threads. 21 + - `draft`, boolean indicating message is not ready to be sent 22 + - `buffered`, boolean indicating message is buffered and must be accepted first 23 + - `accepted_at`, time when message is accepted 24 + - `accepted_by`, the session that actually accepted the message, typically null (unless not the `to`) 25 + - `initial`, the user's initial message 26 + - `message`, message in it's ready form 27 + 28 + ### Session 29 + 30 + - `new` create a new session 31 + - `fork` forks the current session 32 + - `[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. 33 + 34 + #### Fork 35 + 36 + 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). 37 + 38 + ### Buffering 39 + 40 + 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. 41 + 42 + ## Draft 43 + 44 + 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). 45 + 46 + ## Storage 47 + 48 + 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. 49 + 50 + Messages are stored as JSON files in agentfs, by their id. 51 + 52 + # Tool Calling Tracing 53 + 54 + `opencode-call-response` also includes a plugin that can be used to write agentfs tool call logs! 55 + 56 + # Configuration 57 + 58 + configuration is a toml file or json. 59 + 60 + - storage directory. default `~/.local/share/opencode/storage/call-response`. 61 + - tools enable, array of tools. default true (all tools). 62 + - tracing enable. default true. 63 + - parameter defaults ought be broadly configurable. 64 + - turso cdc disable (enabled by default) 65 + 66 + # Future Work 67 + 68 + - configuration to constrain parameters shown/available, allowing additional lockdown. examples: restrict to only current directory sessions, restrict to only active sessions. 69 + - `elaborate` to allow for an initial prompt from an agent to receive additional processing. probably via a fork? before being marked ready. 70 + - explore other addressing modes for `fork` 71 + - a raw file mode, without agentfs.
+1057
doc/discovery/agentfs-api.md
··· 1 + # AgentFS API Discovery 2 + 3 + > An in-depth exploration of AgentFS database schema, library API, and usage patterns 4 + 5 + ## Overview 6 + 7 + AgentFS is a filesystem explicitly designed for AI agents, built on top of SQLite (via libSQL/Turso). It provides three core storage abstractions: 8 + 9 + 1. **Virtual Filesystem** - POSIX-like filesystem with files, directories, symlinks, and special files 10 + 2. **Key-Value Store** - Simple JSON-serialized key-value storage for agent state 11 + 3. **Tool Call Audit Trail** - Insert-only log of tool/function invocations for debugging and compliance 12 + 13 + **Key Design Philosophy**: Everything is stored in a single SQLite database file, enabling: 14 + - **Auditability** - Complete history queryable via SQL 15 + - **Reproducibility** - `cp agent.db snapshot.db` captures exact state 16 + - **Portability** - Single file can be moved, versioned, or deployed anywhere 17 + 18 + ## Can AgentFS be used without FUSE? 19 + 20 + **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: 21 + - Running existing tools that expect a POSIX filesystem 22 + - Interactive shell sessions (`agentfs run`) 23 + - Debugging and inspection 24 + 25 + For programmatic access, **use the SDKs directly** - they're faster, more reliable, and give you finer control. 26 + 27 + --- 28 + 29 + ## Database Schema (Version 0.4) 30 + 31 + ### Schema Versioning 32 + 33 + AgentFS uses schema versioning to handle migrations. Current version is stored in `fs_config`: 34 + 35 + ```sql 36 + SELECT value FROM fs_config WHERE key = 'schema_version'; 37 + -- Returns: '0.4' 38 + ``` 39 + 40 + Version history: 41 + - **0.0**: Base schema (fs_inode, fs_dentry, fs_data, fs_symlink, fs_config, kv_store, tool_calls) 42 + - **0.2**: Added `nlink` column to `fs_inode` for hard link counts 43 + - **0.4**: Added nanosecond precision timestamps (`atime_nsec`, `mtime_nsec`, `ctime_nsec`) and `rdev` for special files 44 + 45 + --- 46 + 47 + ### 1. Virtual Filesystem Tables 48 + 49 + #### `fs_config` - Filesystem Configuration 50 + 51 + Immutable configuration set at filesystem creation. 52 + 53 + ```sql 54 + CREATE TABLE fs_config ( 55 + key TEXT PRIMARY KEY, 56 + value TEXT NOT NULL 57 + ) 58 + ``` 59 + 60 + **Required Keys**: 61 + | Key | Value | Description | 62 + |-----|--------|-------------| 63 + | `chunk_size` | '4096' | Size of data chunks in bytes (default: 4096) | 64 + | `schema_version` | '0.4' | Current schema version | 65 + 66 + **Note**: `chunk_size` is immutable after creation. All chunks except the last chunk of a file must be exactly `chunk_size` bytes. 67 + 68 + --- 69 + 70 + #### `fs_inode` - File and Directory Metadata 71 + 72 + Stores file/directory metadata using a Unix-like inode design. 73 + 74 + ```sql 75 + CREATE TABLE fs_inode ( 76 + ino INTEGER PRIMARY KEY AUTOINCREMENT, 77 + mode INTEGER NOT NULL, 78 + nlink INTEGER NOT NULL DEFAULT 0, 79 + uid INTEGER NOT NULL DEFAULT 0, 80 + gid INTEGER NOT NULL DEFAULT 0, 81 + size INTEGER NOT NULL DEFAULT 0, 82 + atime INTEGER NOT NULL, 83 + mtime INTEGER NOT NULL, 84 + ctime INTEGER NOT NULL, 85 + rdev INTEGER NOT NULL DEFAULT 0, 86 + atime_nsec INTEGER NOT NULL DEFAULT 0, 87 + mtime_nsec INTEGER NOT NULL DEFAULT 0, 88 + ctime_nsec INTEGER NOT NULL DEFAULT 0 89 + ) 90 + ``` 91 + 92 + **Fields**: 93 + - `ino` - Unique inode number (root is always ino=1) 94 + - `mode` - Unix mode bits combining file type and permissions (see Mode Encoding below) 95 + - `nlink` - Hard link count (0 for unlinked files being deleted, 2+ for directories) 96 + - `uid` / `gid` - Owner user/group IDs 97 + - `size` - File size in bytes (0 for directories) 98 + - `atime` / `mtime` / `ctime` - Access/modification/change time (Unix epoch, seconds) 99 + - `atime_nsec` / `mtime_nsec` / `ctime_nsec` - Nanosecond components of timestamps 100 + - `rdev` - Device number for character/block devices (major/minor encoded) 101 + 102 + **Mode Encoding** (32-bit): 103 + 104 + ``` 105 + File type (upper 4 bits): 106 + 0o170000 - File type mask (S_IFMT) 107 + 0o100000 - Regular file (S_IFREG) 108 + 0o040000 - Directory (S_IFDIR) 109 + 0o120000 - Symbolic link (S_IFLNK) 110 + 0o010000 - FIFO/named pipe (S_IFIFO) 111 + 0o020000 - Character device (S_IFCHR) 112 + 0o060000 - Block device (S_IFBLK) 113 + 0o140000 - Socket (S_IFSOCK) 114 + 115 + Permissions (lower 12 bits): 116 + 0o000777 - Permission bits (rwxrwxrwx) 117 + 118 + Example modes: 119 + 0o100644 - Regular file, rw-r--r-- 120 + 0o040755 - Directory, rwxr-xr-x 121 + 0o120777 - Symlink, rwxrwxrwx 122 + ``` 123 + 124 + **Special Inodes**: 125 + - Inode 1 is always the root directory 126 + 127 + **Consistency Rules**: 128 + - Root inode (ino=1) MUST always exist 129 + - Directories MUST have mode with S_IFDIR bit set 130 + - Regular files MUST have mode with S_IFREG bit set 131 + - File size MUST match total size of all `fs_data` chunks 132 + 133 + --- 134 + 135 + #### `fs_dentry` - Directory Entries 136 + 137 + Maps filenames to inodes (the filesystem namespace). 138 + 139 + ```sql 140 + CREATE TABLE fs_dentry ( 141 + id INTEGER PRIMARY KEY AUTOINCREMENT, 142 + name TEXT NOT NULL, 143 + parent_ino INTEGER NOT NULL, 144 + ino INTEGER NOT NULL, 145 + UNIQUE(parent_ino, name) 146 + ) 147 + 148 + CREATE INDEX idx_fs_dentry_parent ON fs_dentry(parent_ino, name) 149 + ``` 150 + 151 + **Fields**: 152 + - `name` - Basename (filename or directory name) 153 + - `parent_ino` - Parent directory inode number 154 + - `ino` - Inode this entry points to 155 + 156 + **Notes**: 157 + - Multiple dentries MAY point to the same inode (hard links) 158 + - Root directory (ino=1) has no dentry (no parent) 159 + - Link count in `fs_inode.nlink` tracks hard links 160 + 161 + **Path Resolution Algorithm**: 162 + ```sql 163 + -- To resolve "/a/b/c": 164 + 1. Start at ino=1 (root) 165 + 2. SELECT ino FROM fs_dentry WHERE parent_ino=1 AND name='a' 166 + 3. SELECT ino FROM fs_dentry WHERE parent_ino=<result> AND name='b' 167 + 4. SELECT ino FROM fs_dentry WHERE parent_ino=<result> AND name='c' 168 + ``` 169 + 170 + --- 171 + 172 + #### `fs_data` - File Content Chunks 173 + 174 + Stores file content in fixed-size chunks. 175 + 176 + ```sql 177 + CREATE TABLE fs_data ( 178 + ino INTEGER NOT NULL, 179 + chunk_index INTEGER NOT NULL, 180 + data BLOB NOT NULL, 181 + PRIMARY KEY (ino, chunk_index) 182 + ) 183 + ``` 184 + 185 + **Fields**: 186 + - `ino` - Inode number 187 + - `chunk_index` - Zero-based chunk index (chunk 0 = bytes 0 to chunk_size-1) 188 + - `data` - Binary content (BLOB), exactly `chunk_size` bytes except last chunk 189 + 190 + **Chunk Access**: 191 + ```sql 192 + -- Read entire file 193 + SELECT data FROM fs_data WHERE ino=? ORDER BY chunk_index ASC; 194 + 195 + -- Read bytes 1000-5000 (with chunk_size=4096) 196 + SELECT chunk_index, data FROM fs_data 197 + WHERE ino=? AND chunk_index >= 0 AND chunk_index <= 1 198 + ORDER BY chunk_index ASC; 199 + -- Returns chunk 0 (bytes 0-4095) and chunk 1 (bytes 4096-8191) 200 + -- Extract bytes 1000-4095 from chunk 0 201 + -- Extract bytes 0-904 from chunk 1 202 + ``` 203 + 204 + **Byte Offset Calculation**: 205 + - `chunk_index = byte_offset / chunk_size` 206 + - `offset_in_chunk = byte_offset % chunk_size` 207 + - `byte_offset = chunk_index * chunk_size + offset_in_chunk` 208 + 209 + **Consistency Rules**: 210 + - Directories MUST NOT have data chunks 211 + - All chunks except the last chunk MUST be exactly `chunk_size` bytes 212 + - Last chunk MAY be smaller than `chunk_size` 213 + 214 + --- 215 + 216 + #### `fs_symlink` - Symbolic Link Targets 217 + 218 + ```sql 219 + CREATE TABLE fs_symlink ( 220 + ino PRIMARY KEY, 221 + target TEXT NOT NULL 222 + ) 223 + ``` 224 + 225 + **Fields**: 226 + - `ino` - Inode number of the symlink (must have S_IFLNK mode) 227 + - `target` - Target path (may be absolute or relative) 228 + 229 + **Note**: Symlink resolution (following symlinks) is implementation-defined. The SDKs implement depth-limited following (max 40 levels). 230 + 231 + --- 232 + 233 + ### 2. Overlay Filesystem Tables (Optional) 234 + 235 + The overlay filesystem provides copy-on-write semantics, layering a writable delta on top of a read-only base. 236 + 237 + #### `fs_whiteout` - Deletion Markers 238 + 239 + Tracks deleted paths to prevent base layer visibility. 240 + 241 + ```sql 242 + CREATE TABLE fs_whiteout ( 243 + path TEXT PRIMARY KEY, 244 + parent_path TEXT NOT NULL, 245 + created_at INTEGER NOT NULL 246 + ) 247 + 248 + CREATE INDEX idx_fs_whiteout_parent ON fs_whiteout(parent_path) 249 + ``` 250 + 251 + **Fields**: 252 + - `path` - Normalized absolute path that has been deleted 253 + - `parent_path` - Parent directory path (for efficient child lookups) 254 + - `created_at` - Deletion timestamp (Unix epoch, seconds) 255 + 256 + **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. 257 + 258 + **Whiteout Creation**: 259 + ```sql 260 + INSERT INTO fs_whiteout (path, parent_path, created_at) 261 + VALUES (?, ?, ?) 262 + ON CONFLICT(path) DO UPDATE SET created_at = excluded.created_at 263 + ``` 264 + 265 + --- 266 + 267 + #### `fs_origin` - Copy-Up Origin Tracking 268 + 269 + Maintains inode consistency across layers for FUSE cache compatibility. 270 + 271 + ```sql 272 + CREATE TABLE fs_origin ( 273 + delta_ino INTEGER PRIMARY KEY, 274 + base_ino INTEGER NOT NULL 275 + ) 276 + ``` 277 + 278 + **Fields**: 279 + - `delta_ino` - Inode number in delta layer 280 + - `base_ino` - Original inode number from base layer 281 + 282 + **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. 283 + 284 + --- 285 + 286 + #### `fs_overlay_config` - Overlay Configuration 287 + 288 + ```sql 289 + CREATE TABLE fs_overlay_config ( 290 + key TEXT PRIMARY KEY, 291 + value TEXT NOT NULL 292 + ) 293 + ``` 294 + 295 + **Required Keys**: 296 + | Key | Value | Description | 297 + |-----|--------|-------------| 298 + | `base_path` | '/absolute/path' | Canonical path to base directory | 299 + 300 + --- 301 + 302 + ### 3. Key-Value Store Table 303 + 304 + #### `kv_store` - Agent State Storage 305 + 306 + ```sql 307 + CREATE TABLE kv_store ( 308 + key TEXT PRIMARY KEY, 309 + value TEXT NOT NULL, 310 + created_at INTEGER DEFAULT (unixepoch()), 311 + updated_at INTEGER DEFAULT (unixepoch()) 312 + ) 313 + 314 + CREATE INDEX idx_kv_store_created_at ON kv_store(created_at) 315 + ``` 316 + 317 + **Fields**: 318 + - `key` - Unique key identifier (any naming convention supported, e.g., `user:123`, `session:state`) 319 + - `value` - JSON-serialized value 320 + - `created_at` / `updated_at` - Timestamps (Unix epoch, seconds) 321 + 322 + **Upsert Pattern**: 323 + ```sql 324 + INSERT INTO kv_store (key, value, updated_at) 325 + VALUES (?, ?, unixepoch()) 326 + ON CONFLICT(key) DO UPDATE SET 327 + value = excluded.value, 328 + updated_at = unixepoch() 329 + ``` 330 + 331 + **Use Cases**: 332 + - Agent preferences and configuration 333 + - Session state and context 334 + - Conversation history (optional extension) 335 + - Structured data that doesn't fit filesystem model 336 + 337 + --- 338 + 339 + ### 4. Tool Call Audit Trail Table 340 + 341 + #### `tool_calls` - Tool Invocation Log 342 + 343 + Insert-only audit log for debugging and compliance. 344 + 345 + ```sql 346 + CREATE TABLE tool_calls ( 347 + id INTEGER PRIMARY KEY AUTOINCREMENT, 348 + name TEXT NOT NULL, 349 + parameters TEXT, 350 + result TEXT, 351 + error TEXT, 352 + status TEXT NOT NULL DEFAULT 'pending', 353 + started_at INTEGER NOT NULL, 354 + completed_at INTEGER, 355 + duration_ms INTEGER 356 + ) 357 + 358 + CREATE INDEX idx_tool_calls_name ON tool_calls(name) 359 + CREATE INDEX idx_tool_calls_started_at ON tool_calls(started_at) 360 + ``` 361 + 362 + **Fields**: 363 + - `id` - Unique tool call identifier 364 + - `name` - Tool name (e.g., 'read_file', 'web_search', 'execute_code') 365 + - `parameters` - JSON-serialized input parameters (NULL if no parameters) 366 + - `result` - JSON-serialized result (NULL if error) 367 + - `error` - Error message (NULL if success) 368 + - `status` - 'pending', 'success', or 'error' 369 + - `started_at` - Invocation timestamp (Unix epoch, seconds) 370 + - `completed_at` - Completion timestamp (NULL if pending) 371 + - `duration_ms` - Execution duration in milliseconds (NULL if pending) 372 + 373 + **Consistency Rules**: 374 + 1. Exactly one of `result` or `error` should be non-NULL (mutual exclusion) 375 + 2. `completed_at` MUST always be set (no NULL values on completion) 376 + 3. `duration_ms` MUST equal `(completed_at - started_at) * 1000` 377 + 4. Parameters and results MUST be valid JSON strings when present 378 + 5. Records MUST NOT be updated or deleted (insert-only) 379 + 380 + **Performance Analysis Query**: 381 + ```sql 382 + SELECT 383 + name, 384 + COUNT(*) as total_calls, 385 + SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as successful, 386 + SUM(CASE WHEN status='error' THEN 1 ELSE 0 END) as failed, 387 + AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms ELSE 0 END) as avg_duration_ms 388 + FROM tool_calls 389 + GROUP BY name 390 + ORDER BY total_calls DESC 391 + ``` 392 + 393 + --- 394 + 395 + ## SDK API Reference 396 + 397 + ### Rust SDK (`agentfs_sdk` crate) 398 + 399 + **Installation**: 400 + ```toml 401 + [dependencies] 402 + agentfs-sdk = "latest" 403 + ``` 404 + 405 + **Opening an AgentFS Database**: 406 + 407 + ```rust 408 + use agentfs_sdk::{AgentFS, AgentFSOptions}; 409 + 410 + // Persistent storage with identifier 411 + let agent = AgentFS::open(AgentFSOptions::with_id("my-agent")).await?; 412 + // Creates: .agentfs/my-agent.db 413 + 414 + // Ephemeral in-memory database 415 + let agent = AgentFS::open(AgentFSOptions::ephemeral()).await?; 416 + 417 + // Custom database path 418 + let agent = AgentFS::open(AgentFSOptions::with_path("./data/mydb.db")).await?; 419 + 420 + // With overlay filesystem (copy-on-write) 421 + let agent = AgentFS::open( 422 + AgentFSOptions::with_id("my-overlay") 423 + .with_base("/path/to/project") 424 + ).await?; 425 + ``` 426 + 427 + **Filesystem Operations**: 428 + 429 + ```rust 430 + use agentfs_sdk::{DEFAULT_DIR_MODE, DEFAULT_FILE_MODE}; 431 + 432 + // Create directory 433 + agent.fs.mkdir("/documents", 0, 0).await?; 434 + 435 + // Write file (creates parent directories automatically) 436 + agent.fs.write_file("/documents/readme.txt", b"Hello, world!").await?; 437 + 438 + // Read file 439 + let content = agent.fs.read_file("/documents/readme.txt").await?; 440 + 441 + // Get file stats 442 + let stats = agent.fs.stat("/documents/readme.txt").await?; 443 + if let Some(s) = stats { 444 + println!("Size: {} bytes", s.size); 445 + println!("Is file: {}", s.is_file()); 446 + } 447 + 448 + // List directory 449 + let entries = agent.fs.readdir("/documents").await?; 450 + if let Some(files) = entries { 451 + for file in files { 452 + println!(" {}", file); 453 + } 454 + } 455 + 456 + // Create file with specific mode 457 + let (stats, file) = agent.fs.create_file( 458 + "/data/test.txt", 459 + DEFAULT_FILE_MODE, 460 + 0, 461 + 0, 462 + ).await?; 463 + file.pwrite(0, b"Hello").await?; 464 + 465 + // Delete file 466 + agent.fs.unlink("/documents/readme.txt").await?; 467 + 468 + // Remove directory (recursive) 469 + agent.fs.rm("/documents", true, false).await?; 470 + 471 + // Rename/move 472 + agent.fs.rename("/old.txt", "/new.txt").await?; 473 + 474 + // Copy file 475 + agent.fs.copy_file("/source.txt", "/dest.txt").await?; 476 + 477 + // Check existence 478 + let _ = agent.fs.access("/path/to/file").await?; 479 + ``` 480 + 481 + **Key-Value Operations**: 482 + 483 + ```rust 484 + use serde_json::json; 485 + 486 + // Set a value 487 + agent.kv.set("user:preferences", &json!({"theme": "dark"})).await?; 488 + 489 + // Get a value 490 + let prefs: Option<serde_json::Value> = agent.kv.get("user:preferences").await?; 491 + 492 + // Delete a value 493 + agent.kv.delete("user:preferences").await?; 494 + 495 + // List all keys 496 + let keys = agent.kv.keys().await?; 497 + for key in keys { 498 + println!("{}", key); 499 + } 500 + ``` 501 + 502 + **Tool Call Tracking**: 503 + 504 + ```rust 505 + use serde_json::json; 506 + 507 + // Start a tool call 508 + let call_id = agent.tools.start("search", Some(json!({"query": "Rust"}))).await?; 509 + 510 + // Mark as successful 511 + agent.tools.success(call_id, Some(json!({"results": ["doc1", "doc2"]})).await?; 512 + 513 + // Or mark as failed 514 + agent.tools.error(call_id, "Connection timeout").await?; 515 + 516 + // Record a completed call (spec-compliant) 517 + agent.tools.record( 518 + "search", 519 + 1234567890, 520 + 1234567892, 521 + Some(json!({"query": "Rust"})), 522 + Some(json!({"results": ["doc1", "doc2"]})), 523 + None, 524 + ).await?; 525 + 526 + // Get tool call 527 + let call = agent.tools.get(call_id).await?; 528 + if let Some(c) = call { 529 + println!("Tool: {}, Status: {}", c.name, c.status); 530 + } 531 + 532 + // Get recent tool calls 533 + let recent = agent.tools.recent(Some(10)).await?; 534 + 535 + // Get statistics for a tool 536 + let stats = agent.tools.stats_for("search").await?; 537 + if let Some(s) = stats { 538 + println!("Total: {}, Success: {}, Avg: {:.2}ms", 539 + s.total_calls, s.successful, s.avg_duration_ms); 540 + } 541 + ``` 542 + 543 + **Low-Level Database Access**: 544 + 545 + ```rust 546 + // Get a connection for custom queries 547 + let conn = agent.get_connection().await?; 548 + 549 + // Execute custom SQL 550 + let mut rows = conn.query("SELECT * FROM fs_inode WHERE ino > ?", (10,)).await?; 551 + while let Some(row) = rows.next().await? { 552 + // Process row... 553 + } 554 + ``` 555 + 556 + --- 557 + 558 + ### Python SDK (`agentfs-sdk` pip package) 559 + 560 + **Installation**: 561 + ```bash 562 + pip install agentfs-sdk 563 + ``` 564 + 565 + **Opening an AgentFS Database**: 566 + 567 + ```python 568 + import asyncio 569 + from agentfs_sdk import AgentFS, AgentFSOptions 570 + 571 + async def main(): 572 + # Persistent storage with identifier 573 + agent = await AgentFS.open(AgentFSOptions(id='my-agent')) 574 + # Creates: .agentfs/my-agent.db 575 + 576 + # Ephemeral in-memory database 577 + agent = await AgentFS.open() 578 + 579 + # Custom database path 580 + agent = await AgentFS.open(AgentFSOptions(path='./data/mydb.db')) 581 + 582 + # Context manager support 583 + async with await AgentFS.open(AgentFSOptions(id='my-agent')) as agent: 584 + await agent.kv.set('key', 'value') 585 + # Automatically closed on exit 586 + ``` 587 + 588 + **Filesystem Operations**: 589 + 590 + ```python 591 + # Write a file (creates parent directories automatically) 592 + await agent.fs.write_file('/data/config.json', '{"key": "value"}') 593 + 594 + # Read a file 595 + content = await agent.fs.read_file('/data/config.json') 596 + 597 + # Read as bytes 598 + data = await agent.fs.read_file('/data/image.png', encoding=None) 599 + 600 + # List directory 601 + entries = await agent.fs.readdir('/data') 602 + 603 + # Get file stats 604 + stats = await agent.fs.stat('/data/config.json') 605 + print(f"Size: {stats.size} bytes") 606 + print(f"Modified: {stats.mtime}") 607 + print(f"Is file: {stats.is_file()}") 608 + 609 + # Create directory 610 + await agent.fs.mkdir('/new-folder') 611 + 612 + # Delete a file 613 + await agent.fs.unlink('/data/config.json') 614 + 615 + # Remove directory (recursive) 616 + await agent.fs.rm('/folder', recursive=True, force=False) 617 + 618 + # Rename/move 619 + await agent.fs.rename('/old.txt', '/new.txt') 620 + 621 + # Copy file 622 + await agent.fs.copy_file('/source.txt', '/dest.txt') 623 + 624 + # Check existence 625 + try: 626 + await agent.fs.access('/documents/readme.txt') 627 + print("File exists") 628 + except Exception: 629 + print("File not found") 630 + ``` 631 + 632 + **Key-Value Operations**: 633 + 634 + ```python 635 + # Set a value 636 + await agent.kv.set('user:123', {'name': 'Alice', 'age': 30}) 637 + 638 + # Get a value 639 + user = await agent.kv.get('user:123') 640 + 641 + # Delete a value 642 + await agent.kv.delete('user:123') 643 + 644 + # List by prefix (custom implementation) 645 + # Note: Python SDK doesn't have built-in prefix listing 646 + # Use direct SQL access for prefix queries: 647 + conn = agent.fs._db 648 + cursor = await conn.execute("SELECT key FROM kv_store WHERE key LIKE ?", ('user:%',)) 649 + ``` 650 + 651 + **Tool Call Tracking**: 652 + 653 + ```python 654 + # Start a tool call 655 + call_id = await agent.tools.start('search', {'query': 'Python'}) 656 + 657 + # Mark as successful 658 + await agent.tools.success(call_id, {'results': ['result1', 'result2']}) 659 + 660 + # Or mark as failed 661 + await agent.tools.error(call_id, 'Connection timeout') 662 + 663 + # Record a completed call 664 + await agent.tools.record( 665 + 'search', 666 + started_at=1234567890, 667 + completed_at=1234567892, 668 + parameters={'query': 'Python'}, 669 + result={'results': ['result1', 'result2']} 670 + ) 671 + 672 + # Query tool calls 673 + calls = await agent.tools.get_by_name('search', limit=10) 674 + recent = await agent.tools.get_recent(since=1234567890) 675 + 676 + # Get statistics 677 + stats = await agent.tools.get_stats() 678 + for stat in stats: 679 + print(f"{stat.name}: {stat.successful}/{stat.total_calls} successful") 680 + ``` 681 + 682 + --- 683 + 684 + ### TypeScript SDK (`agentfs-sdk` npm package) 685 + 686 + **Installation**: 687 + ```bash 688 + npm install agentfs-sdk 689 + ``` 690 + 691 + **Opening an AgentFS Database**: 692 + 693 + ```typescript 694 + import { AgentFS } from 'agentfs-sdk'; 695 + 696 + // Persistent storage with identifier 697 + const agent = await AgentFS.open({ id: 'my-agent' }); 698 + // Creates: .agentfs/my-agent.db 699 + 700 + // Or use ephemeral in-memory database 701 + const ephemeralAgent = await AgentFS.open(); 702 + ``` 703 + 704 + **Filesystem Operations**: 705 + 706 + ```typescript 707 + // Write files (parent dirs created automatically) 708 + await agent.fs.writeFile('/documents/readme.txt', 'Hello, world!'); 709 + await agent.fs.writeFile('/data/config.json', JSON.stringify({ key: 'value' })); 710 + 711 + // Write binary data 712 + const imageBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47]); 713 + await agent.fs.writeFile('/images/icon.png', imageBuffer); 714 + 715 + // Read files 716 + const content = await agent.fs.readFile('/documents/readme.txt', 'utf-8'); 717 + console.log(content); // "Hello, world!" 718 + 719 + const binary = await agent.fs.readFile('/images/icon.png'); 720 + console.log(binary); // <Buffer 89 50 4e 47> 721 + 722 + // Get file statistics 723 + const stats = await agent.fs.stat('/documents/readme.txt'); 724 + console.log({ 725 + inode: stats.ino, 726 + size: stats.size, 727 + mode: stats.mode.toString(8), // "100644" for regular file 728 + isFile: stats.isFile(), // true 729 + isDirectory: stats.isDirectory(), // false 730 + modified: new Date(stats.mtime * 1000) 731 + }); 732 + 733 + // List directory contents 734 + const files = await agent.fs.readdir('/documents'); 735 + console.log(files); // ['readme.txt'] 736 + 737 + // Create directories 738 + await agent.fs.mkdir('/new-folder'); 739 + 740 + // Remove files and directories 741 + await agent.fs.unlink('/documents/readme.txt'); 742 + await agent.fs.rmdir('/empty-folder'); 743 + await agent.fs.rm('/folder', { recursive: true, force: true }); 744 + 745 + // Rename/move files 746 + await agent.fs.rename('/old-name.txt', '/new-name.txt'); 747 + 748 + // Copy files 749 + await agent.fs.copyFile('/source.txt', '/destination.txt'); 750 + 751 + // Check file access (existence) 752 + try { 753 + await agent.fs.access('/documents/readme.txt'); 754 + console.log('File exists'); 755 + } catch (e) { 756 + console.log('File does not exist'); 757 + } 758 + ``` 759 + 760 + **Key-Value Operations**: 761 + 762 + ```typescript 763 + // Key-value operations 764 + await agent.kv.set('user:preferences', { theme: 'dark' }); 765 + const prefs = await agent.kv.get('user:preferences'); 766 + 767 + // Tool call tracking 768 + await agent.tools.record( 769 + 'web_search', 770 + Date.now() / 1000, 771 + Date.now() / 1000 + 1.5, 772 + { query: 'AI' }, 773 + { results: [...] } 774 + ); 775 + ``` 776 + 777 + --- 778 + 779 + ## Direct SQL Access 780 + 781 + Since AgentFS is just a SQLite database, you can query it directly with any SQLite client: 782 + 783 + ```bash 784 + # Using sqlite3 CLI 785 + sqlite3 .agentfs/my-agent.db 786 + 787 + # List all files with paths (recursive CTE) 788 + WITH RECURSIVE file_tree(ino, path) AS ( 789 + SELECT 1, '/' 790 + UNION ALL 791 + SELECT d.ino, path || d.name || '/' 792 + FROM fs_dentry d 793 + JOIN file_tree ft ON d.parent_ino = ft.ino 794 + ) 795 + SELECT path, i.size, i.mode 796 + FROM file_tree ft 797 + JOIN fs_inode i ON ft.ino = i.ino 798 + WHERE (i.mode & 0o170000) = 0o100000; -- Regular files only 799 + ``` 800 + 801 + --- 802 + 803 + ## Overlay Filesystem (Copy-on-Write) 804 + 805 + The overlay filesystem combines a read-only base layer (host filesystem) with a writable delta layer (AgentFS). 806 + 807 + **Lookup Semantics**: 808 + 1. Check if path exists in delta layer → return delta entry 809 + 2. Check if path has a whiteout → return "not found" 810 + 3. Check if path exists in base layer → return base entry 811 + 4. Return "not found" 812 + 813 + **Whiteouts**: 814 + - Created when deleting a file that exists only in base layer 815 + - Mark paths as deleted so base layer doesn't show through 816 + - Example: Delete `/README.md` (exists in base), insert whiteout for `/README.md` 817 + 818 + **Copy-Up**: 819 + - When first writing to a base file, it's copied to delta layer 820 + - Origin tracking ensures inode numbers stay consistent (FUSE cache compatibility) 821 + - Example: Read `/src/main.rs` (from base), write to it → copy to delta 822 + 823 + **Usage**: 824 + ```rust 825 + // Create overlay on top of project directory 826 + let agent = AgentFS::open( 827 + AgentFSOptions::with_id("my-overlay") 828 + .with_base("/path/to/project") 829 + ).await?; 830 + 831 + // All modifications go to delta (my-overlay.db) 832 + // Reads fall through to base filesystem (/path/to/project) 833 + 834 + // Get modified paths 835 + let delta_paths = agent.get_delta_paths().await?; 836 + 837 + // Get deleted paths (whiteouts) 838 + let whiteouts = agent.get_whiteouts().await?; 839 + ``` 840 + 841 + ### Delta-to-Base Commit: **NOT SUPPORTED** 842 + 843 + **Important Limitation**: AgentFS does **NOT** provide any built-in functionality to write/commit delta changes back to the base filesystem. 844 + 845 + **Design Rationale**: 846 + - The base layer is intentionally **read-only** - this enables: 847 + - Safe sandboxing of untrusted code 848 + - Reproducible execution (base never changes) 849 + - Easy rollback (just discard delta database) 850 + - Multiple independent sandboxes sharing the same base 851 + 852 + **What Does Work**: 853 + - `agentfs diff <id_or_path>` - CLI command to **view** changes (shows Added/Modified/Deleted) 854 + - `get_delta_paths()` - SDK method to list all modified/new files in delta 855 + - `get_whiteouts()` - SDK method to list all deleted paths 856 + 857 + **What Does NOT Work**: 858 + - No API to apply/merge delta changes into base 859 + - No `commit` or `sync-to-base` command 860 + - No built-in functionality to write delta back to base 861 + 862 + **Workarounds** (if you need to apply delta changes to base): 863 + 864 + 1. **Manual Copy** - Iterate through delta paths and copy to base: 865 + ```bash 866 + # Example shell script to apply changes 867 + agentfs diff my-overlay | while read change_type type_char path; do 868 + case $change_type in 869 + A|M) cp ".agentfs/my-overlay.db:$path" "/base/path/$path" ;; 870 + D) rm "/base/path/$path" ;; 871 + esac 872 + done 873 + ``` 874 + 875 + 2. **Use `agentfs exec`** with overlay disabled: 876 + ```bash 877 + # Temporarily disable overlay, copy changes directly 878 + agentfs exec my-agent cp -r /delta/* /base/ 879 + ``` 880 + 881 + 3. **Database Query** - Extract delta changes and apply via direct filesystem operations: 882 + ```sql 883 + -- Get all added/modified files 884 + SELECT d.name, d.parent_ino, i.mode, i.size 885 + FROM fs_dentry d 886 + JOIN fs_inode i ON d.ino = i.ino 887 + WHERE i.ino > 1 AND (i.mode & 0o170000) = 0o100000; 888 + 889 + -- Then iterate results and copy to base using your preferred method 890 + ``` 891 + 892 + **Why No Built-in Commit**: 893 + The overlay filesystem follows Linux's `overlayfs` design, which treats the lower (base) layer as immutable. This is intentional for security and reproducibility: 894 + - AI agents can safely experiment without modifying source code 895 + - Multiple agents can run concurrently against the same project 896 + - Easy to "factory reset" by deleting the delta database 897 + - Base layer serves as a canonical, immutable reference 898 + 899 + --- 900 + 901 + ## Performance Characteristics 902 + 903 + ### Chunk-Based Storage 904 + 905 + **Advantages**: 906 + - Efficient random access (read only needed chunks) 907 + - Sparse file support (missing chunks return zeros) 908 + - Memory-friendly for large files 909 + 910 + **Trade-offs**: 911 + - Small file overhead (minimum one chunk) 912 + - Chunk boundary alignment for partial writes 913 + 914 + **Chunk Size Selection**: 915 + - Default: 4096 bytes (typical filesystem block size) 916 + - Larger chunks: Better sequential read performance, more wasted space for small files 917 + - Smaller chunks: Better random access, more overhead for large files 918 + 919 + ### Caching 920 + 921 + The Rust SDK includes an LRU cache for directory entry lookups: 922 + - Cache size: 10,000 entries 923 + - Maps `(parent_ino, name) → child_ino` 924 + - Reduces repeated path resolution queries 925 + - Especially beneficial for repeated directory listings 926 + 927 + ### Connection Pooling 928 + 929 + All SDKs use connection pooling: 930 + - Rust: `ConnectionPool` with configurable size 931 + - Python: Turso connection reuse 932 + - TypeScript: Database connection reuse 933 + 934 + --- 935 + 936 + ## Common Patterns 937 + 938 + ### 1. Agent State Snapshot 939 + 940 + ```rust 941 + // Snapshot agent state 942 + use std::fs; 943 + fs::copy(".agentfs/my-agent.db", "snapshots/agent-backup.db")?; 944 + 945 + // Later restore 946 + fs::copy("snapshots/agent-backup.db", ".agentfs/my-agent.db")?; 947 + ``` 948 + 949 + ### 2. Tool Call Analysis 950 + 951 + ```sql 952 + -- Find slowest tool calls 953 + SELECT name, AVG(duration_ms) as avg_ms 954 + FROM tool_calls 955 + WHERE status = 'success' 956 + GROUP BY name 957 + ORDER BY avg_ms DESC; 958 + 959 + -- Find failed tool calls in last hour 960 + SELECT name, error, COUNT(*) as failures 961 + FROM tool_calls 962 + WHERE status = 'error' AND started_at > ? 963 + GROUP BY name, error 964 + ORDER BY failures DESC; 965 + ``` 966 + 967 + ### 3. File System Diff 968 + 969 + ```sql 970 + -- Compare two agent states (with attached databases) 971 + ATTACH DATABASE 'snapshots/before.db' AS before; 972 + 973 + SELECT 'added' as change, after_d.name || '/' || after_i.name as path 974 + FROM after.fs_dentry after_d 975 + JOIN after.fs_inode after_i ON after_d.ino = after_i.ino 976 + LEFT JOIN before.fs_dentry before_d ON before_d.name = after_d.name 977 + WHERE before_d.name IS NULL 978 + 979 + UNION ALL 980 + 981 + SELECT 'removed' as change, before_d.name || '/' || before_i.name as path 982 + FROM before.fs_dentry before_d 983 + JOIN before.fs_inode before_i ON before_d.ino = before_i.ino 984 + LEFT JOIN after.fs_dentry after_d ON after_d.name = before_d.name 985 + WHERE after_d.name IS NULL; 986 + ``` 987 + 988 + --- 989 + 990 + ## Extension Points 991 + 992 + The AgentFS specification includes extension points for additional functionality: 993 + 994 + ### Key-Value Store Extensions 995 + - Namespaced keys with hierarchy support 996 + - Value versioning/history 997 + - TTL (time-to-live) for automatic expiration 998 + - Value size limits and quotas 999 + 1000 + ### Filesystem Extensions 1001 + - Extended attributes table 1002 + - File ACLs and advanced permissions 1003 + - Quota tracking per user/group 1004 + - Version history and snapshots 1005 + - Content deduplication 1006 + - Compression metadata 1007 + - File checksums/hashes 1008 + 1009 + ### Tool Call Extensions 1010 + - Session/conversation grouping (`session_id` field) 1011 + - User attribution (`user_id` field) 1012 + - Cost tracking (`cost` field for API calls) 1013 + - Parent/child relationships for nested tool calls 1014 + - Token usage tracking 1015 + - Input/output size metrics 1016 + 1017 + **Implementation Guidance**: Extensions SHOULD use separate tables to maintain referential integrity with the core schema. 1018 + 1019 + --- 1020 + 1021 + ## Summary 1022 + 1023 + **AgentFS provides**: 1024 + - A complete POSIX-like filesystem implementation in SQLite 1025 + - Key-value store for agent state 1026 + - Tool call audit trail for debugging and compliance 1027 + - Overlay filesystem for copy-on-write sandboxing 1028 + - Full-featured SDKs for Rust, Python, and TypeScript 1029 + - Direct SQL access for custom queries 1030 + 1031 + **Key Advantages**: 1032 + - Queryable history via SQL 1033 + - Simple snapshot/restore via file copy 1034 + - Portable single-file database 1035 + - Works without FUSE (use SDKs directly) 1036 + - Designed for AI agents (auditability, reproducibility) 1037 + 1038 + **When to Use FUSE**: 1039 + - Running existing POSIX tools 1040 + - Interactive shell sessions 1041 + - Debugging filesystem behavior 1042 + 1043 + **When to Use SDK**: 1044 + - Programmatic access (most cases) 1045 + - Better performance and control 1046 + - Multi-language support (Rust, Python, TypeScript) 1047 + - No filesystem mount requirements 1048 + 1049 + --- 1050 + 1051 + ## References 1052 + 1053 + - [AgentFS Repository](https://github.com/tursodatabase/agentfs) 1054 + - [AgentFS Specification](.test-agent/agentfs/SPEC.md) 1055 + - [AgentFS Manual](.test-agent/agentfs/MANUAL.md) 1056 + - [Turso Database](https://github.com/tursodatabase/turso) 1057 + - [Announcement Blog Post](https://turso.tech/blog/agentfs)