Decisional memory for AI agents, in TypeScript.
One entry point — store() — and one exit point — recall(). In between
sits a System-1 router that decides, in a fast typed pass, where to write and
where to read across three internal stores:
| Store | Backend | Purpose |
|---|---|---|
vector |
Qdrant (remote REST) | Embeddings for semantic memory / lookup |
graph |
Neo4j | Knowledge graph, populated by LLM entity/relation extraction |
index |
Qdrant (separate collection) | File/folder index with short LLM descriptions — local, S3/MinIO, Nextcloud/WebDAV |
The agent only ever sees a few methods — store(), recall(), forget(). The
router routes; the stores execute. Every subsystem call degrades gracefully: one
store down yields a degraded result, never an exception.
Package: @requrv/remem
bun add @requrv/remem # or: npm i @requrv/rememRequires a reachable Qdrant and Neo4j (local, Podman/Docker, or remote).
import { ReMem } from '@requrv/remem'
const mem = new ReMem({
vector: { url: 'http://localhost:6333' },
graph: { uri: 'bolt://localhost:7687', username: 'neo4j', password: 'changeit' },
embedder: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY! },
chat: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY! },
})
await mem.connect()
// One entry point — the router decides where (vector, graph, …).
const { id } = await mem.store({ content: 'Marco collabora con Lucia sul progetto Remem' })
// One exit point — the router picks the stores; each answers or degrades.
const ctx = await mem.recall('chi collabora con Marco?')
console.log(ctx.toPrompt()) // compact, context-ready block for your agent
// Forget a memory by its id (see `ctx.memories[].id`) — vector + graph.
await mem.forget(id)
await mem.close()loadConfigFromEnv() builds the config from .env — it picks OpenAI when
OPENAI_API_KEY is set, otherwise local Ollama:
import { createReMem, loadConfigFromEnv } from '@requrv/remem'
const mem = createReMem(loadConfigFromEnv())Key variables (see .env.example): QDRANT_URL, NEO4J_URI/NEO4J_USER/NEO4J_PASSWORD,
OPENAI_API_KEY / OPENAI_BASE_URL (any OpenAI-compatible endpoint),
OLLAMA_BASE_URL, optional ROUTER, TENANT, EMBEDDING_DIMENSIONS.
const mem = new ReMem({
vector: { url: 'http://localhost:6333' },
graph: { uri: 'bolt://localhost:7687', username: 'neo4j', password: 'changeit' },
embedder: { provider: 'ollama', model: 'nomic-embed-text' },
chat: { provider: 'ollama', model: 'llama3.1' },
})routeWrite picks a subset of { vector, graph } (the index is never a write
target); routeRead picks a subset of { vector, graph, index }.
heuristic(default) — rule-based, fast, free, deterministic: relational content goes to the graph, file/path-like queries go to the index, vector always.llm— yourChatModelanswers a routing question; any failure falls back to the heuristic, it never throws.laya— the same decision is delegated to an external Laya runtime over HTTP (laya.endpoint,threshold,timeoutMs); same fail-safe fallback.
Choose it in the config (router: 'heuristic' | 'llm' | 'laya'), or inject your
own implementation:
const mem = new ReMem({ ...config, router: 'llm' })
const mem = new ReMem(config, { router: myRouter }) // custom RouterRouter is a small async interface (routeWrite, routeRead → Promise<RouteDecision>).
Memories live in logical collections (not physical ones). Tag at write, filter at read — a multi-collection query is a single Qdrant OR filter:
await mem.store({ content: '…', collection: 'project-a' }) // default: 'default'
await mem.recall('query', { collections: 'project-a' }) // one
await mem.recall('query', { collections: ['a', 'b'] }) // many (OR)
await mem.recall('query') // all collectionsforget(memoryId) deletes a single memory from the subsystems it landed in.
The vector point is removed by id; the graph is cleaned via provenance
— every entity/relation written by store() is tagged with the memory id that
produced it, so forget removes exactly the graph data that memory was the
last source of (data still referenced by other memories is kept). It is
idempotent (forgetting an unknown id is a no-op) and degrades like
everything else: one store down yields a degraded result, never a throw.
// id comes from store() or from any recall: ctx.memories[].id
await mem.forget(id)
await mem.forget(id, { only: ['vector'] }) // skip the graph cleanup
// → { deletedFrom: ['vector','graph'], graph: { entities: 2, relations: 1 } }recall() renders each memory with its id — toPrompt() lines look like
- (0.87) [<uuid>] snippet… — so an agent can copy the id straight into
forget() (or the mem_forget MCP tool).
Note: provenance is recorded at write time. Memories stored before you upgrade will have no graph provenance, so forgetting them removes the vector point but leaves pre-existing graph entities/relations in place.
Set tenant in the config — one ReMem instance = one tenant — and all data
(memories, index, graph) is scoped to it, enforced at query time (no per-call
tenant argument, no way to address another tenant):
const tenantA = new ReMem({ ...config, tenant: 'acme' })
const tenantB = new ReMem({ ...config, tenant: 'globex' })
// same Qdrant + Neo4j, zero cross-tenant visibilityIsolation is payload-based (vector/index) plus entity-id namespacing (graph). Note: enabling a tenant on pre-existing data makes unscoped points invisible — treat it as a fresh namespace.
The index turns file/folder trees into a compact map your agent can read: each
entry gets a short LLM description (dirs from their children's names, files from a
text preview). Update is explicit only — store() never touches the index:
const mem = new ReMem({
...config,
index: {
describeFiles: true, // default: dirs only
sources: [
{ kind: 'local', id: 'repo', root: '/path/to/project' },
{ kind: 's3', id: 'docs', bucket: 'my-bucket', prefix: 'docs/', region: 'eu-west-1' },
{ kind: 'webdav', id: 'nc', url: 'https://cloud.example.com/remote.php/dav/files/u', user: 'u', password: 'p', prefix: 'Docs' },
],
},
})
const map = await mem.index() // scan + diff + describe (incremental, cached)
await mem.index({ only: ['repo'] }) // or a single sourcerecall() with an index-relevant query returns the matching entries; toPrompt()
renders the map. Rescans are incremental (ETag/mtime|size aware) with per-source
orphan cleanup — unchanged files are not re-described.
new ReMem(config: ReMemConfig, opts?: { router?: Router; logger?: Logger })
await mem.connect() // idempotent bootstrap (collections + schema)
await mem.store({ content, source?, collection?, metadata? }) // → { id, writtenTo }
await mem.recall(query, opts?) // → Recall
// opts: { limit?, only?: Subsystem[], collections?: string | string[] }
await mem.forget(id, opts?) // → ForgetResult (opts: { only?: Subsystem[] })
await mem.index(opts?) // → IndexMap (opts: { only?: sourceId[] })
await mem.close()Recall — { memories, graph?, index?, sources, degraded?, toPrompt() }
(each memory in memories carries its id):
toPrompt() renders a compact block (## Memory / ## Knowledge / ## Index)
ready to inject into your agent's context; degrades are annotated, empty sections
read (no results).
Also exported: createReMem, loadConfigFromEnv, ReMemConfigSchema,
HeuristicRouter, error hierarchy (ReMemError, ConfigError, ConnectionError, …),
noopLogger/consoleLogger, and the domain types (Subsystem, StoreInput,
RecallOptions, Recall, Router, RouteDecision, …).
The default router is free (no LLM calls). LLM usage comes only from:
knowledge-graph extraction on store(), index descriptions on index(), and — if
you enable it — the llm router.
ReMem ships two MCP servers exposing five tools — mem_remember, mem_recall,
mem_forget, mem_index, mem_status — so an agent decides by itself when
to remember, when to retrieve, and when to forget. mem_forget takes a memory
UUID (visible in every mem_recall result line) and deletes it from the vector
store and the knowledge graph; in enterprise mode scope: "company" requires
the admin role, like the other writes.
The agent spawns the server; one process = one tenant (TENANT env, optional).
Same env config as the library (.env.example). OpenCode
(opencode.json):
Claude Code:
claude mcp add remem --env QDRANT_URL=http://localhost:6333 --env OPENAI_API_KEY=… \
-- npx -y -p @requrv/remem remem-mcpOne service, one API key per user. Each recall fuses personal memory with
company memory (labeled sections); writes go to the user's own tenant, and
scope: "company" requires the admin role.
cd deploy && cp .env.example .env # fill OPENAI_API_KEY + MCP_USERS (one entry per user)
docker compose -f compose.yaml up -dAgents then point at http://<host>:3100/mcp with their bearer key
(Authorization: Bearer <key>). GET /healthz is the liveness probe.
The HTTP server requires Bun (included in the Docker image; for manual
runs: bun remem-mcp-http).
Single machine? The repo-root docker-compose.yaml runs this same
remem-mcp server together with the backends in one command — see
Self-hosting.
One command starts the whole stack — Qdrant + Neo4j + the remem-mcp HTTP
server — with persistent volumes:
cp .env.example .env # fill OPENAI_API_KEY; edit MCP_USERS for >1 user
docker compose up -d- LLM: an OpenAI-compatible key (
OPENAI_API_KEY, default base URL is AI Hive) — or 100% local with Ollama (EMBEDDER_PROVIDER=ollama,CHAT_PROVIDER=ollama,OLLAMA_BASE_URL=http://host.docker.internal:11434): no content leaves your network. - Connect: agents point at
http://localhost:3100/mcpwithAuthorization: Bearer <key>(the key is inMCP_USERS,rk-localby default);GET /healthzis the liveness probe. - Teams: for multi-user deployments on a shared host, see the enterprise
stack in
deploy/— same services, team-orientedMCP_USERS. - Library: wire ReMem into your own agent (Quick start above) — it talks
to the same Qdrant/Neo4j, also reachable on
localhostwhile this stack runs.
For company deployments (hosted multi-tenant service, production use in commercial products, support): ReMem is dual-licensed (GPL-3.0 only or ReQurv Commercial License) — contact licensing@requrv.io.
bun install
bun run infra:up # backends only: Qdrant (6333) + Neo4j (7474/7687)
cp .env.example .env # then fill in your keys
bun run typecheck
bun test # unit + integration (integration self-skips without infra)
bun run build # ESM + CJS + .d.ts → dist/
bun run dev # examples/basic.tsExamples in examples/: basic.ts, recall-index.ts, index-remote.ts,
custom-router.ts.
Contributions are welcome. See CONTRIBUTING.md for development setup, code conventions and the architecture.
Dual-licensed: GPL-3.0-only or the ReQurv Commercial License (production/ commercial use — contact licensing@requrv.io). See LICENSE and LICENSE-COMMERCIAL.md.

{ "mcp": { "remem": { "type": "local", "command": ["npx", "-y", "-p", "@requrv/remem", "remem-mcp"], "environment": { "QDRANT_URL": "http://localhost:6333", "NEO4J_URI": "bolt://localhost:7687", "NEO4J_USER": "neo4j", "NEO4J_PASSWORD": "changeit", "OPENAI_API_KEY": "sk-…" } } } }