Primitives
Memory
Typed long-term memory with hybrid recall, owners, supersede and duplicate consolidation.
Typed long-term memory for agents. A memory is a short piece of text with a kind, tags, an optional owner and metadata. Recall combines full-text and vector search, and superseding keeps old facts out of the way without deleting their history.
A memory store is a primitive inside a group, with an immutable id and a name. A group can hold as many as you need: notes for the agent's own knowledge, user-prefs per user, a shared playbook.
const notes = await group.memory.create('notes'); // 409 already_exists if taken; notes.id is its id
const prefs = group.memory('user-prefs'); // by name in the group, resolved on first use
const same = p.memory(notes.id); // the same store, by idnotes = await group.memory.create("notes")From the CLI: primitives primitive create notes --type memory --group agent-42 (prints the id).
Memory kinds
| Kind | Holds | Example |
|---|---|---|
episodic | Things that happened | "Deployed v2.3 to staging; smoke tests failed on auth." |
semantic | Facts and preferences | "User prefers pnpm over npm." |
procedural | How to do something | "To release: bump version, tag, run pnpm publish." |
Kinds let an agent ask for the right kind of knowledge. A planner might recall only procedural memories, and a chat agent only semantic ones.
Remembering
const memory = await notes.remember({
kind: 'semantic',
text: 'User prefers pnpm over npm',
tags: ['preferences'],
owner: 'user-17',
metadata: { source: 'chat:2024-10-01' },
});memory = await notes.remember(
kind="semantic",
text="User prefers pnpm over npm",
tags=["preferences"],
)| Field | Description |
|---|---|
kind | episodic, semantic or procedural. Required. |
text | The memory itself, up to 32 KiB. Required. |
tags | Up to 32 strings of up to 64 characters, for filtering. |
owner | Optional owner, such as a user or agent ID. Recall and list can filter on it. |
metadata | Optional JSON object, such as provenance. |
remember returns the stored memory: { id, kind, text, tags, owner, metadata, superseded_by, created_at, updated_at }.
Recalling
const hits = await notes.recall('package manager?', {
kinds: ['semantic', 'episodic'],
tags: ['preferences'],
limit: 5,
});
// [{ memory: { id, kind, text, … }, score, bm25_rank, vector_rank }]hits = await notes.recall("package manager?", kinds=["semantic", "episodic"], limit=5)
print(hits[0].memory.text)Recall is hybrid. It ranks memories with BM25 over their text and tags and, when embeddings are configured, with cosine similarity over their vectors, then fuses the two rankings with Reciprocal Rank Fusion. bm25_rank and vector_rank show where each hit placed in each list.
| Option | Description |
|---|---|
kinds | Only these kinds. Default all. |
tags | Every tag must be present. |
owner | Only this owner's memories. |
limit | Default 10, max 50. |
includeSuperseded | Also return superseded memories. Default false. |
A query with no words returns the most recent matching memories.
Embeddings
Embeddings are optional and configured on the engine, not per call. Point it at any OpenAI-compatible /embeddings API before starting it:
export PRIMITIVES_EMBEDDINGS_URL="https://api.openai.com/v1"
export PRIMITIVES_EMBEDDINGS_KEY="sk-…"
export PRIMITIVES_EMBEDDINGS_MODEL="text-embedding-3-small" # the default
primitives devWith embeddings, remember and supersede store a vector for each memory and recall embeds the query. Without them, recall is BM25 full-text search. If the provider fails, memories are stored without a vector and recall falls back to full text: requests never fail because of embeddings.
Listing and reading
const { data, next_cursor } = await notes.list({ kind: 'semantic', tag: 'preferences', limit: 50 });
const one = await notes.get(id);list returns newest first, includes superseded memories, and pages with cursor.
Superseding and forgetting
To correct a fact, supersede it. The new memory replaces the old one in recall, and the old one stays readable with superseded_by set:
await notes.supersede(oldId, { text: 'User now prefers bun' }); // kind, tags, metadata and owner carry over unless given
await notes.forget(id); // delete one memorySuperseding a memory that's already superseded fails with 409 conflict.
Consolidation
consolidate merges duplicates among active memories of the same kind and owner. It treats two memories as duplicates when their normalized text is identical, or when both have embeddings with cosine similarity at or above threshold (default 0.95). The newest memory of each cluster is kept, and the others are superseded by it:
const { merged, superseded } = await notes.consolidate({ threshold: 0.9 });Consolidation never deletes anything, and it runs only when you call it. LLM-based consolidation, such as summarizing related episodes or resolving contradictions, is coming soon.
Snapshots, forks and pause
Each memory store has its own lifecycle. notes.fork({ group: 'agent-43' }) copies it into a new store with a new id, so a forked agent starts out knowing everything its parent knew, and anything it learns stays in the copy. notes.snapshot('before-import') and notes.restore('before-import') roll back this store only, and notes.pause() stops every read and write with 423 paused until notes.resume().
const child = await notes.fork({ group: 'agent-43', name: 'notes' }); // 404 not_found if agent-43 doesn't existMCP tools
| Tool | Read-only | Arguments |
|---|---|---|
memory_remember | no | primitive?, kind, text, tags?, metadata? |
memory_recall | yes | primitive?, query, kinds?, tags?, limit? |
memory_forget | no | primitive?; action: delete (default), supersede or consolidate; id?, text?, tags? |
primitive is the memory store to use: its id or its name from primitive_list. It can be left out when the group has exactly one.