tempora
Deterministic temporal operators for AI agent memory.
LLMs understand. Code keeps the books.
Agent memory breaks when facts change. Given
#0 "User lives in Delhi"
#41 "User lives in Bangalore"
#306 "User lives in Mumbai"
ask "Where did I live before Mumbai?" and production memory systems guess: on the public conflict benchmark (MemoryAgentBench, ICLR 2026) Mem0 scores 18% and Zep/Graphiti 7% — because every version of the fact lands in the prompt and one model call must untangle ordering, meaning, and answer at once.
Don't Ask the LLM to Track Freshness (May 2026) showed the
fix for the simplest case: let the LLM only find relevant facts, then pick the newest with
Python max() — jumping single-hop accuracy to ~95%. But max() answers exactly one question
shape. Real questions about changing facts have four:
| question | operator | resolver |
|---|---|---|
| "Where do I live?" | current |
timeline[-1] |
| "Where did I live before?" | previous |
timeline[-2] |
| "How many cities have I lived in?" | count |
len(timeline) |
| "List everywhere I've lived" | history |
", ".join(timeline) |
tempora implements the missing three, plus the one-call router that picks the right one.
Architecture
question ─────────────────────────────────────────────┐
│ │
▼ ▼
route() LLM sees only the question: extract() LLM sees facts + question:
"what KIND of returns one label "which facts are about this
question is this?" current|previous| subject?" Told explicitly NOT to
count|history judge recency. Returns ALL versions.
│ │
└────────────────────┬─────────────────────────────┘
▼
resolve() pure Python: sort candidates by serial,
dedupe, apply the one-line operator.
No LLM. Cannot hallucinate ordering,
cannot miscount, fully auditable.
Two constrained LLM calls for understanding; zero LLM involvement in the temporal logic.
Results
Both arms below share identical BM25 retrieval and identical candidate extraction — the only
difference is whether the deterministic step knows more operations than max(). Questions are
generated mechanically from MemoryAgentBench's own fact conflicts, so gold answers are computed
from data, never by an LLM.
| operator | n | newest-only baseline | tempora | router accuracy |
|---|---|---|---|---|
| current | 12 | 83% | 83% | 100% |
| previous | 12 | 8% | 83% | 92% |
| count | 12 | 0% | 83% | 100% |
| history | 12 | 0% | 83% | 100% |
| total | 48 | 23% | 83% |
The residual gap is candidate-extraction misses on a small open model, not resolver error — the resolvers are pure functions with unit tests.
We also reproduce the freshness paper's single-hop pipeline at 91%
(openai/gpt-oss-120b, n=100, 6k context), against published memory-system baselines of
54% (HippoRAG-v2), 18% (Mem0), and 7% (Zep/Graphiti).
Install
pip install git+https://github.com/nitininhouse/tempora.git
Core has a single dependency (rank-bm25, used only by the benchmarks). No LLM SDK: you pass
any callable (system: str, user: str) -> str.
Quickstart
import os
from tempora import answer
from tempora.llm_helpers import openai_compatible # optional convenience
llm = openai_compatible(api_key=os.environ["GROQ_API_KEY"]) # any OpenAI-compatible endpoint
facts = [ # (serial, text) — higher serial = recorded later
(0, "User lives in Delhi."),
(41, "User lives in Bangalore."),
(306, "User lives in Mumbai."),
]
answer("Where did the user live before Mumbai?", facts, llm)
# {'answer': 'Bangalore', 'operator': 'previous', 'candidates': [...]}
Run python examples/quickstart.py for all four operators against the same facts.
API
| function | does |
|---|---|
answer(question, facts, llm) |
full pipeline; returns {answer, operator, candidates} |
route(question, llm) |
classify the question into an operator name |
extract(question, facts, llm) |
LLM candidate extraction, all fact versions |
resolve(operator, candidates) |
pure-Python resolution; no LLM |
build_timeline(candidates) |
serial-ordered, deduplicated value list |
Reproduce the numbers
pip install -e ".[bench]"
export GROQ_API_KEY=... # or any OpenAI-compatible endpoint via base_url
cd benchmarks
python generate_operator_set.py # 160 questions from real MAB conflicts
python run_operators.py # baseline vs tempora, per-operator table
python run_freshness.py # single-hop reproduction
Prior art, honestly
- Don't Ask the LLM to Track Freshness proved deterministic-newest beats LLM judgment and named non-freshness operators as open future work. tempora is that future-work section, built.
- Temporal KGQA (TEQUILA, CronKGQA) used operator decomposition — before/after/first/last — over structured temporal knowledge graphs years ago. Agent memory forgot this; tempora re-applies the idea to serial-ordered conversational facts.
- Zep/Graphiti stores bitemporal history and Mem0 exposes a
history()API — storage without question routing, which is how a system that stores history still scores 7% answering questions about it.
Limitations
- Operator set derives from one benchmark's synthetic conflicts; phrasing diversity is limited.
- Serial order stands in for time. Inferring order from language ("after I graduated…") is the next milestone.
- Four operators. Real usage will want
as-of(date),still-valid,duration. - Multi-hop conflict chains remain open here as everywhere (published best ≤7%; the freshness paper's chain extension reaches ~30%).
License
MIT © Nitin Gupta
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 tempora_memory-0.1.0.tar.gz.
File metadata
- Download URL: tempora_memory-0.1.0.tar.gz
- Upload date:
- Size: 11.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfc5e22b693ece06cb87a340dada1cd34be6c4f66e5b92409b67934c05fe2570
|
|
| MD5 |
98d54c3033e9cd1fd802576174ec21e3
|
|
| BLAKE2b-256 |
64d75043a0fbacb21a1c74919bd3ab734f640fdd3ea0a52727351fae608cebea
|
File details
Details for the file tempora_memory-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tempora_memory-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b7b36fa1c4f97a5d90d43d61f829bf114ac41fc5c17349b0841dd7c0eae11aa
|
|
| MD5 |
20f13be1b832bdaf23158f906a0f8b4b
|
|
| BLAKE2b-256 |
e9fdb1a193d479407e276d019dc7faa303af2c92367bc8cc255b22a407ba4a70
|