Atomic memory infrastructure for agents — atomic facts on write, atomic sub-question decomposition on read.
Project description
atomir
Atomic memory infrastructure for agents. Memory is atomic on both ends: atomic facts on write (extract → reconcile), atomic sub-question decomposition on read (decompose → retrieve per sub-question → union).
The thesis
Most memory systems store raw text blobs and retrieve with a single fuzzy similarity search. atomir does the opposite at both ends:
- Write — a message is split into small, self-contained facts, and each is reconciled into memory (ADD new, UPDATE a changed value keeping history, DELETE what's no longer true, NOOP duplicates). A similarity gate biases toward ADD so distinct facts never over-merge.
- Read — a question is decomposed into atomic sub-questions (only when it helps), each retrieved independently, then results are unioned. This surfaces facts a single whole-question embedding misses.
Vendor-neutral by construction
The LLM, the embedder, and the vector store are each an interface chosen at
runtime by config ({provider, config} blocks). The engine imports only the
interfaces — never a provider SDK or vendor name. Swapping providers is one
config change; adding a new one is a single class plus a registry line.
Defaults use fake backends, so everything runs with no external keys.
| Slot | Built-in providers |
|---|---|
| LLM | fake, groq, openai, anthropic, ollama |
| Embedder | fake, jina, voyage, openai, ollama |
| Store | json, qdrant |
Each provider is selected by LLM_BACKEND / EMBED_BACKEND / STORE_BACKEND
with its key in LLM_API_KEY / EMBED_API_KEY (Ollama and the fakes need none).
LLM_BASE_URL / EMBED_BASE_URL can point at self-hosted or proxy endpoints.
Install
pip install -e . # core (offline: fake LLM + fake embedder + JSON store)
pip install -e ".[qdrant]" # add the Qdrant backend
pip install -e ".[api]" # add the FastAPI server
pip install -e ".[all]" # everything
groq and jina need no extra — they call their HTTP APIs over the standard
library.
Quickstart — embedded, no Docker
Runs fully offline with the default fake backends:
from atomir.assembly import build_memory_service
mem = build_memory_service() # backends chosen by .env
mem.add("user123", "I'm vegetarian and my manager is Dana Lopez.")
mem.add("user123", "I'm working on Project Atlas.")
hits = mem.search("user123", "who should I email about my project?")
print(hits["subquestions"]) # the sub-questions it asked
for r in hits["results"]:
print(r["text"], round(r["score"], 3))
mem.get_all("user123")
mem.delete("user123", fact_id)
mem.reset("user123")
To use real providers, copy .env.example to .env and set the keys/backends.
Production — Docker Compose (API + Qdrant server)
cp .env.example .env # optional: add real keys; without it, LLM/embedder run fake
docker compose up --build # brings up the API and a Qdrant server
The API points at the Qdrant service via STORE_URL=http://qdrant:6333. Then:
curl -XPOST localhost:8000/memories -H 'content-type: application/json' \
-d '{"user_id":"u1","text":"My manager is Dana."}'
curl -XPOST localhost:8000/search -H 'content-type: application/json' \
-d '{"user_id":"u1","query":"who is my manager?"}'
HTTP endpoints
| Method | Path | Body / query | Returns |
|---|---|---|---|
| POST | /memories |
{user_id, text} |
{operations, facts} |
| POST | /search |
{user_id, query, k?, decompose?} |
{subquestions, results} |
| POST | /answer |
{user_id, query, k?, decompose?} |
{answer, subquestions, results} |
| GET | /memories |
?user_id= |
list of facts |
| DELETE | /memories/{id} |
?user_id= |
{deleted, id} (404 if absent) |
| DELETE | /memories |
?user_id= |
{reset} |
| GET | /health |
— | {status, store, llm, embedder} |
MemoryClient(base_url) (in atomir.client) wraps these with the same method
names and return shapes.
Configuration
All config is read from the environment (see .env.example): LLM_BACKEND,
LLM_API_KEY, MODEL, EMBED_BACKEND, EMBED_API_KEY, EMBED_DIM,
RECONCILE_MIN_SIM, STORE_BACKEND, COLLECTION, STORE_URL, STORE_PATH.
Composed answers
search returns facts (+ sub-questions); answer additionally composes a
grounded final sentence from them (LLM told to use only the retrieved facts):
mem.answer("user123", "who should I email about my project?")
# -> {"answer": "...", "subquestions": [...], "results": [...]}
Robustness
Provider calls retry transient failures (HTTP 429 rate-limits and 5xx) with
backoff, honoring Retry-After. The JSON store writes atomically (temp file →
fsync → os.replace), so a crash mid-write can't corrupt the file.
Known limitations
RECONCILE_MIN_SIMis embedder-dependent. The default0.5is tuned for Jina viaeval/tune.py(it sits between measured unrelated ~0.45 and same-attribute ~0.60 similarity). Switching embedders? Re-runeval/tune.py.- JSON store is single-process. Writes are now atomic (no corruption), but it holds no cross-process lock and rewrites the whole file per save — great for dev and small deployments; use Qdrant at scale.
- No multi-fact transactions. Each write is atomic and per-user serialized
(Step 9), and a partial
addis self-healing on retry (reconcile NOOPs facts already stored). Full all-or-nothing rollback across anaddis deferred (DECISION #5) — open an issue if you need it.
Project details
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 atomir-0.3.0.tar.gz.
File metadata
- Download URL: atomir-0.3.0.tar.gz
- Upload date:
- Size: 29.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6579cad315304e4b978c742337db342fd6e9e95a189b763e96fa459a3c38ce13
|
|
| MD5 |
f1108b93ec9dd56c36a4e760fca45dc8
|
|
| BLAKE2b-256 |
c9811719ce2a481f3ef0083fa00a4d8cdab1dcf9b96777ba7b80e4ad57603694
|
File details
Details for the file atomir-0.3.0-py3-none-any.whl.
File metadata
- Download URL: atomir-0.3.0-py3-none-any.whl
- Upload date:
- Size: 40.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f75a235b79a1383735fb0b93c9969c835564167970f62dc248d85ff4b096a76
|
|
| MD5 |
fd831047be1de7fdfe4d335bd17496ea
|
|
| BLAKE2b-256 |
4cc7da4dba26efd900d9ed648c91c9871b032b701bf26de13c2efc212cfa8037
|