A single-file memory store for AI agents where recall gets smarter with use.
Project description
SynapticDB
SynapticDB is a single-file memory store for AI agents. It combines keyword search, vector search, and a graph of associations learned from how memories are stored and retrieved. Every result carries a calibrated confidence value, so an agent can tell "there is no good answer" from "here are ten weak ones".
Status: v0.1. One half is measured and working. The other half is not.
Hybrid search and confidence scoring work. Against the benchmark's locked baseline — BM25, FAISS, and a cross-encoder reranker — SynapticDB answers 17 of 25 associative queries to the baseline's 10, at identical direct recall of 25 of 25.
The association graph is implemented and enabled: temporal proximity, co-retrieval, decay, explicit feedback, manual links, and pruning. The benchmark does not show the graph improving recall. Before the graph existed, the same corpus already scored 17 of 25. The graph now supplies a reasoning path for the 7 answers the baseline misses, but hybrid search was already returning those 7 on its own. The graph explains results it did not produce.
Use SynapticDB today for the search and confidence path. Treat the graph as experimental. What does not work, and why explains each mechanism that fell short and how I know.
Install
pip install "synapticdb[embeddings]"
| install | pulls | embeddings come from |
|---|---|---|
synapticdb |
pydantic, numpy | your embedding_fn, which is then required |
synapticdb[embeddings] |
the above plus sentence-transformers |
the built-in 384-dimensional all-MiniLM-L6-v2, downloaded on first use |
Take the plain install when you already have an embedding provider, or when you
do not want a PyTorch-sized dependency. Passing embedding_fn overrides the
default either way.
from synapticdb import Synaptic
with Synaptic(":memory:") as memories:
memories.remember("Client X requires SOC2 for vendor deployments")
result = memories.recall("deployment requirements for Client X")
print(result.memories[0].memory.content)
Pass embedding_fn to Synaptic to use a different local or hosted provider.
The function takes a string and returns a numeric sequence with a stable
dimension.
How it works
The whole store is one SQLite file. There is no server, no index directory, and
no separate vector database. A path opens or creates a database; ":memory:"
keeps it in process.
The schema is small: memories, an FTS5 virtual table mirrored from memories
by trigger, an edges table for the association graph, a queries table that
records each recall so feedback can refer back to it, and a meta key-value
table. Embeddings live in a BLOB on the memory row and are scored as one
cached NumPy matrix multiply. At v0 scale, that dense scan is cheaper than
maintaining an approximate-nearest-neighbor index.
One recall() runs five steps:
- Search twice. FTS5 BM25 ranks by keyword. Cosine similarity over the stored embeddings ranks by meaning.
- Fuse. Reciprocal rank fusion merges the two ranked lists, then min-max normalizes the result.
- Spread. The top fused hits seed a bounded spreading activation pass across the edge graph. Edge weights decay to the current read time, so one query never sees two neighbors aged to different instants.
- Blend. Activation claims a share of the final score set by graph maturity. A cold graph degrades to pure hybrid search, which is why an empty database behaves sensibly on its first query.
- Score. Each returned memory gets a
scoreand aconfidence, described below, and is labeledviaassearch,association, orboth.
Edges come from four origins: temporal (memories written close together),
co_retrieval (memories returned together), explicit (connect()), and
semantic (embedding similarity, shipped disabled — see below). Writing a
memory periodically triggers a maintenance pass that prunes weak edges.
Knowing when there is no answer
Each result carries two numbers, and they answer different questions. score
ranks results within one recall and is normalized per query, so it cannot be
compared across queries. confidence is the cosine similarity between the query
and the memory. It is an absolute scale, so one threshold behaves the same on
every query.
result = memories.recall("deployment requirements for Client X", min_confidence=0.6)
relevant = result.memories # may be shorter than top_k, or empty
min_confidence drops results below the floor, so a recall can return fewer
than top_k memories, or none. The empty list is the useful part: it is how an
agent reports that it does not know. You can also filter afterward on
item.confidence if you would rather see everything.
On the benchmark, a 0.6 floor kept 41 of the 42 correct answers and rejected all 12 questions the corpus cannot answer. The highest-scoring unanswerable question reached 0.540. Confidence AUC is 0.994, meaning a correct answer outscores an unanswerable question 99.4% of the time.
The threshold is only as good as your embeddings. Confidence inherits
whatever separation your embedding_fn provides. A low-dimensional or poorly
fitted embedding leaves unrelated queries looking similar to everything, and no
threshold will repair that. Measure on your own data before relying on a fixed
floor.
Associations score low on confidence by construction. Weak textual similarity
is exactly why keyword and vector search missed them. Treat the floor as a dial:
raise it for direct matches only, lower it to admit associations.
Reproducing the numbers
Every figure above comes from bench/records/calibration-chained/, which is
committed. Reproduce it with:
uv sync --extra bench
uv run python -m bench --profile chained --retriever synaptic
The run takes about 35 seconds after a first-time model download, and prints a per-seed table of direct hits, associative hits, path-backed wins, MRR, chain coverage, and confidence AUC.
The benchmark is a merge gate, not a demonstration. It ingests a deterministic synthetic corpus in frozen narrative order, replays warm-up events, and scores holdout queries the retriever has never seen. Gold answers and reasoning paths belong to the evaluator; no retriever receives them. Warm-up events may not target a holdout chain's answer, and both the generator and the loader refuse a dataset that violates that rule — the constraint is enforced in code that fails, not prose that persuades.
bench/README.md documents the profiles, the parameter sweeps, and every
measurement.
What an associative answer looks like
One of the 25 associative holdout queries, taken from that run. The baseline misses it; SynapticDB returns it at rank 6 with confidence 0.640.
Query: Why does the observatory dome trigger wind protection during calm weather?
Answer returned: Converting knots before publishing reduced false closure alerts while preserving every genuinely windy shutdown.
The answer shares almost no words with the question. It is reachable only through two intermediate memories, stored at different times and never mentioned in the query:
- Wind data reaches the dome controller through the Boreal weather adapter added for the new roof station.
- Boreal labels its readings as meters per second but forwards the station's original knot values unchanged.
This is the shape of query hybrid search is built to lose, and the case the association graph exists to serve. Be precise about the credit, though: the graph records the path above, but hybrid search was already returning this answer before the graph was built. See below.
What does not work, and why
Four learning mechanisms are implemented and do not earn their place yet. I am listing them because a benchmark that only reports wins is not a benchmark.
- Semantic seeding ships disabled. An A/B and a threshold sweep from 0.60 to 0.85 showed it adds zero unique wins at every threshold. At 0.60 it created 1069 semantic edges, and only 4 landed on a real associative chain. Embedding similarity is orthogonal to the reasoning the corpus asks for, and it duplicated what vector search already found. The code stays; the default does not.
- Co-retrieval and feedback run but cannot be scored here. Both grow edges as designed — feedback lifted average reinforcement from 0.000 to 0.164 — on warm-up topics the holdout never traverses. Warm-up is disjoint from the holdout on purpose. Making them overlap would be train/test leakage, so this needs a new dataset design rather than a parameter change.
- Decay cannot be exercised by the benchmark. Ingestion stamps memories at a 2030 epoch, so every edge is future-dated relative to the read and decays by exactly 1.0. Unit tests cover decay instead. Fixing this means moving the epoch into the past.
- Maintenance and pruning are a regression check, not a measurement. Maintenance triggers on write, and the harness performs every write during ingest, before anything has aged. It deletes nothing. Measuring it needs writes interleaved with simulated time.
Alternatives
If you want a vector store, use Chroma, LanceDB, or sqlite-vec. They are
mature, and vector search is their whole job. SynapticDB is worth a look when
you want keyword and vector search fused in a single file with no service to
run, and a confidence value you can threshold to decide whether to answer at
all. The association graph is the research question the project exists to test,
and it has not paid off yet.
Development
SynapticDB requires Python 3.10 or newer. These checks are required in CI:
uv sync --extra dev
uv run ruff format --check .
uv run ruff check .
uv run mypy
uv run python -W error -m pytest
uv run python -W error -m bench --profile smoke --retriever fixture --check --no-write
uv run python -W error -m bench --profile smoke --retriever synaptic --check --no-write
The smoke profile uses a fixture embedding and needs no model download. Build
the distributions with uv run python -m build. The package is distributed and
imported as synapticdb.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file synapticdb-0.1.0.tar.gz.
File metadata
- Download URL: synapticdb-0.1.0.tar.gz
- Upload date:
- Size: 197.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec8fb7f86d587d5de1883e56aeaedc99e4e6259876987734a60858fdb09887d9
|
|
| MD5 |
ac769e155b765cb06b8cb38ae4b1405a
|
|
| BLAKE2b-256 |
f8026ab078b80669eae76bfd664796ac229bd428dd412fa730b15ff26a66ead3
|
Provenance
The following attestation bundles were made for synapticdb-0.1.0.tar.gz:
Publisher:
release.yml on xhillman/synapticdb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
synapticdb-0.1.0.tar.gz -
Subject digest:
ec8fb7f86d587d5de1883e56aeaedc99e4e6259876987734a60858fdb09887d9 - Sigstore transparency entry: 2253035677
- Sigstore integration time:
-
Permalink:
xhillman/synapticdb@188cc956750576356646ffcf8f9c4364ac0aa684 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/xhillman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@188cc956750576356646ffcf8f9c4364ac0aa684 -
Trigger Event:
push
-
Statement type:
File details
Details for the file synapticdb-0.1.0-py3-none-any.whl.
File metadata
- Download URL: synapticdb-0.1.0-py3-none-any.whl
- Upload date:
- Size: 42.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23c9374b8f45a37c19fe3a6dc5ca864729d9dcc4359fbc07508d679ca782eee8
|
|
| MD5 |
420a18e1e0190edf142bd4b13e06e488
|
|
| BLAKE2b-256 |
0c9d59dc394bdc8a873236bc7b9406f85c52bb7d9f417462adbb5b4c25f77a7c
|
Provenance
The following attestation bundles were made for synapticdb-0.1.0-py3-none-any.whl:
Publisher:
release.yml on xhillman/synapticdb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
synapticdb-0.1.0-py3-none-any.whl -
Subject digest:
23c9374b8f45a37c19fe3a6dc5ca864729d9dcc4359fbc07508d679ca782eee8 - Sigstore transparency entry: 2253035954
- Sigstore integration time:
-
Permalink:
xhillman/synapticdb@188cc956750576356646ffcf8f9c4364ac0aa684 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/xhillman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@188cc956750576356646ffcf8f9c4364ac0aa684 -
Trigger Event:
push
-
Statement type: