11 SQLite3 Persistent Storage¶
Run it:
What it shows: replacing the default in-memory store with a custom Storage::Base subclass backed by SQLite3, proving that tasks survive a full server restart.
Files¶
| File | Purpose |
|---|---|
examples/11_sqlite_storage/server.rb |
SqliteStorage < A2A::Storage::Base and the PersistentEchoAgent server |
examples/11_sqlite_storage/client.rb |
populate phase (send tasks, save IDs) and verify phase (fetch tasks after restart) |
examples/11_sqlite_storage/run |
Two-phase lifecycle: populate → stop → restart → verify → cleanup |
examples/11_sqlite_storage/Gemfile |
Extends the project gemspec + adds sqlite3 |
examples/11_sqlite_storage/Brewfile |
Declares the sqlite3 binary dependency for Homebrew |
Dependencies¶
This demo has its own Gemfile and Brewfile. The run script handles setup before spawning anything:
- Binary check — verifies
sqlite3is on$PATH; on macOS runsbrew bundle install --file=Brewfileif it is not. Other platforms must provide the binary. - Gem install — runs
bundle installwith the localGemfile, which usesgemspec path: "../../"to pull in all project dependencies plussqlite3.
Once setup completes, server.rb can simply require "sqlite3" with no inline install logic.
The two-phase demo¶
Phase 1 — populate
The server starts with an empty database. The client sends three tasks (alpha, beta, gamma) and writes their IDs to a temp JSON file, then the server stops.
Phase 2 — verify
The server restarts pointing at the same database file. At startup it prints the existing task count (existing tasks in DB: 3), confirming the data survived. The client reads the saved IDs, fetches each task from the freshly booted server, and asserts all three are present and completed.
SqliteStorage implementation¶
SqliteStorage subclasses A2A::Storage::Base and implements all five required methods:
class SqliteStorage < A2A::Storage::Base
def initialize(path)
@db = SQLite3::Database.new(path)
@mutex = Mutex.new
@db.execute("PRAGMA journal_mode=WAL")
@db.execute("PRAGMA busy_timeout=5000")
setup_schema
end
def save(task)
@mutex.synchronize do
now = Time.now.iso8601
@db.execute(
"INSERT INTO tasks (id, data, created_at, updated_at) VALUES (?, ?, ?, ?) " \
"ON CONFLICT(id) DO UPDATE SET data=excluded.data, updated_at=excluded.updated_at",
[task.id, task.to_h.to_json, now, now]
)
end
task
end
def find(id)
row = @mutex.synchronize { @db.get_first_row("SELECT data FROM tasks WHERE id=?", [id]) }
return nil unless row
A2A::Models::Task.from_hash(JSON.parse(row[0]))
end
def find!(id)
find(id) or raise A2A::TaskNotFoundError, "Task #{id} not found"
end
def list
rows = @mutex.synchronize { @db.execute("SELECT data FROM tasks ORDER BY rowid") }
rows.map { |row| A2A::Models::Task.from_hash(JSON.parse(row[0])) }
end
def delete(id)
@mutex.synchronize { @db.execute("DELETE FROM tasks WHERE id=?", [id]) }
end
end
Tasks are stored as JSON blobs. from_hash reconstructs the full Task object including nested TaskStatus, Artifact, and Part models.
WAL mode allows concurrent readers while a write is in progress — important when multiple fibers are saving and fetching tasks simultaneously in a Falcon server.
Injecting the storage¶
The storage instance is passed to A2A.server via the storage: keyword:
storage = SqliteStorage.new(db_path)
A2A.server(agent_card: card, executor: EchoExecutor.new, storage: storage).run
No other library configuration is needed. The server's built-in RPC handlers (tasks/get, tasks/list, tasks/cancel, etc.) all use the injected store.
Protocol coverage¶
| Spec section | What the demo shows |
|---|---|
Storage::Base injection |
A2A.server(storage:) accepts any Storage::Base subclass — no library changes needed |
SqliteStorage#save |
Tasks serialized to JSON and upserted via ON CONFLICT DO UPDATE |
SqliteStorage#find! |
Task fetched by ID across process boundaries; raises TaskNotFoundError if missing |
SqliteStorage#list |
All stored tasks returned in insertion order |
SqliteStorage#size |
Task count reported at server startup to confirm DB contents |
| Cross-restart persistence | Tasks created in server process 1 are visible to server process 2 via the shared DB file |
| WAL mode concurrency | PRAGMA journal_mode=WAL allows concurrent readers during writes |
Brewfile / Gemfile pattern |
Per-demo dependency files keep application code free of setup logic |
Related guide¶
See Custom Storage for a detailed walkthrough of the Storage::Base interface and adapter patterns for other databases.