Agent memory

Build agents from decisions, not documents.

Most AI memory is RAG over PDFs — a search index over text humans wrote about their work. InvariantDB stores the work itself: every access, every context, every reasoning step, every decision, with bitemporal provenance. Replay any past state, ask what did the agent know when it acted, train new agents from real behavior. Memory without amnesia.

A graph database is a brain. A vector database is recall. The difference matters the moment your agent has to answer why did I think that? or what did I know at 9am yesterday? — questions similarity search literally cannot answer.

First, create an API key. Every code example below assumes you have a gq_... key. Get yours →  ·  Then either set INVARIANTDB_API_KEY=gq_... in your environment or pass it to the SDK constructor.

Why graph + bitemporal, not just vector

A vector database holds chunks of text and finds similar ones. That's recall. It can't tell you:

InvariantDB stores every property along two time axes — VALID (the real-world effective date) and RECORDED (the moment the database wrote it). It records the agent's working memory as a structured graph. It chains every mutation in a tamper-evident audit log. And it natively understands "this belief was derived from these episodes."

Vector search is still here — built in, with HNSW indexes — so RAG patterns work too. But the graph + bitemporal layer underneath is what lets you replay any past decision.

The seven primitives

1. Episodic memory — db.recordEpisode / db.replayEpisodes

Per-agent sequential event log. One row per observation / intermediate thought / tool call / decision. Time-ordered, queryable, audit-grade.

// Record an event in the agent's session
CALL db.recordEpisode(
  'conv-abc-123',          // session id
  'observation',           // kind: observation | thought | toolCall | decision
  'User asked about Q3 forecast',
  {topic: 'forecast', source: 'user_message'}   // payload
)
YIELD eventId, ts;

// Replay the session — last 50 events
CALL db.replayEpisodes('conv-abc-123', 50)
YIELD eventId, ts, kind, text, payload
RETURN ts, kind, text, payload
ORDER BY ts;

Conversation-scoped sub-tenancy restricts a Capability.session token to a single session id — perfect for per-conversation isolation.

2. Inference provenance + belief revision — db.recordBelief / db.reviseBelief / db.beliefLineage

When an agent forms a belief from evidence, record the derivation. When it later revises the belief, you don't lose the prior version — the lineage walks back through every revision.

// "I believe customer acme will churn, based on these episodes"
CALL db.recordBelief(
  'churn-risk-acme',
  'High churn risk',
  0.85,
  ['event-id-99', 'event-id-104', 'event-id-112']
)
YIELD beliefId;

// Later: revise after new evidence
CALL db.reviseBelief(
  'churn-risk-acme',
  'Moderate churn risk',
  0.55,
  'saved by upsell'
)
YIELD oldBeliefId, newBeliefId, revisedAt;

// Walk every version + its evidence (hops=3 populates depth + evidence)
CALL db.beliefLineage('churn-risk-acme', 3)
YIELD beliefId, claim, confidence, createdAt, supersededAt, depth, evidence;

You can replay not just what the agent believes, but why — and what it believed before.

3. Working-memory consolidation — db.consolidateSession / db.sessionSummaries

Roll a session's raw episodes into a structured summary. Useful for context-window compression: keep last N raw events + a running summary of everything older.

// Fold a session into a structured summary (optional: pass eventIds to
// limit which events are attached to the SUMMARIZES edges). Client-side
// keep-recent windowing composes on top — events are never deleted.
CALL db.consolidateSession(
  'conv-abc-123',
  'Q3 forecast conversation: user asked about revenue, agent walked forecast + risks.'
)
YIELD summaryId, eventsCovered;

// List all summaries oldest first
CALL db.sessionSummaries('conv-abc-123')
YIELD summaryId, summary, eventsCovered, createdAt;

The summary is a first-class node — searchable, walkable, and itself versioned bitemporally. Re-consolidate later, the previous summary stays in history.

4. Salience decay + reinforcement — db.applySalienceDecay / db.reinforceSalience

Not every memory is equally important. Salience is a node property that decays with time and reinforces with each access.

// Reinforce when a memory is used in a successful response
CALL db.reinforceSalience($episodeId, 1.0)
YIELD strId, salience;

// Periodically decay by label (halve every 6h since last access, floor 0.05)
CALL db.applySalienceDecay('Event', 21600, 0.05)
YIELD label, decayed;

// Recall by salience
MATCH (e:Event)
WHERE e.session_id = 'conv-abc-123'
RETURN e ORDER BY e.salience DESC LIMIT 20;

Decay + reinforcement give you a memory that prioritizes without you hand-coding eviction logic.

5. Conversation sub-tenancy

Mint a capability token scoped to one conversation:

curl -fsS -X POST -H "Authorization: Bearer $ADMIN_KEY" \
    -H "Content-Type: application/json" \
    --data '{
      "graph": "memory",
      "principal": "agent-deepthought",
      "scope": ["cypher_query"],
      "session": "conv-abc-123",
      "ttl_seconds": 3600
    }' \
    https://invariantdb.com/capabilities/issue

The returned token can only access episodes / beliefs / summaries tagged with sessionId = 'conv-abc-123'. Other sessions become invisible — no app-layer tenancy code. Expires after TTL.

6. Vector + graph fusion — gds.semanticMatchExpand

Most RAG returns top-K vector hits and stops. This procedure starts there — top-K semantic seeds — then expands through the graph for N hops along configured edge types.

// Semantic search + 2-hop graph expansion in one call
CALL gds.semanticMatchExpand(
  'Event',                  // label
  'embedding',              // vector property
  $query_embedding,         // [f32; dim]
  2,                        // hops (BFS expansion depth)
  10                        // optional K seeds (default 10)
)
YIELD nodeId, hops, score
RETURN nodeId, hops, score
ORDER BY hops, score DESC;

hops is 0 for seed vector hits and 1..N for expansion-ring members. The score propagates from the seed so each result keeps its parent similarity. One Cypher call; no app-layer expansion loop.

7. Embedding model versioning — db.embeddingModelDrift

When you switch embedding models — text-embedding-3-largevoyage-3 → some-new-thing — old vectors are silently incompatible. InvariantDB stamps each vector with its model identifier and surfaces drift:

CALL db.embeddingModelDrift()
YIELD label, prop, lockedModel, currentDefault, driftKind, builtAt, vectorCount
RETURN label, prop, lockedModel, currentDefault, driftKind, vectorCount
ORDER BY builtAt DESC;

The cure is a re-embed migration. The signal is what you have to ask for elsewhere.

What to use when

You want to…Use
Log every step the agent tookdb.recordEpisode (1)
Compress old context into running summariesdb.consolidateSession (3)
Track what the agent believes and whydb.recordBelief + db.beliefLineage (2)
Make some memories matter moredb.applySalienceDecay + db.reinforceSalience (4)
Isolate one conversation from othersCapability tokens with session (5)
Replace your vector DB + RAG loopgds.semanticMatchExpand (6)
Detect a stale embedding pipelinedb.embeddingModelDrift (7)
Replay what an agent knew at time TMATCH ... AT VALID '<ts>' (universal — bitemporal) or {query, atTime} body field on /cypher (new 2026-06-27)
One-call ranked recall for an agent turnPOST /graphs/{name}/memory/recall (new 2026-06-27) — BM25 + graph-expand + salience rerank server-side, top-k snippets in one round-trip

New 2026-06-27: /memory/recall — one call replaces 3

Before today, an MCP integration that did "find what the agent should remember for this turn" orchestrated 3 cypher calls client-side: gds.fulltextSearch → graph-expand → salience rerank. That's 3 round-trips + your own ranking glue. The new endpoint does it server-side and returns top-k snippets in one call.

curl -H "Authorization: Bearer $INVARIANTDB_API_KEY" -H "content-type: application/json" \
     -X POST https://invariantdb.com/e/graphs/$GRAPH/memory/recall \
     -d '{"query":"what did the user ask about pricing yesterday","k":5,"hops":1,"salience_weight":0.3}'

Response shape: {snippets:[{id,str_id,score,labels,content,expanded_from}], total_candidates_scored, took_ms}. Default salience_weight=0 = pure BM25 (no salience infra needed). Cap is k≤50, hops≤3. See quickstart for the per-turn hook pattern.

New 2026-06-27: atTime body field on /cypher

The bitemporal storage was always sound — db.propertyHistory + MATCH ... AT VALID 'ts' have worked since v1. What was missing: an ad-hoc atTime body field on the HTTP endpoint so SDKs could time-travel a regular query without rewriting Cypher. Now it works:

curl -H "Authorization: Bearer $INVARIANTDB_API_KEY" -H "content-type: application/json" \
     -X POST https://invariantdb.com/e/graphs/$GRAPH/cypher \
     -d '{"query":"MATCH (e:Event) RETURN count(e)","atTime":"2025-01-01T00:00:00Z"}'

The engine parses the query, walks every MATCH clause, and broadcasts AT VALID 'atTime'. Rejected with HTTP 400 if the query already contains explicit AT VALID / AT RECORDED (silent-override prevention). For ad-hoc UIs / replays this is much cleaner than string-concatenating the AT VALID clause yourself.

End-to-end pattern: chat agent with replayable memory

API key setup: Create an API key at apikeys.html — keys start with gq_ and are bearer tokens you pass in the Authorization header.
from invariantdb import Client
db = Client("https://invariantdb.com", api_key="gq_...")
session = "conv-abc-123"

def on_user_message(text):
    # 1. Record the inbound observation
    db.cypher(graph="memory", query="""
        CALL db.recordEpisode($s, 'observation', $text, {})
    """, parameters={"s": session, "text": text})

    # 2. Recall: semantic + graph-expansion for what's relevant
    relevant = db.cypher(graph="memory", query="""
        CALL gds.semanticMatchExpand('Event', 'embedding', $emb, 2, 5)
        YIELD nodeId, hops, score
        MATCH (e:Event) WHERE id(e) = nodeId
        RETURN e.text AS text, hops, score
        ORDER BY hops, score DESC
    """, parameters={"emb": embed(text)}).rows

    # 3. Run your LLM with `relevant` in the prompt
    response = call_llm(text, relevant)

    # 4. Record the model's thought + decision
    db.cypher(graph="memory", query="""
        CALL db.recordEpisode($s, 'thought', $reasoning, {})
        CALL db.recordEpisode($s, 'decision', $response, {})
    """, parameters={"s": session, "reasoning": response.reasoning,
                     "response": response.text})

    # 5. If the response asserted a belief, record its derivation
    for belief in response.beliefs:
        db.cypher(graph="memory", query="""
            CALL db.recordBelief($agent, $key, $content, $evidence)
        """, parameters={"agent": "agent-deepthought",
                         "key": belief.key, "content": belief.content,
                         "evidence": belief.evidenceEventIds})

    return response

def replay(session, at_time):
    """Show the user what the agent knew at any point in the conversation."""
    return db.cypher(graph="memory", query="""
        CALL db.replayEpisodes($s, 1000)
        YIELD ts, kind, text WHERE ts <= datetime($at)
        RETURN ts, kind, text ORDER BY ts
    """, parameters={"s": session, "at": at_time}).rows

Every piece is engine-native. The application code never implements eviction, tenancy, replay, or lineage on its own.

Operational notes

Quickstart →  ·  Features →  ·  Regulated industries →  ·  Live demo →  ·  Walkthrough →