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 keeps a hash-chained history of node and relationship writes. And it natively understands "this belief was derived from these episodes."

Vector search is still here — built in — 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: a string; a map or list is refused
)
YIELD eventId, ts;

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

Events of every session are stored in the same graph. A credential that can read the graph can read every session.

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 summary. An optional third argument, sinceTs
// (nanoseconds since the Unix epoch), limits the summary to events
// from that time on. Events are never deleted.
CALL db.consolidateSession(
  'conv-abc-123',
  'Q3 forecast conversation: user asked about revenue, agent walked forecast + risks.'
)
YIELD summaryId, eventCount, firstTs, lastTs;

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

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, oldSalience, newSalience;

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

// Recall by salience
MATCH (:Session {id: 'conv-abc-123'})-[:HAD_EVENT]->(e:Event)
RETURN e ORDER BY e.salience DESC LIMIT 20;

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

5. Capability tokens

An admin can issue a short-lived token bound to one graph, a principal and a scope list:

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

Scopes include read, write and admin. ttl_seconds can be at most 300 by default; a longer value is refused. Send the token as Authorization: Capability <token>. A token issued this way is not limited to one session: it can read every session in the graph. To keep conversations apart, use a separate graph for each and bind each token or API key to its graph.

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,         // query vector
  10,                       // k: number of seed matches
  2                         // hops: expansion depth
)
YIELD nodeId, distance, score
RETURN nodeId, distance, score
ORDER BY distance, score DESC;

distance is 0 for seed vector hits and 1..N for nodes reached by expansion. 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-large → voyage-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)
Give an agent short-lived access to one graphCapability tokens (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}. The response also has mode, the search mode that ran. With the default salience_weight=0, salience does not change the ranking. Defaults: label Memory, property content; to search events written by db.recordEpisode, pass "label": "Event", "property": "text". 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"}'

Each top-level MATCH in the query is evaluated as of atTime. The request is refused with HTTP 400 if the query already contains a time clause such as AT VALID or AT RECORDED, or has no top-level MATCH. 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.embedText($text) YIELD embedding
        CALL gds.semanticMatchExpand('Event', '_embedding_v1', embedding, 5, 2)
        YIELD nodeId, distance, score
        MATCH (e:Event) WHERE id(e) = nodeId
        RETURN e.text AS text, distance, score
        ORDER BY distance, score DESC
    """, parameters={"text": 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) YIELD eventId AS e1
        CALL db.recordEpisode($s, 'decision', $response) YIELD eventId AS e2
        RETURN e1, e2
    """, 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($key, $content, $confidence, $evidence)
        """, parameters={"key": belief.key, "content": belief.content,
                         "confidence": belief.confidence,
                         "evidence": belief.evidenceEventIds})

    return response

def replay(session, at_ns):
    """Events recorded up to a time. at_ns is nanoseconds since the Unix epoch."""
    return db.cypher(graph="memory", query="""
        CALL db.replayEpisodes($s, 1000)
        YIELD ts, kind, text WHERE ts <= $at
        RETURN ts, kind, text ORDER BY ts
    """, parameters={"s": session, "at": at_ns}).rows

The procedures above record episodes, beliefs and lineage and replay a session. The example embeds text with the server’s embedding model, so it needs one configured.

Operational notes

Quickstart →  ·  Features →  ·  Regulated industries →  ·  Demo →  ·  Walkthrough →