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 Groq↔OpenAI,
Jina↔Voyage, or Qdrant↔pgvector is one config change plus one small class.
Defaults use fake backends, so everything runs with no external keys.
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} |
| 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.
Known limitations
- Reconciler threshold is untuned.
RECONCILE_MIN_SIMdefaults to0.6; on Jina, real "same-attribute" pairs measured ~0.6, so it sits right on the edge. It should be tuned per embedder with the eval harness, not trusted as-is. - The JSON backend is NOT crash-safe. It rewrites the whole file without atomic replace/fsync — dev and tests only. Use Qdrant for durable storage.
- No transactions. Writes are serialized per user with a simple lock (Step 9); full transactional rollback is deferred (DECISION #5).
- Read returns facts, not a composed answer.
searchreturns the relevant facts and sub-questions; turning them into a final sentence is the caller's LLM's job.
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.1.0.tar.gz.
File metadata
- Download URL: atomir-0.1.0.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47af75d499017515c05bf86281e98fb1300a5b208e1a119ad9d16d5c2b53a8cb
|
|
| MD5 |
5fc380a67c7cac2c17e150033d1bcc5b
|
|
| BLAKE2b-256 |
8ac6f2f902e38e01fcba3ee136a33e6b0cfd80468b5b1ccfc6a944646ea1f903
|
File details
Details for the file atomir-0.1.0-py3-none-any.whl.
File metadata
- Download URL: atomir-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.2 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 |
f9231a5632c29e98f7a567c20e93d7e2b7e718a618458bde4e8f562caf707f39
|
|
| MD5 |
326e5e8cd98a6c7ce602376c5ddb15ca
|
|
| BLAKE2b-256 |
5a672aaf7ce44061f67a9598733a1f948301a2dd3164b251fcc360497ad6a1a4
|