Long-term memory

A thread remembers one conversation. Long-term memory keeps facts about a person (a user, a customer, a patient) across all their conversations, in the same storage. Use it for preferences, standing instructions and lasting facts ("vegetarian", "prefers email", "allergic to penicillin"). For searching your own documents, use collections instead.

from criogenai import Agent, Memory, Storage

storage = Storage("sqlite://agent.db")
agent = Agent(
    "openai:gpt-5-mini",
    storage=storage,
    # One memory per user: users/<user_id>. Facts are recalled before
    # each model call; the model gets remember, forget and search tools.
    memory=Memory.by_state("user_id", "users"),
)

await agent.thread("monday").send("I'm vegetarian", user_id="42")
# A later conversation recalls it.
await agent.thread("friday").send("Dinner ideas?", user_id="42")

# Your app can read and write memory too.
await storage.remember("users/42", "allergy", "peanuts")
facts = await storage.search("users/42", "allergy")
await storage.forget_all("users/42")  # everything about one user
import { Agent, Embedder, Memory, Storage } from "criogenai";

const storage = new Storage("sqlite://agent.db");
const agent = new Agent({
  model: "openai:gpt-5-mini",
  storage,
  // One memory per user: users/<user_id>. Facts are recalled before each
  // model call; the model gets remember, forget and search tools.
  memory: Memory.byState("user_id", "users"),
});

await agent.thread("monday").send("I'm vegetarian", { state: { user_id: "42" } });
// A later conversation recalls it.
await agent.thread("friday").send("Dinner ideas?", { state: { user_id: "42" } });

// Your app can read and write memory too.
await storage.remember("users/42", "allergy", "peanuts");
const facts = await storage.search("users/42", "allergy");
await storage.forgetAll("users/42"); // everything about one user
use criogenai_agent::{Agent, AgentInput, Memory};
use criogenai_storage::{Namespace, Storage};

let storage = Storage::connect("sqlite://agent.db").await?;
let agent = Agent::builder(model)
    .storage(storage.clone())
    // One memory per user: users/<user_id>. Facts are recalled before
    // each model call; the model gets remember, forget and search tools.
    .memory(Memory::by_state("user_id", Namespace::parse("users")?))
    .build()?;

let monday = AgentInput::from("I'm vegetarian").with("user_id", "42");
agent.thread("monday").send(monday).await?;

// A later conversation recalls it.
let friday = AgentInput::from("Dinner ideas?").with("user_id", "42");
agent.thread("friday").send(friday).await?;

// Delete everything about one user.
let user = Namespace::parse("users/42")?;
storage.memory().delete_namespace(&user).await?;

Namespaces: whose facts these are#

Memory is organised like paths: users/42, customers/c-7, clinics/north/patients/p-19. Every fact lives under one namespace, and a run reads and writes exactly one:

PythonTypeScriptNamespace
Memory.by_state("user_id", "users")Memory.byState("user_id", "users")users/<user_id>, from the value you pass with each message
Memory.shared("team/support")Memory.shared("team/support")One namespace for every run

With by_state, pass the key with the message: send(text, user_id="42") (send(text, { state: { user_id: "42" } })).

  • A run without it fails with invalid_state rather than reading someone else's facts. Pass it with every agent.run.
  • On a thread, the value of the first message is kept for the thread's life: later messages may leave it out, and a different value fails with invalid_input.
  • Take the value from your authentication, never from what the user typed.

Recall: what the model sees#

Before each model call, up to recall facts (5 by default) are added to the request: first those matching the words of the latest user message (or its meaning, with an embedder), then the newest. They are marked as information, not instructions, and they are not stored in the transcript, so the thread stays the conversation and nothing else.

The memory tools#

The model also gets three tools:

ToolWhat the model can do
rememberSave a fact under a key (a new value replaces the old one)
forgetDelete a fact by key
search_memorySearch the namespace when recall did not bring what it needs

The model decides what to save. Say what is worth keeping in the agent's instructions ("save lasting preferences and allergies with remember; never save card numbers"). With the audit trail on, each memory write is recorded (with the key hashed, not in clear).

Options, as keyword arguments (Python) or an options object (TypeScript):

OptionDefaultWhat it does
recall5Facts added before each model call; 0 turns recall off
toolsonFalse / false removes the three tools (recall only)
ttl / ttlSecsnoneFacts the agent saves expire after this many seconds

Your app's own reads and writes#

The same facts are open to your code through Storage:

PythonTypeScriptWhat it does
remember(ns, key, value, ttl=None)remember(ns, key, value, { ttlSecs })Save (or replace) a fact; the value is any JSON
recall(ns, key)recall(ns, key)One fact, or None / null
search(ns, text, limit=10)search(ns, text, limit)Facts in the namespace and below, by keywords
forget_all(ns)forgetAll(ns)Delete everything in the namespace and below; returns how many

Use them to load facts from your own systems (an allergy list from the patient record), to show a person what is remembered about them, and to delete it on request.

By meaning#

With an embedding model, a question such as "Which antibiotics should I avoid?" finds a fact saying "Severe penicillin reaction", with no word in common. Vectors are stored (and encrypted) with the facts. If the embedding model fails, recall falls back to keywords. Without an embedder, search is by keywords.

from criogenai import Agent, Embedder, Memory

memory = Memory.by_state("patient", "patients").semantic(Embedder.openai("text-embedding-3-small"))
# Or keep everything on your own servers: Embedder.ollama("nomic-embed-text")
agent = Agent("openai:gpt-5-mini", memory=memory)
const meaning = Memory.byState("patient", "patients").semantic(Embedder.openai("text-embedding-3-small"));
// Or keep everything on your own servers: Embedder.ollama("nomic-embed-text")
const clinician = new Agent({ model: "openai:gpt-5-mini", memory: meaning });
use criogenai_agent::{Agent, Memory, put_embedded};
use criogenai_providers::ProviderEmbedder;
use criogenai_storage::Namespace;
use serde_json::json;

let embedder = ProviderEmbedder::new(Endpoint::openai(key), "text-embedding-3-small")?;
let memory =
    Memory::by_state("patient", Namespace::parse("patients")?).semantic(embedder.clone());
let agent = Agent::builder(model)
    .storage(storage.clone())
    .memory(memory)
    .build()?;

// Facts your app saves itself need a vector too.
let patient = Namespace::parse("patients/p1")?;
let fact = json!("Severe penicillin reaction");
put_embedded(
    storage.memory().as_ref(),
    &embedder,
    &patient,
    "allergy",
    fact,
    None,
)
.await?;
// "Which antibiotics should I avoid?" now finds the allergy.

Facts the agent saves (remember) get their vector automatically. In Python and TypeScript, facts your app saves itself (storage.remember) are found by keywords only for now; Rust has put_embedded for them.

Embedder.ollama(...) keeps the text on your own machines; hosted embedders (Embedder.openai, Embedder.gemini) send each fact and question to the provider.

Deleting a person#

forget_all("users/42") (forgetAll) removes every fact under the namespace. A person's conversations are threads: delete those too (thread.delete()) to erase everything. With retention, old threads can be deleted on a schedule; memory facts expire with a ttl.

Pitfalls#

  • Namespaces are not a security boundary by themselves. They separate facts as long as the state key comes from your authentication. If a user can choose their user_id, they can read someone else's memory.
  • Do not store what you must not keep. Anything the model saves is stored. Tell it what not to save, and use redact or a hook for identifiers that must never reach storage or a provider.
  • Recall costs tokens on every model call. Keep facts short, and lower recall if they are long.

Next#