Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
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"
authors = ["Shawn Hartsock <hartsock@users.noreply.github.com>"]
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"
Expand Down
37 changes: 37 additions & 0 deletions crates/agent-store/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)]]);
}
}
Loading