RememberStackremember.dev/docs

Retrieval: operations, search, graph and SQL

An agent needs different things from memory at different moments: who a name refers to, what is currently true about a project, what exactly a source said, how two people are connected. One search box that returns "similar text" answers none of these well. It returns a pile of passages and leaves the model to sort fact from rumour and old from new.

RememberStack gives you three layers to read from, from most guided to most open:

  1. Four assured operations: fixed, documented reads that cover most agent context.
  2. Retrieval primitives: search, lookup, graph traversal, hydration and neighbouring passages, for when you need a specific shape.
  3. SQL queries over the query space, for questions no fixed read anticipates.

None of them calls a language model. The operations and primitives return the same self-describing envelope; SQL queries return a table with its own account of limits and drops.

Ways to reach the memory

The same reads are available from four places. Pick by who is asking:

SurfaceUse it forWhat it reaches
Python client (import remember)Your own code: sync jobs, pipelines, an agent you build.Everything: ingest, readiness, the four operations, every primitive, SQL queries and saved queries, with typed results.
CLI (remember)A terminal or a shell script: one-off questions, checking what the memory holds, trying a SQL query.Ingest, the four operations (remember query, remember operations run), SQL and saved queries, adjacent chunks. Prints JSON.
MCP (remember mcp)A coding agent such as Claude Code or Codex.Ingest, readiness, the four operations and the seven SQL query tools. No primitives.
HTTP APIAny other language or runtime.Every route; the other three are built on it.

The four assured operations

An assured operation is a read whose behaviour, parameters and result contract are fixed by RememberStack and registered in every deployment. They are the default tools for an agent; the remember MCP server exposes them as tools. GET /operations lists them with their full schemas.

OperationAnswersReturns
resolve_entity"Who or what is this name?"Ranked entity candidates, never a silent pick.
facts_context"What does memory hold true about this?"Adjudicated facts (relations and observations) under a time scope, with sampled evidence.
claims_and_sources_context"What exactly did the sources say?"Current claims and matching source passages.
combined_context"Give me both."Claims-and-sources and facts, side by side, in ContextBundle/v2.

When to use each

Start with resolve_entity whenever the question names someone or something. It turns "Ravi" into an entity_id, or tells you there are two Ravis, or that there is none. Pass the chosen IDs to the next call as entity_ids. See Entities.

Use facts_context for what is true. Current state, who owns what, what changed, what was true on a date. Facts are de-duplicated, dated and counted across sources, so ten notes saying the same thing come back as one fact with evidence_count 10. Arguments:

ArgumentDefaultLimits
queryrequired1 to 8,192 characters
entity_idsnone1 to 19 IDs
k151 to 30 facts
evidence_per_fact31 to 5 claims
hops11 or 2
predicatenoneone predicate name
time{"mode": "current"}current, at, overlap, history; see Time

With entity_ids and a current or at time scope, facts_context first expands the graph hops steps out from those entities, then searches fact text inside that neighbourhood. That is how "what is Ravi working on?" finds the facts about Ravi's projects, not only facts with Ravi in the text.

Use claims_and_sources_context for what was said. Exact wording, quotes, tone, who said what and when, or anything the fact layer did not capture. It returns current testimony only. Arguments: query, optional entity_ids (up to 20), k (default 50, up to 100) and candidate_k (default 200, up to 400) nominations per search channel.

Use combined_context when you want both and do not want to make two calls. It runs the two operations independently and returns them side by side, never blended. It takes query, entity_ids, hops, predicate and time.

import remember
 
with remember.Client() as memory:
    who = memory.resolve_entity("Ravi")
    ravi = who.entities[0].entity_id if len(who.entities) == 1 else None
 
    facts = memory.facts_context(
        "what is Ravi working on", entity_ids=[ravi] if ravi else None
    )
    said = memory.claims_and_sources_context("Ravi cutover date")
    both = memory.combined_context("billing migration cutover")

Note

The Python client's claims_and_sources_context sends only query. To pass entity_ids, k or candidate_k, call memory.run_operation(name="claims_and_sources_context", arguments={...}).

A good default order for an agent: resolve the names, ask facts_context, and fall back to claims_and_sources_context for exact wording or for anything the facts do not cover. See Give an agent context.

Retrieval primitives

Primitives are the building blocks the operations are made of, exposed for when you need one exact shape. They are available over HTTP and in the Python client. They are not assured operations and are not separate MCP tools.

PrimitiveClient methodWhat it returns
Search claimssearch_claims(query=, k=10, channel="semantic")Claims, from one search channel (semantic or bm25), k up to 400.
Search chunkssearch_chunks(query=, k=10, channel="semantic")Source passages, likewise.
Adjacent chunksadjacent_chunks(chunk_id=, window=1)The passages either side of a chunk in document order, window 1 or 2. Use it when an answer is cut off at a chunk boundary.
Resolveresolve(name=, context_entity_ids=())Like resolve_entity, optionally reordered by up to 8 entities already in focus.
Lookup relationslookup_relations(subject_entity_id=, predicate=, object_entity_id=, valid_at=, k=50)Relations matching a pattern, current or at one instant, at most k.
Lookup observationslookup_observations(entity_id=, property_query=, k=10)One entity's observations, optionally by property text.
Graph neighbourhoodgraph_neighborhood(entity_id=, hops=2, ...)The entities and relations within hops (up to 4), capped at limit (up to 500) with a continuation.
Graph pathgraph_path(from_entity_id=, to_entity_id=, max_hops=4)Shortest connections between two entities (up to 6 hops).
Citation pathgraph_citation_path(from_doc_id=, to_doc_id=, max_hops=6)How one document leads to another through citations.
Hydrate relationhydrate_relation(relation_id=)A relation with all its claims and source documents.
Relation transcripttranscript_relation(relation_id=)Why the memory believes a relation: its decision history.

The graph reads take valid_at and believed_at together for a two-clock read (see Time). A graph path is returned whole or not at all: if any edge on it no longer holds, the path is dropped, because a path with a gap is a false statement about a connection.

See Search, Graph and Resolve, lookup, hydrate, transcript.

SQL queries over the query space

For questions the operations and primitives do not cover ("which documents mention both Dana and the billing migration, newest first?"), you can run SQL queries over the query space, memory_v1: a set of prepared, read-only views and functions over claims, facts, entities, documents, chunks, evidence and history.

It is not open access to the database. Every statement is parsed and validated against the query space before it runs, and anything outside it is rejected. Results are bounded and read-only.

with remember.Client() as memory:
    result = memory.open_query(
        "SELECT claim_id, claim_text, asserted_at FROM claims_live"
        " ORDER BY asserted_at DESC NULLS LAST LIMIT 20"
    )
    for claim_id, claim_text, asserted_at in result.rows:
        print(asserted_at, claim_text)

Each row is a list of values in the order of result.columns.

You can explore the query space (describe_query_space, search_query_space), check a statement without running it (explain_query), and store reviewed statements as saved queries that agents run by name with parameters. See Explore memory with SQL, Saved queries and Query space.

How hybrid retrieval works

A question goes to meaning, keyword and graph search, which nominate candidates; PostgreSQL confirms each against the live state in one snapshot and drops anything withdrawn or superseded; the result lists facts, evidence, time windows and contradictions, and how many candidates were dropped.

Underneath, every read that searches follows the same pattern: nominate, fuse, confirm.

Nominate. Independent channels each propose candidates:

  • Semantic: vector similarity search with pgvector, over embeddings of chunks, claims, facts and entity profiles. The query text is embedded with the deployment's embedding model.
  • BM25: keyword ranking over chunks and claims, with PostgreSQL's pg_textsearch. It catches exact names, codes and numbers that vectors blur.
  • Graph: for facts_context with entity anchors, the live PostgreSQL graph expands to the anchors' neighbours, and fact search runs inside that scope.

Fuse. Channel rankings are merged with reciprocal rank fusion (RRF): each candidate scores the sum of 1 / (60 + rank) over the channels that found it. Something that ranks well in both semantic and keyword search rises; the per-channel scores stay visible in the result's ranking.

Confirm (hydrate). Search indexes are a way to find candidates; they are never the authority. Every candidate is re-read from the live PostgreSQL tables in one consistent snapshot before it is returned. A candidate that no longer holds (a claim that is no longer current testimony, a fact that was retracted or falls outside the time scope, an entity that was merged away) is dropped, and the envelope counts it in dropped_by_hydration. Stale index entries can cost recall; they cannot put a false answer in front of your agent.

In claims_and_sources_context this runs twice, once for claims and once for passages, each with its own semantic and BM25 nomination (up to candidate_k each), fusion and confirmation. Claims that say the same thing from several documents are grouped, with a corroboration count. The two lists come back in one envelope, kept apart: evidence for claims, chunks for passages.

Where to go next