From 393cebfd0c16d59fc45ec0826db1bb048a5da3df Mon Sep 17 00:00:00 2001 From: hartsock Date: Sun, 14 Jun 2026 09:33:05 -0400 Subject: [PATCH] =?UTF-8?q?Add=20SqliteBackend::from=5Fconnection=20?= =?UTF-8?q?=E2=80=94=20incremental-adoption=20seam=20(0.1.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHAT: SqliteBackend::from_connection(Connection) wraps a connection a consumer already owns; SqliteBackend::connection() lends it back for domain-table SQL. Bump 0.1.0 -> 0.1.1. WHY: newt-agent's ConversationStore and modulex-mcp's Store each own a live rusqlite::Connection. Without this seam, adopting agent-store would mean either a second connection to the same file or rewriting all domain SQL at once. This lets a consumer hand over its connection, keep its existing rusqlite code via connection(), and gain the agent-store primitives on the same database — so the first integration PRs stay small. Regression test: from_connection_wraps_and_shares_the_database asserts Backend trait writes and connection() escape-hatch writes hit the same database. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 2 +- Cargo.toml | 8 +++---- crates/agent-store/src/backend.rs | 37 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6fdafe..a74ad26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agent-store" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 082f932..3354ff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ resolver = "2" members = ["crates/agent-store"] -# 0.1.x semver line (Shawn, 2026-06-13): first publish = 0.1.0, cut manually -# once agent-store is consumed by newt-agent and modulex-mcp in the field. +# 0.1.x semver line (Shawn, 2026-06-13): 0.1.0 published; 0.1.1 adds the +# incremental-adoption seam (SqliteBackend::from_connection) for consumers. [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" @@ -13,7 +13,7 @@ authors = ["Shawn Hartsock "] repository = "https://github.com/Gilamonster-Foundation/agent-store" [workspace.dependencies] -agent-store = { path = "crates/agent-store", version = "=0.1.0" } +agent-store = { path = "crates/agent-store", version = "=0.1.1" } anyhow = "1.0" blake3 = "1.8" diff --git a/crates/agent-store/src/backend.rs b/crates/agent-store/src/backend.rs index e1a4cc1..2bd85e2 100644 --- a/crates/agent-store/src/backend.rs +++ b/crates/agent-store/src/backend.rs @@ -76,6 +76,25 @@ impl SqliteBackend { Ok(Self { conn }) } + /// Wrap a connection a consumer already owns — the **incremental-adoption + /// seam**. A consumer (newt's `ConversationStore`, modulex's `Store`) that + /// already holds a `rusqlite::Connection` hands it over, keeps running its + /// own domain SQL through [`SqliteBackend::connection`], and gets the + /// agent-store primitives on the *same* database: no second connection, no + /// big-bang rewrite. Pragmas are the caller's responsibility here (the + /// connection is assumed already configured). + pub fn from_connection(conn: rusqlite::Connection) -> Self { + Self { conn } + } + + /// Borrow the underlying SQLite connection for backend-specific + /// (domain-table) SQL. SQLite-only by nature — the [`Backend`] trait stays + /// the portable, backend-agnostic surface; this escape hatch is how a + /// consumer keeps its existing rusqlite code while adopting the substrate. + pub fn connection(&self) -> &rusqlite::Connection { + &self.conn + } + fn apply_pragmas(conn: &rusqlite::Connection) -> Result<()> { // WAL + a generous busy timeout: multiple co-located agents serialize // on the write lock instead of failing fast. (NFS-home degradation to @@ -208,4 +227,22 @@ mod tests { let db = SqliteBackend::in_memory().unwrap(); assert_eq!(db.dialect(), Dialect::Sqlite); } + + #[test] + fn from_connection_wraps_and_shares_the_database() { + // A consumer's own connection, handed to the substrate. + let conn = rusqlite::Connection::open_in_memory().unwrap(); + let db = SqliteBackend::from_connection(conn); + + // Substrate writes through the Backend trait... + db.exec("CREATE TABLE t (x INTEGER)", &[]).unwrap(); + // ...and the consumer keeps its own rusqlite domain SQL via the escape + // hatch — both hit the same database. + db.connection() + .execute("INSERT INTO t VALUES (7)", []) + .unwrap(); + + let rows = db.query("SELECT x FROM t", &[]).unwrap(); + assert_eq!(rows, vec![vec![Value::Int(7)]]); + } }