Skip to main content

nous-state

A probabilistic agent state layer for long-running personal AI agents.

"Knowledge is prediction, not storage."

Python License: MIT arXiv


Paper

This repository accompanies the preprint:

Nous: A Predictive World Model for Long-Term Agent Memory Pranav Singh — Indian Institute of Technology Ropar arXiv:2606.22030 · cs.AI · June 2026

If you use this work, please cite:

@article{singh2026nous,
  title   = {Nous: A Predictive World Model for Long-Term Agent Memory},
  author  = {Singh, Pranav},
  journal = {arXiv preprint arXiv:2606.22030},
  year    = {2026}
}

To reproduce the LoCoMo benchmark results reported in the paper, see benchmark/README.md.


The Problem

Every long-running AI agent eventually hits the same wall:

User (Month 1): "I work at Sarvam AI using Mistral for NyayaSahayak."
User (Month 4): "I switched to GPT-4 and joined Google DeepMind."
Agent (Month 5): *confidently tells someone Pranav uses Mistral at Sarvam AI*

Vector databases store both facts. Knowledge graphs require manual conflict resolution. Neither gives you a mathematically principled answer to which fact is currently true.

nous-state solves this with Bayesian probability distributions — the same math used in GPS navigation, spam filters, and medical diagnostics.


How It Works

Instead of storing facts, Nous maintains belief distributions over entity attributes:

Pranav.employer = { "Sarvam AI": 0.82, "unknown": 0.18 }

When new evidence arrives, it performs a Bayesian update:

"Pranav joined Google DeepMind" →
Pranav.employer = { "Google DeepMind": 0.86, "Sarvam AI": 0.12, "unknown": 0.02 }

Every update is recorded as an immutable delta — a change in understanding, not just a fact. This means:

  • Contradictions are resolved mathematically, not heuristically
  • History is queryable — "What did the agent believe about Pranav in March?"
  • Forgetting is principled — unused beliefs decay toward uncertainty via entropy decay
  • Identity hints — two entity names with high mutual information are flagged as likely the same person

Install

pip install nous-state

Zero runtime dependencies. Pure Python stdlib only (math, sqlite3, json, urllib).

⚠️ Honest caveat: The engine's math is only as good as the extraction feeding it. The built-in rule-based extractor handles simple patterns. For messy real-world text, use the LLMExtractor — or bring your own parser. Garbage extraction → garbage beliefs, no matter how elegant the Bayesian update.


Quickstart

Rule-based extraction (no LLM needed)

from nous import Nous

memory = Nous("agent_memory.db")

# Session 1
memory.observe("Pranav works at Sarvam AI as an ML engineer.")
memory.observe("He is building NyayaSahayak using Mistral.")

# Session 4 — things changed
memory.observe("Pranav left Sarvam AI and joined Google DeepMind.")
memory.observe("NyayaSahayak now uses GPT-4 for better legal reasoning.")

# Query current beliefs
print(memory.predict("Pranav", "employer"))
# → {"Google DeepMind": 0.86, "Sarvam AI": 0.12}

print(memory.predict("NyayaSahayak", "model"))
# → {"GPT-4": 0.86, "Mistral": 0.12}

With LLM extraction (natural language → structured beliefs)

from nous import Nous
from nous.llm_extractor import LLMExtractor

extractor = LLMExtractor(
    api_key="sk-or-...",            # Any OpenRouter key
    user_context={"name": "Pranav"} # Resolves "I/me/my" → "Pranav"
)

memory = Nous("agent_memory.db", extractor=extractor)

memory.observe("I switched from Mistral to GPT-4 because legal reasoning improved.")
memory.observe("Actually wait, someone said Pranav is still at Sarvam AI?")
memory.observe("No confirmed, he's definitely at Google DeepMind on Gemini.")

print(memory.predict("Pranav", "employer"))
# → {"Google DeepMind": 0.97, "Sarvam AI": 0.03}

Explainability

# Full auditable history
for delta in memory.history("Pranav", "employer"):
    print(f"Surprise: {delta.surprise:.1f} bits | {delta.evidence[:60]}")
# → Surprise: 4.3 bits | Actually, I left Sarvam AI and joined Google...
# → Surprise: 0.2 bits | Pranav is definitely at Google DeepMind, I saw...

# Time-travel: what did the agent believe 30 days ago?
past_belief = memory.query_at("Pranav", "employer", at_time=timestamp_30_days_ago)

Surprise scoring

bits = memory.surprise("Pranav is still at Sarvam AI.")
# → 5.1 bits  (high — contradicts current belief)

bits = memory.surprise("Pranav works at Google DeepMind.")
# → 0.1 bits  (low — we already know this)

API Reference

Nous(db_path, extractor=None)

Method Description
observe(text, source, reliability) Ingest text, update beliefs
predict(entity, attribute) Get current probability distribution
query_at(entity, attribute, timestamp) Time-travel query
history(entity, attribute) Full delta log for an attribute
explain(entity, attribute, value) Why does the agent believe X?
surprise(text) Information content in bits before observing
get_coupling(entity_a, entity_b) Identity similarity hint (0–1)
get_entity_profile(entity) All attributes for an entity
apply_decay(current_time) Apply forgetting to stale dimensions

LLMExtractor(api_key, model, user_context)

Works with any OpenAI-compatible API endpoint (OpenRouter, OpenAI, local via LM Studio).

Parameter Default Description
api_key required Your API key
model google/gemini-2.5-flash Any model on OpenRouter
user_context {} Dict with name key to resolve "I/me/my"

Architecture

Natural Language
      ↓
LLMExtractor (or rule-based Extractor)
      ↓
(entity, attribute, value) tuples
      ↓
BayesianUpdater → surprise score → posterior distribution
      ↓
Dimension (probability distribution)   +   Delta (immutable history)
      ↓                                          ↓
WorldModel (in-memory cache)          DeltaLog (SQLite)
      ↓
PersistenceLayer (SQLite — survives restarts)

Key properties:

  • O(1) per-attribute reads — dictionary lookup, not vector search
  • O(k) writes — multiply k floats, normalise (k = number of known values, typically < 10)
  • Append-only delta log
  • Zero external dependencies

Where Nous Fits (and Where It Doesn't)

Problem Vector DB Knowledge Graph nous-state
Contradictory facts Stores both, LLM decides Manual conflict rules Bayesian update (automatic)
Stale high-confidence facts Still retrieved Still in graph Probability mass shifts
"Why does agent believe X?" Not possible Requires audit log Native (delta history)
Multi-hop relational queries ❌ ✅ Native ❌
Semantic document retrieval ✅ Native ❌ ❌
Per-attribute read cost O(n) ANN search O(edges) traversal O(1) dict lookup

Research Status

This is research-stage code. The preprint (arXiv:2606.22030) describes this as a first public report — not a final or fully audited result. Ablations, a second benchmark (LongMemEval), and broader backbone evaluation are planned as immediate next steps and will be reported in a future revision.

The benchmark/ directory contains the evaluation code that produced the LoCoMo numbers in the paper. See benchmark/README.md for reproduction instructions.


License

MIT — see LICENSE.

Contributing

Issues and PRs welcome. If you hit a real-world edge case, opening an issue is genuinely valuable.

Metadata

Release files for nous-state 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nous-state 0.2.0
File Size Uploaded
nous_state-0.2.0.tar.gz 52.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nous-state 0.2.0
File Interpreter ABI Platform
nous_state-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 101.5 kB

Release files / nous_state-0.2.0.tar.gz

Download URL nous_state-0.2.0.tar.gz
Size 52.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ac6524a2b9efb823979eba2368e80dbd628fb284f0e64d819e3afce01a3365e5
BLAKE2b-256 checksum
How to use checksums
dd8978f33727776a30cb35ca07bb51dedd8b600b2f94eeba9ffcd0a2d0999296
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / nous_state-0.2.0-py3-none-any.whl

Download URL nous_state-0.2.0-py3-none-any.whl
Size 49.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad5be91ab5e0e68c6b228a2e484a3ebc4af2beda3dac45ebb1a4a0c9dff500b0
BLAKE2b-256 checksum
How to use checksums
950d5b003169d3ce0f5eb68852b779e106e7b6de29480fe109c63994dcbb97f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page