Skip to main content
Glama
Verace-Pvt-Ltd

agentic-memory

README.md
<p align="center">
  <img src="assets/banner.svg" alt="Agentic Memory β€” A Persistent, Bitemporal, Injection-Safe Memory MCP for AI Agents, by Verace Pvt. Ltd." width="100%">
</p>

<p align="center"><b>A Persistent, Bitemporal, Injection-Safe Memory MCP for AI Agents</b></p>

> A memory server for coding agents (Claude Code, Claude Desktop, Cursor, Antigravity, Codex CLI, or any
> MCP-compliant client) that goes beyond the naive "five separate buckets" model of agent memory:
> non-destructive belief revision, injection-safe context synthesis, real embeddings with ANN search,
> multi-tenant namespace isolation, and a choice of a zero-dependency SQLite backend or a
> Postgres + pgvector + Row-Level-Security backend for real deployments.

---

## πŸ›οΈ Architecture

Six memory layers behind one unified retrieval call. Diagram renders natively on GitHub (Mermaid) β€”
open this file on github.com if you're viewing raw markdown:

```mermaid
flowchart TD
    ENV(["Real-Time Environment<br/>user queries / tool output"])

    subgraph L1["1 Β· SENSORY MEMORY"]
        L1D["Ring buffer, saliency-scored<br/>Retention: ms β†’ minutes"]
    end

    subgraph L2["2 Β· WORKING MEMORY"]
        L2D["Token-budgeted session context<br/>chronological, auto-compressing<br/>Retention: minutes β†’ hours"]
    end

    subgraph L3["3 Β· EPISODIC MEMORY"]
        L3D["Causal event trajectories<br/>vector similarity retrieval<br/>Retention: weeks β†’ months"]
    end

    subgraph L4["4 Β· SEMANTIC MEMORY"]
        L4D["Subject-predicate-object graph<br/>non-destructive belief revision<br/>hybrid vector + text + PPR search"]
    end

    subgraph L5["5 Β· LONG-TERM MEMORY"]
        L5D["Ebbinghaus-decay retrievability<br/>Truth Maintenance System<br/>Retention: days β†’ years"]
    end

    subgraph L6["6 Β· PROCEDURAL MEMORY"]
        L6D["Registered skills / action recipes<br/>success-rate tracked"]
    end

    UQ{{"unified_query()<br/>ranks + fuses all 6 layers by one<br/>relevance function (similarity +<br/>recency + importance + graph proximity)"}}
    OUT(["One synthesized,<br/>injection-safe context block"])

    ENV --> L1
    L1 -- "promotion on high saliency" --> L2
    L2 --> L3
    L2 --> L4
    L3 <-.-> L4
    L3 --> L5
    L4 --> L5

    L1 --> UQ
    L2 --> UQ
    L3 --> UQ
    L4 --> UQ
    L5 --> UQ
    L6 --> UQ
    UQ --> OUT

    classDef sensory fill:#d1f5d3,stroke:#2e7d32,color:#1b1b1b
    classDef working fill:#e3d9f7,stroke:#6a3fb5,color:#1b1b1b
    classDef episodic fill:#c9dcf7,stroke:#2451a8,color:#1b1b1b
    classDef semantic fill:#c3ecd9,stroke:#1b7a4d,color:#1b1b1b
    classDef longterm fill:#cfe8ff,stroke:#1565c0,color:#1b1b1b
    classDef procedural fill:#ffe6cc,stroke:#e07b00,color:#1b1b1b
    classDef fusion fill:#111111,stroke:#111111,color:#ffffff

    class L1,L1D sensory
    class L2,L2D working
    class L3,L3D episodic
    class L4,L4D semantic
    class L5,L5D longterm
    class L6,L6D procedural
    class UQ,OUT fusion
```

A single `unified_query` call ranks and fuses all six layers by one consistent relevance function
(embedding similarity + recency + importance + graph proximity), then returns **one** synthesized,
injection-safe context block β€” not six separate results the caller has to stitch together itself.

### What makes this different from a naive per-layer memory store

- **Non-destructive belief revision.** Updating a fact never silently overwrites it. The old belief is
  marked superseded (with a timestamp and a pointer to what replaced it) rather than deleted β€” full
  audit history via `get_belief_history()` / `get_semantic_belief_history()`.
- **Injection-safe context synthesis.** Every retrieved memory item is wrapped in
  `<memory trust="..." source="...">` tags with an explicit preamble telling the LLM that memory content
  is untrusted historical data, not instructions. **All** angle brackets in stored content are escaped
  (not just the `<memory>` tag itself) β€” so a stored item can't forge *any* tag, including ones like
  `<system>`, to break out of its fence.
- **Write-time trust isn't blind.** Every stored item carries a `trust` level. Self-reported, unverified
  claims (an agent's own confidence/success/reward score) are ranking-discounted relative to system- or
  user-provided facts β€” an agent can't inflate its own memory's influence just by claiming high
  importance. A `verify_memory()` call lets a separate, more-trusted reviewer corroborate an item after
  the fact and restore full ranking weight; the original writer can never call it on its own write.
- **Contradiction flagging, not silent overwrite.** A cheap antonym-predicate heuristic flags directly
  conflicting facts about the same subject (e.g. "Dan LIKES pizza" vs "Dan HATES pizza") for review
  instead of guessing which one is right β€” this is *not* general semantic contradiction detection (an
  open NLP problem), just a fast, honest, zero-model check for the specific opposite-predicate case.
- **Real embeddings, with a safety net.** Defaults to `sentence-transformers` (all-MiniLM-L6-v2) when
  installed; falls back to a deterministic hash embedder otherwise. The vector index dimension is
  auto-detected from whichever embedder is actually active β€” mixing a database built with one embedder
  and a process running another raises a clear, actionable error at startup instead of silently
  corrupting or crashing mid-session.
- **Real ANN search, with a safety net.** Uses `hnswlib` HNSW when installed; falls back to brute-force
  NumPy cosine search otherwise β€” and near-exhaustive queries route to brute-force even when HNSW is
  available, since HNSW is optimized for k β‰ͺ n and is measurably *slower* than a vectorized brute-force
  pass once k approaches the corpus size.
- **Multi-tenant namespace isolation.** Every memory item carries a `namespace`. On the Postgres backend
  this is enforced by Postgres Row-Level Security β€” not just an app-layer filter that a bug could skip.
  Consolidation, reflection, and graph export are all namespace-scoped too β€” none of them mix or leak
  data across tenants.
- **Two backends, one API.** SQLite by default (zero external dependencies, single-process). Postgres +
  pgvector + RLS for real multi-writer, multi-tenant deployments β€” same `MemoryEngine` API either way.
  Optional Qdrant and Milvus vector-store connectors are also included for teams that want a dedicated
  vector database instead.

---

## πŸ“¦ Installation

One package, every feature included by default (real `sentence-transformers` embeddings, `hnswlib` ANN
search, and Postgres/pgvector support are all installed out of the box β€” no extras to pick). Not on
PyPI yet β€” install straight from GitHub for now:

```bash
pip install "agentic-memory @ git+https://github.com/Verace-Pvt-Ltd/agentic-memory.git"
```

Or clone and install locally:

```bash
git clone https://github.com/Verace-Pvt-Ltd/agentic-memory.git
cd agentic-memory
pip install -e .
```

### Connect it to your AI coding tool

**Claude Code (recommended β€” plugin marketplace, gets autonomous hooks too):**

```
/plugin marketplace add Verace-Pvt-Ltd/agentic-memory
/plugin install agentic-memory@agentic-memory
```

Same two steps non-interactively (CI, setup scripts) via the CLI:

```bash
claude plugin marketplace add Verace-Pvt-Ltd/agentic-memory
claude plugin install agentic-memory@agentic-memory
```

This registers the MCP server through Claude Code's [plugin marketplace system](https://code.claude.com/docs/en/plugin-marketplaces)
and auto-registers 9 autonomous hooks (`hooks/hooks.json`) that read and write memory on every turn
without the agent needing to call an MCP tool explicitly β€” see
[Autonomous operation via hooks](#autonomous-operation-via-hooks) below. Verify with
`claude plugin details agentic-memory@agentic-memory`, which should list `Status: enabled` and
`Hooks (9)`. The Python package still needs to be installed separately first (see
[Installation](#-installation) above) β€” the plugin distributes the MCP registration, not a bundled
Python runtime.

**Any other tool (Cursor, Antigravity, Codex CLI, Claude Desktop, or any other MCP-capable agent):**
see **[SETUP.md](SETUP.md)** β€” copy one prompt into your agent's chat and it detects your tool and
configures itself, including autonomous hooks where that tool supports them.

<details>
<summary>Manual installation per tool (if you'd rather not use the plugin marketplace or SETUP.md prompt)</summary>

**Claude Code (manual, no plugin system β€” MCP tools only, no autonomous hooks):**

```bash
claude mcp add agentic-memory -- agentic-memory --transport stdio
```

**Any other MCP client** (Claude Desktop, Cursor, Antigravity, Codex CLI β€” MCP tools only, no
autonomous hooks unless configured per [SETUP.md](SETUP.md)):

```bash
# Auto-install into Claude Desktop's config
agentic-memory --setup-claude

# Print copy-paste config snippets for Claude Desktop, Cursor, Antigravity, and Codex CLI
agentic-memory --print-config
```

</details>

MCP (Model Context Protocol) is a shared standard across all of these β€” the same server works with any
of them via:

```json
{
  "mcpServers": {
    "agentic-memory": {
      "command": "agentic-memory",
      "args": ["--transport", "stdio"]
    }
  }
}
```

---

## πŸͺ Autonomous operation via hooks

MCP tools require the agent to decide to call them. Hooks make memory read/write happen
automatically, tied to the host tool's own lifecycle events (a new session starting, a prompt
being submitted, a tool call succeeding or failing, the session stopping) β€” no explicit tool
call needed. Four tools have a real hook/lifecycle system this project integrates with, each
adapter living in its own module (`memory/hooks.py`, `memory/hooks_antigravity.py`,
`memory/hooks_cursor.py`, `memory/hooks_codex.py`) with a matching config template under
`hooks/`. Confidence differs per platform β€” verified against real installed binaries where one
was available in this environment, doc-derived only where it wasn't:

| Tool | Events wired | Confidence |
| :--- | :--- | :--- |
| **Claude Code** (plugin marketplace) | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop, TaskCompleted, PreCompact, SessionEnd | **Confirmed working end-to-end** β€” verified via `claude plugin details`, which reports `Status: enabled` and `Hooks (9)` against a real installed instance. |
| **Antigravity** (`agy`) | PreToolUse, PostToolUse, PreInvocation, Stop | **Confirmed to execute** β€” a real tool-triggering prompt through a live `agy` install produced the hook's own stderr output (model load) in `agy`'s logs, with no error. The resulting database write location wasn't independently confirmable from outside `agy`'s process (possible subprocess sandboxing), so treat as working-but-not-fully-proven rather than broken. |
| **Cursor** (`cursor-agent` / Cursor IDE) | sessionStart, beforeSubmitPrompt, beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, afterFileEdit, afterAgentResponse, stop, sessionEnd | **Schema confirmed valid** against Cursor's own docs; **not confirmed to fire** β€” `cursor-agent` requires an interactive login this environment didn't have. A community report also states several of these (sessionStart, beforeSubmitPrompt, stop, afterAgentResponse) are documented but don't currently execute in Cursor CLI specifically, only in the Cursor IDE β€” CLI users may see partial coverage until this is independently confirmed. |
| **Codex CLI** | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, PreCompact, SessionEnd | **Doc-derived only** β€” no `codex` binary was available in this environment to verify against, so neither the schema nor the firing behavior has been live-tested. |

Claude Desktop and any other generic MCP client have no hook/lifecycle system to integrate
with β€” they get the 16 MCP tools only, called explicitly by the agent.

See [SETUP.md](SETUP.md) for the exact install steps per platform, including where each
`hooks/*.json` template needs to be copied.

---

## 🐘 Optional: Postgres / Supabase backend

For a real deployment with concurrent multi-process writers and enforced multi-tenant isolation, run
Agentic Memory against Postgres + pgvector instead of SQLite. Self-hosted via Docker, no cloud account
required:

```bash
docker compose up -d          # starts Postgres + pgvector (bare, unconfigured)
python -m memory.backends.postgres --migrate --dsn postgresql://postgres:postgrespassword@localhost:5433/memory
```

The migration step auto-detects the correct vector column dimension from whichever embedder is active
in your Python environment (via `get_embedding_dim()`) and generates a secure app-role password if you
don't supply one β€” nothing about the schema is hand-edited or hardcoded.

```python
from memory.backends.postgres import PostgresBackend
from memory.engine import MemoryEngine

backend = PostgresBackend(dsn="postgresql://postgres:postgrespassword@localhost:5433/memory")
engine = MemoryEngine(backend=backend)
```

Row-Level Security policies enforce `namespace` isolation at the database engine level β€” a query for
`namespace="tenant_a"` genuinely cannot see `tenant_b`'s rows, even if application code has a bug.

**Known limitations of the self-hosted Docker setup**: no automated backups/point-in-time recovery, no
high-availability/failover (single-container Postgres), and no encryption-at-rest beyond whatever the
underlying disk provides β€” all buildable, none included out of the box today.

---

## πŸ”Œ MCP Tools

| Tool | Purpose |
| :--- | :--- |
| `sensory_ingest` | Ingest a real-time event into sensory memory |
| `working_memory_update` | Add a turn to the active session's working memory |
| `episodic_record` | Log a task trajectory; pass `episode_id` from an earlier call to update it instead of creating a duplicate |
| `semantic_upsert` | Add/revise a subject-predicate-object fact (flags antonym contradictions) |
| `long_term_store` | Store a durable preference or fact (Truth Maintenance on update) |
| `procedural_register` | Register a reusable skill/action recipe (upserts by name β€” won't duplicate) |
| `unified_query` | Fused, ranked, injection-safe context across all layers; optional `top_k` cap |
| `query_as_of` | Point-in-time retrieval using bitemporal fields |
| `get_belief_history` | Full revision history for a long-term memory key |
| `get_semantic_belief_history` | Full revision history for a semantic fact |
| `verify_memory` | Mark a fact/episode/skill as corroborated by a trusted reviewer β€” never by its own writer |
| `forget` | GDPR-style hard delete (distinct from non-destructive supersede) |
| `evict` | Namespace-scoped capacity/decay-based pruning |
| `reflect` | Heuristic pattern synthesis over recent episodes/facts, namespace-scoped, deduplicated |
| `graph_export_dot` | Export the semantic knowledge graph as Graphviz DOT, namespace-scoped |
| `trigger_consolidation` | Run the sensory→working→episodic→semantic→long-term promotion cycle, namespace-scoped |
| `inspect_memory_health` | Per-layer counts and operational metrics |

Resources: `memory://sensory/stream`, `memory://working/active`, `memory://semantic/graph.dot`,
`memory://longterm/user`. Prompt: `synthesize_context`.

---

## 🐍 Python SDK Quickstart

```python
from memory import MemoryEngine

engine = MemoryEngine(db_path="memory.db")  # SQLite by default

engine.sensory_ingest("User clicked urgent alert", source="ui_event")
engine.working_update("user", "Can you deploy the payments service to prod?")
engine.working_update("assistant", "Running the blue-green deploy pipeline now.")

engine.episodic_record(
    title="Payments deploy",
    goal="Deploy payments service to prod",
    context="prod cluster",
    steps=[{"action": "run_pipeline", "input": {}, "output": {"status": "ok"}, "success": True}],
    outcome="success",
)

engine.semantic_add("User", "PREFERS", "blue-green deploys", confidence=0.9)
engine.long_term_store(
    "user_preference", "deploy_strategy", "blue-green",
    summary="User always wants blue-green deploys for prod",
    importance=0.9, is_pinned=True,
)

result = engine.query("deploy payments service to production")
print(result.synthesized_prompt_context)   # one injection-safe, ranked context block

stats = engine.consolidate()   # promote sensory -> working -> episodic -> semantic -> long-term
```

Every write accepts `namespace=` (default `"default"`) and `trust=` (default `TrustLevel.USER`) for
multi-tenant scoping and provenance tracking.

---

## πŸ§ͺ Testing

```bash
python3 -m pytest tests/ -v
```

98 tests covering persistence across restart, non-destructive belief revision, prompt-injection
fencing (including non-`<memory>` tag names), namespace isolation across every subsystem (query,
consolidation, reflection, DOT export, belief history), trust-weighted ranking and the verification
mechanism, contradiction flagging, ANN index correctness (with and without `hnswlib` installed),
embedding-dimension mismatch fail-fast behavior, eviction, Postgres dialect translation logic, and
the Claude Code / Antigravity / Cursor / Codex CLI hook adapters. The
Postgres backend has been verified against a real running instance (writes, reads, RLS enforcement,
`forget`, `evict`) β€” not just reviewed statically.

---

## ⚠️ Known limitations

- No published benchmark comparison (LoCoMo, LongMemEval, or similar) against Mem0, Zep/Graphiti, or
  MemGPT/Letta exists yet β€” retrieval quality claims should be treated as unvalidated against other
  systems until that exists. Internal, judge-free retrieval measurements against the MemFail benchmark
  (arXiv:2605.26667) are available in `benchmarks/`.
- `reflect()` and episodic→semantic extraction (`trigger_consolidation`) use real LLM synthesis when
  called live through an MCP client that supports
  [MCP sampling](https://modelcontextprotocol.io/docs/concepts/sampling) β€” the server asks the
  *connected client's own model* to synthesize insights / extract triples (`ctx.sample(...)` in
  `mcp_server.py`), so no separate API key or subscription is needed. This only works for live tool
  calls, since sampling requires an active client connection: headless paths (autonomous hooks, the
  background consolidation loop) have no live client to sample from and fall back to
  `HeuristicReflector`/`HeuristicExtractor` (template sentences / regex patterns) β€” real coverage,
  but not LLM-quality synthesis. If a sampling call fails or the connected client doesn't support it,
  the same heuristic fallback applies automatically, so `reflect()`/`trigger_consolidation()` never
  produce nothing. `LLMReflector`/`LLMExtractor`/`StructuredPromptExtractor` (in `consolidator.py` and
  `utils.py`) remain available as a separate, explicitly-opt-in path for anyone who wants headless
  LLM-backed extraction/reflection backed by their own `Callable[[str], str]` (e.g. a direct API call) β€”
  not wired in by default.
- Contradiction detection only catches the specific antonym-predicate-on-same-subject case (Tier 1). It
  does not detect general semantic contradictions between differently-worded facts β€” that remains an
  open problem no memory system solves generically without an expensive, unreliable LLM pass at write time.
- Postgres ANN search currently loads embeddings into an in-process index rather than querying
  pgvector's native index directly β€” works well at moderate scale, doesn't yet solve the
  "dataset too large for one process's memory" case that's part of the reason to use Postgres at all.
- Qdrant/Milvus connectors are real (genuine client calls, no fallback stub) but have only been exercised
  in local/embedded mode (`:memory:` Qdrant, `milvus-lite`) β€” not against a remote/distributed cluster.

---

## 🀝 Contributing

Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## πŸ“œ License

MIT License β€” see [LICENSE](LICENSE). Β© Verace Pvt. Ltd.