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 id
notes = await group.memory.create("notes")

From the CLI: primitives primitive create notes --type memory --group agent-42 (prints the id).

Memory kinds

KindHoldsExample
episodicThings that happened"Deployed v2.3 to staging; smoke tests failed on auth."
semanticFacts and preferences"User prefers pnpm over npm."
proceduralHow 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

memory.ts
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.py
memory = await notes.remember(
    kind="semantic",
    text="User prefers pnpm over npm",
    tags=["preferences"],
)
FieldDescription
kindepisodic, semantic or procedural. Required.
textThe memory itself, up to 32 KiB. Required.
tagsUp to 32 strings of up to 64 characters, for filtering.
ownerOptional owner, such as a user or agent ID. Recall and list can filter on it.
metadataOptional 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.

OptionDescription
kindsOnly these kinds. Default all.
tagsEvery tag must be present.
ownerOnly this owner's memories.
limitDefault 10, max 50.
includeSupersededAlso 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 dev

With 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 memory

Superseding 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 exist

MCP tools

ToolRead-onlyArguments
memory_remembernoprimitive?, kind, text, tags?, metadata?
memory_recallyesprimitive?, query, kinds?, tags?, limit?
memory_forgetnoprimitive?; 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.