grit
An embedded, bi-temporal property graph for agent memory. One SQLite file, in-process, written in Rust. No server, no daemon, no network — compiles for macOS, Windows, Linux, iOS, and Android.
grit is the piece of grit at the center of the pearl stack — Layer 1, doing deterministic graph storage and hybrid retrieval only. The LLM extraction pipeline that deposits memory around it is nacre (Layer 2); the agent app is Layer 3. The full design contract — invariants, scope limits, testing bar — is AGENTS.md, and it is binding for anyone (human or agent) working here.
use ;
let g = open?;
g.apply?;
g.apply?;
// Hybrid recall: BM25 + vectors + graph expansion, RRF-fused, budgeted.
let hits = g.search?;
// Time travel is a query, not archaeology.
let then = g.traverse?;
let past = g.node_history?; // every edge ever believed
Why on SQLite
The embedded-graph-database niche is a graveyard: the ideal engine is archived, others are dormant, server-based, or license-encumbered — and none of it runs inside an iOS app. Meanwhile the graph semantics agent memory needs are a recipe, not a product. So grit is a deliberately thin layer (~a few kLOC) on the one storage engine that is in-process, iOS-native, public domain, and older than most databases. SQLite carries the dangerous parts — durability, crash recovery, decades of hardening — plus FTS5 and sqlite-vec, statically linked.
Not a vector database
The default architecture for "agent memory" is chunks in a vector store. grit is deliberately not that, and the trade is worth stating plainly.
A dedicated vector database — zvec, LanceDB, Qdrant, Milvus — optimizes one operation: approximate nearest-neighbor search over millions of embeddings, via ANN indexes and quantization. At that job it beats grit by orders of magnitude and always will: grit's vector leg is an exhaustive, exact scan (sqlite-vec), so query cost is O(corpus) where an ANN index pays O(~log corpus). At ten million vectors that is the difference between milliseconds and tens of seconds. grit does not enter that race — by decision, not accident (see the performance envelope below).
What walking away from ANN buys, for the job grit actually has:
- Recall 1.0, deterministically. Exact scan means no approximation, no
insertion-order dependence, no tuning knobs. The same file returns the
same ranking — which is what makes retrieval testable: nacre's
golden-trace oracle and the
EXPLAIN QUERY PLANpins in CI both depend on it. (FalkorDB's HNSW index makes upstream Graphiti's own retrieval nondeterministic across runs; that bug class is structurally absent here.) - Zero index lifecycle. A fact is searchable in the transaction that wrote it. No build step, no memory-resident graph, no degradation under the upsert/invalidate churn agent memory actually does, nothing to rebuild after a sync — and no second store to keep consistent with the file.
- Similarity is one leg, not the model. Hits are RRF-fused from BM25 (word and CJK-trigram), vector cosine, and graph expansion — filtered bi-temporally, with provenance attached. "What did I believe in March, and which conversation told me?" is not expressible as k-NN over chunks.
- It runs where the agent runs. One statically-linked SQLite file, including on iOS/Android, where a database engine sidecar is a non-starter.
The cost, measured rather than hand-waved: the exhaustive scan moves ~2.2 GB/s per query on Apple Silicon, so the 50 ms search budget covers roughly 25k dim-1024 vectors in the queried group. Group-partitioned vec tables (schema v5) keep real workloads — many moderate namespaces — inside that line; a single group at the 1M-edge envelope top currently exceeds it, with the numbers and the mitigation plan (binary quantization + exact re-rank, still deterministic) recorded in docs/vector-leg-latency.md.
Past the envelope, use both: bulk retrieval over a million document chunks belongs in a dedicated vector store; what the agent learned from those documents belongs here. The layers compose — that is the pearl architecture, not a rivalry.
The model
Every mutation is a GraphOp, appended to an op-log; graph tables are
derived state in the same transaction. The vocabulary is closed and small —
it is also the future sync unit:
| Op | Semantics |
|---|---|
AddNode |
create an entity (id-collision: lowest HLC wins, deterministically) |
AddEdge |
create a fat edge — the fact sentence lives on it, with event-time valid_at/invalid_at |
AddEpisode |
record raw provenance and its mentions — every fact traces to sources |
UpdateNode |
revise entity metadata (name/summary/kind/attrs) — per-field last-writer-wins registers; history stays in the op-log |
InvalidateEdge |
close a fact's event-time interval; concurrent invalidations converge to the earliest |
MergeNodes |
execute a dedup decision (Layer 2 decides whether; cycle-safe canonical resolution) |
Purge |
the only destructive op — exact-id right-to-forget, tombstoned, audited |
Bi-temporal throughout. Edges carry event time (when a fact was true in
the world) and system time (when it was believed). Invalidations are
belief-versioned, so "back in March, did we think this job still held?"
is as_of/as_at on an ordinary traversal.
Sync-ready before sync exists. UUIDv7 keys, hybrid logical clocks, and op application that is idempotent and commutative for concurrent ops — property-tested across adversarial interleavings (merge cycles, purges racing adds, updates arriving before their nodes, out-of-order invalidations). Embeddings stay out of the op-log: they're recomputable local state tagged with a model identity, stored and served back through typed getters/setters.
Data outlives the library. Lossless JSONL export/import of the full graph + op-log is a permanent compatibility surface; schema migrations are forward-only and tested against frozen fixture databases from every released version.
Proven by an oracle
grit's API is exercised by more than unit tests:
nacre's golden-trace conformance
replays recorded LLM/embedder responses through the full Rust stack and
diffs the resulting grit graph against pinned Python Graphiti's frozen
output — byte-equal facts, timestamps, attributions. Several grit 0.2
capabilities (UpdateNode, AddEdge.invalid_at, embedding getters, group
scans) exist because that oracle demanded them.
Performance envelope
Scale target: ≤100k nodes / ≤1M edges — years of a person's learned knowledge, not a fleet's raw perception (extract first; that's the whole architecture). Measured at v0.1: traversal (3 hops, validity-filtered, 256-node budget) holds its ≤10 ms p95 target with ≥2× margin at 100k nodes / 300k edges; on a deliberately adversarial 1M-edge uniform-random fixture it measures ~17 ms warm p95, enforced by a CI latency tripwire. Criterion benches fail CI on >20 % regression.
Non-goals
No server mode, wire protocol, or networking. No general Cypher (a parked,
fail-loud translator stub exists in grit-compat). No graph algorithms
(PageRank, community detection) without a proven need. No multi-process
access — one process, one writer. No chasing scale beyond the envelope.
Each of these is an explicit decision in AGENTS.md, reversible only as a
new decision.
Workspace
| Crate | Purpose |
|---|---|
grit-core |
schema, op-log writer actor, traversal, hybrid search — the product |
grit-compat |
parked, fail-loud Graphiti/FalkorDB-dialect translator stub |
grit-cli |
dev tool: import/export, stats, ad-hoc search/traverse |
Commands
The suite includes a kill -9 crash harness (derived tables must equal a
full op-log replay after any crash point), op-log merge-law property tests,
frozen schema-version fixtures, and EXPLAIN QUERY PLAN pinning — an index
regression is a correctness bug at this latency budget. Everything runs
offline.
License
MIT OR Apache-2.0, at your option. See NOTICE for attributions (Graphiti schema concepts, simple-graph traversal templates, Cortex scoring ideas).