atomir
Atomic memory for LLM agents — atomic on both ends: facts are extracted and reconciled on write; questions are decomposed into sub-questions on read.
Why
Most memory systems store text blobs and retrieve with one fuzzy search. atomir doesn't:
- Write — split a message into atomic facts, then reconcile each (ADD / UPDATE-with-history / DELETE / NOOP). A similarity gate stops distinct facts over-merging.
- Read — decompose a question into sub-questions (only when useful), retrieve each, union the results. Surfaces facts a single-blob search misses.
Remember when, not just what. atomir adds an episodic layer: every message becomes a time-ordered event on a per-relationship timeline, and the fact store becomes a projection of that event log. When a state changes — a new job, a move, a new manager — the old value isn't overwritten and lost; it stays on the timeline. Temporal questions ("what job did I have before this one?", "who was my manager in 2023?") are answered by walking the timeline, not by similarity search — deterministic, self-hosted, no graph database. Two atomic ends (facts on write, sub-questions on read) over a temporal spine: facts answer now, events answer when.
Vendor-neutral: LLM, embedder, and store are interfaces chosen by config.
Defaults are fake, so it runs with no keys.
Install
pip install atomir # core, offline-capable
pip install "atomir[qdrant,api]" # Qdrant backend + HTTP API
pip install "atomir[langchain,langgraph]" # framework integrations
Quickstart
from atomir.assembly import build_memory_service
mem = build_memory_service() # backends from .env; defaults to fake (no keys)
mem.add("user123", "I'm vegetarian and my manager is Dana.")
hits = mem.search("user123", "who should I email about my project?")
print(hits["subquestions"], [r["text"] for r in hits["results"]])
mem.answer("user123", "who is my manager?") # composed answer + the facts used
mem.get_all("user123"); mem.delete("user123", fact_id); mem.reset("user123")
Real providers: copy .env.example → .env, then set backends + keys.
Providers
| Slot | Options | Config |
|---|---|---|
| LLM | fake groq openai anthropic gemini azure_openai ollama |
LLM_BACKEND, LLM_API_KEY, MODEL |
| Embedder | fake jina voyage openai gemini azure_openai ollama |
EMBED_BACKEND, EMBED_API_KEY, EMBED_DIM |
| Store | json qdrant |
STORE_BACKEND, STORE_URL / STORE_PATH |
Adding a provider is one class + one registry line. LLM_BASE_URL /
EMBED_BASE_URL target self-hosted or proxy endpoints.
Retrieval: reads fuse dense (embedding) + lexical (BM25) rankings via RRF and
run sub-question retrievals concurrently; set HYBRID_SEARCH=false for dense-only.
Provider calls retry transient failures (rate limits, connection resets).
Episodic memory (optional, experimental)
Set EPISODIC_ENABLED=true for an event-log layer alongside the atomic facts.
Messages become time-ordered events grouped into per-entity, per-verb
branches; the fact store becomes a projection of that log — so a "left Beta,
joined Acme" message leaves exactly one live employer (Acme) with Beta in
history, and the timeline keeps the transition.
Arbitration — facts answer now, events answer when. Reads route each
sub-question: current → facts, temporal → a deterministic chain walk,
semantic → the usual hybrid search.
mem.add("u", "I left Beta in November and joined Acme Corp.")
mem.timeline("u", branch="works_at") # ordered events (when things changed)
mem.forget("u", "Alex") # cascade-delete everything about an entity
New surface (all additive): timeline(...), forget(entity), GET /timeline,
POST /forget, MCP timeline / forget_about tools, and atomir migrate --backfill --user <id> for pre-episodic stores. Off by default, so upgrading
changes nothing until you opt in.
General-purpose by default. No ontology is assumed: branches emerge from
your own messages, kept consistent by feeding the existing registry back into the
extractor/namer/planner. The namer produces knowledge-graph predicates
(joined → works_at) so a query planner's natural hint resolves. For a known
domain, opt into a pack: ONTOLOGY_PACK=personal seeds ~18 predicates.
Branch matching strips entity names before embedding, uses a three-zone decision
(auto-assign / LLM judge / new), and resolves read-time hints by exact match then
relative-best. Every threshold is embedder-dependent — run
python -m eval.episodic.branch_microeval --write to calibrate
BRANCH_MATCH_AUTO / BRANCH_MATCH_GRAY_LOW / BRANCH_RESOLVE_FLOOR /
BRANCH_RESOLVE_MARGIN for your embedder.
Temporal questions are answered by walking a timeline rather than by similarity
search, so it recovers historical states that overwrite-based memory loses (a
former employer, a previous city). Deterministic, self-hosted, no graph DB. An
evaluation harness is included (eval/episodic/); formal benchmark results will
be published separately.
Honest limits: multi-entity graph queries (out of scope — Graphiti-class
systems own that); entity resolution is exact-alias by default (under-merges
rather than risk a wrong merge; ENTITY_V2=true adds embedding+LLM resolution);
JSON episodic store is dev-scale (append-heavy — pair with SQLite/Qdrant at
scale); and branch-naming quality varies with model strength — weak models
name inconsistently, mitigated by the KG-predicate namer and the calibration
harness, but a stronger model or a pack gives the most reliable chain walks.
Agent frameworks
atomir is the memory, not the model: recall before, remember after. Scope
memory by user_id — "user:1" (shared), "user:1#agent:x" (agent-private),
"acme|user:1" (multi-tenant).
LangChain — AtomirRetriever is a real BaseRetriever:
from atomir.integrations.langchain import AtomirMemory
mem = AtomirMemory(build_memory_service(), user_id="user:1")
retriever = mem.as_retriever()
LangGraph — drop-in nodes for multi-agent graphs:
from atomir.integrations.langgraph import recall_node, remember_node
g.add_node("recall", recall_node(mem)) # -> state["memories"]
g.add_node("remember", remember_node(mem)) # stores state["input"]
Agents coordinate through shared memory (persists across runs). Store durable
findings only. Runnable examples: examples/.
Claude / MCP
pip install "atomir[mcp]" exposes atomir as an MCP server, giving Claude
Desktop, Claude Code, Cursor, or any MCP client persistent memory. Add to the
client's MCP config:
"mcpServers": {
"atomir": {
"command": "atomir-mcp",
"env": {
"LLM_BACKEND": "openai", "LLM_API_KEY": "sk-...",
"EMBED_BACKEND": "openai", "EMBED_API_KEY": "sk-...", "EMBED_DIM": "1536",
"STORE_BACKEND": "json", "STORE_PATH": "/absolute/path/atomir-memory.json"
}
}
}
Claude then has four tools — remember, recall, list_memories, forget —
and carries memory across sessions. ATOMIR_USER namespaces the memory.
HTTP API
Run uvicorn atomir.api:app (or docker compose up). MemoryClient(url) wraps
these with identical shapes.
| Method | Path | Returns |
|---|---|---|
| POST | /memories {user_id, text} |
{operations, facts} |
| POST | /search {user_id, query, k?, decompose?} |
{subquestions, results} |
| POST | /answer {user_id, query, ...} |
{answer, subquestions, results} |
| GET | /memories?user_id= |
facts |
| DELETE | /memories/{id}?user_id= |
{deleted, id} |
| DELETE | /memories?user_id= |
{reset} |
| GET | /health |
{status, store, llm, embedder} |
Limitations
RECONCILE_MIN_SIM(default0.5) is embedder-dependent — re-tune witheval/tune.pywhen you switch embedders.- JSON store: atomic writes, but single-process and rewrites the whole file — dev / small scale only; use Qdrant otherwise.
- No multi-fact transactions; a partial
addself-heals on retry (writes are per-user serialized).
License
MIT
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.8.5.tar.gz.
File metadata
- Download URL: atomir-0.8.5.tar.gz
- Upload date:
- Size: 107.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 |
e011f57c680e201826e45714a06a08fb45fcc26793922c42b72699bcd0c82e82
|
|
| MD5 |
050cefdb9a0a625d87897cf186db0096
|
|
| BLAKE2b-256 |
83ea2ba71d8db37dbea2a530ba706735206eed6249c51197fe79bbbaa2fa66bd
|
File details
Details for the file atomir-0.8.5-py3-none-any.whl.
File metadata
- Download URL: atomir-0.8.5-py3-none-any.whl
- Upload date:
- Size: 103.4 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 |
5b1755602ea387062c03032579f59f9d40f838c6e24e24313aa6e07ce6602d09
|
|
| MD5 |
2d0d4f6511ec32536326b26fca820a05
|
|
| BLAKE2b-256 |
73a3c2f3d6d963f4f0c2b30f3a2d3fd5a4ce72e6639527ce319f664a31c518e7
|