A memory substrate for AI agents. Memoir stores what an agent is told, derives durable facts from it, and serves them back — ranked, correctable, and scoped per tenant.
You write conversation turns; a background worker runs LLM extraction over them to produce semantic facts. Recall reads the fact layer; the raw turns stay as the audit trail and the source those facts are re-derived from. Postgres is the source of truth, Qdrant is the vector index, and Memoir owns the schema, the embedding model, and the write-behind queue that keeps the two consistent.
Memoir ships as a library or a service. Same engine, different boundary.
memoir-core— an embeddable Rust library.cargo add polypixel-memoir-core, bring your own Postgres + Qdrant, and call it in-process. No auth: the host process is the trust boundary. This is everything — memory, embedding, extraction, the worker.memoir-service— a gRPC adapter over the library, shipped as a Docker image. Adds local auth (JWT + API keys) and exposes the surface over the wire. A thin wrapper: every handler unwraps the request, calls the library, wraps the response. Network clients use the generated SDKs (polypixel-memoir-sdkon crates.io,@polypixel/memoir-sdkon npm).
Pick the library if you're writing a Rust agent. Pick the service if you want a memory backend other processes or languages talk to.
- Scoped memory. Every write and read is partitioned by an
(agent, org, user)tuple. One tenant never sees another's memories. - Episodic capture, semantic recall. Writes are raw turns; the worker extracts facts from them asynchronously. You query the facts.
- Vector search and ranked query.
searchis raw nearest-neighbor.queryre-ranks by a tunable blend of cosine, confidence, recency, and category, and returns prompt-shaped context. - Temporality. Facts carry an event-time distinct from when Memoir was told. Read the chronological
timeline, orrecall_as_ofa past instant to get the state of knowledge as it stood then. - Categorization and confidence. Extracted facts carry a confidence score and an opt-in NLI category label, both usable as ranking signals or hard filters.
- Correction by teaching. Semantic facts are never hand-edited. A wrong fact is corrected with
feedback— Memoir re-derives from the source. Edit the source itself and the derived facts cascade. Retirements are tracked asrejected(a wrong extraction) orstale(the source changed);extraction_statsreports accuracy per model. - Durable by construction. The write-behind queue is Postgres-backed and survives crashes. Failed jobs surface to an admin view;
reconcileretries them and sweeps orphaned vectors. - Pluggable models. Extraction runs against Ollama, OpenAI, or Anthropic via
LlmConfig. The categorizer is any zero-shot NLI model viaNliConfig. Both are optional — leave them out and those stages simply skip.
[dependencies]
polypixel-memoir-core = "0.1"
qdrant-client = "1.18"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }You need a Postgres database (with pgvector) and a Qdrant instance. docker compose --profile dbs up -d brings both up locally.
use memoir_core::client::Client;
use memoir_core::memory::Scope;
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let qdrant = qdrant_client::Qdrant::from_url("http://localhost:6334").build()?;
let client = Client::builder()
.database_url("postgres://postgres:postgres@localhost:54321/memoir")
.qdrant(qdrant)
.build()
.await?;
client.migrate().await?;
let worker = client.spawn_worker().start().await?;
let scope = Scope {
agent_id: "my-agent".into(),
org_id: "my-org".into(),
user_id: "user-42".into(),
};
client.remember("the user prefers dark roast coffee", scope.clone()).await?;
let hits = client.search("coffee preference", scope).limit(5).await?;
for m in hits.list() {
println!("{}", m.content);
}
worker.shutdown().await;
Ok(())
}Extraction and categorization are opt-in on the builder: .extraction_llm(LlmConfig::ollama(url, model)) turns episodic turns into semantic facts; .categorize_model(NliConfig::default()) labels them. Without them, Memoir is a scoped vector store. See examples/library-quickstart.rs for the full lifecycle and packages/memoir-core/README.md for the API surface.
Run the service when you want a memory backend other processes talk to over gRPC.
docker compose --profile dbs up -d # Postgres + Qdrant
docker run --rm -p 5153:5153 \
-e DATABASE_URL=postgres://postgres:[email protected]:54321/memoir_service \
-e QDRANT_URL=http://host.docker.internal:6334 \
-e MEMOIR_JWT_SECRET=$(openssl rand -base64 32) \
ghcr.io/mylesberueda/memoir/memoir-service:latestMigrations run at startup. The service exposes three gRPC services on port 5153 — MemoryService (remember, search, query, recall, timeline, recall-as-of, edit, feedback, forget), AdminService (failed-job triage, reconcile, extraction stats), and AuthService (bootstrap, login, users, API keys). Auth is local: a bootstrap token creates the first admin, then JWTs and mk.* API keys gate every RPC.
Configuration is environment-driven — DATABASE_URL, QDRANT_URL, MEMOIR_JWT_SECRET are required; SERVICE_SCHEMA/CORE_SCHEMA isolate the auth and memory tables; MEMOIR_EXTRACTION_* wires the extraction LLM. See apps/memoir-service/.env.example.
Releases are tag-driven from main. Pushing a v* tag publishes polypixel-memoir-core and polypixel-memoir-sdk to crates.io, @polypixel/memoir-sdk to npm, and the service image to GHCR. Bump the version in the three manifests to match the tag first; see RELEASE.md for the cutoff procedure and .tasks/1000-release-operator-runbook.md for the rationale.
Fork, branch off dev, open a PR against it. Work lands on dev (staging) and promotes to main (production); CI commits image digests to parallel deploy/* branches that ArgoCD watches, so branch history stays clean. Read infrastructure/IAC_RULES.md before any infrastructure change and infrastructure/DEPLOY.md for the deploy model and rollback runbook.
The repo is maintained with Jujutsu, but it's a git repo underneath — use whatever you like. If you do use jj, it has its own remote/bookmark workflow for pushing branches.
Licensed under either of Apache License 2.0 or MIT at your option.