Skip to content
ONTO

Typed Memory vs Vector Embeddings for AI Agents

Why storing 'Alice is in UTC' as an embedding and retrieving it later by cosine similarity is the wrong primitive — and what to use instead.

· 10 min read · ONTO team

Pop quiz. The user says “I’m in Lyon for the week.” Your agent has to remember this. How do you store it?

Option A. Append the message to the conversation buffer.

Option B. Embed the message and put it in a vector store.

Option C. Extract Alice.current_location = "Lyon" with decay=session_scoped, confidence=0.9, provenance=msg_42, consent_scopes=["memory:write"] and store it as a typed assertion.

If your answer is A or B, your agent will be asking “where are you again?” by Wednesday.

What vectors are good at, and what they aren’t

Vector embeddings encode a meaning-vector for text. Similar meanings land near each other in the space. This is wonderful for one task: finding things that are like your query. “Find me notes similar to this email.” “Find me docs that mention this concept.” Vectors crush this.

Here’s what they’re not good at: answering questions about state. “What’s the user’s timezone right now?” is not a similarity query. It is a state lookup. Asking “what’s the most similar prior message to ‘where is the user’” and reading off the answer is a Rube Goldberg machine for what should be a row lookup.

What typed memory looks like

A typed assertion is a row with structure. In ONTO, the minimum is:

Assertion(
    subject="alice",
    predicate="current_location",
    object="Lyon",
    level="session",            # session | profile | domain | tool
    confidence=0.9,
    decay=Decay.SESSION,
    provenance=Source(message_id="m_42", session_id="s_2026_05_20"),
    consent_scopes=["memory:write"],
    tags=["pii"],
    created_at=...,
)

This row has properties a vector embedding does not have:

  • Identity. (subject, predicate) is a key. The next time the user says “I’m in Berlin,” this row is superseded — not appended near, replaced.
  • Decay. Session-scoped facts disappear with the session. Profile facts (like timezone) decay slowly. Domain facts (a customer’s tier) decay on a policy curve.
  • Provenance. We know exactly which message produced this fact and which extraction prompt. We can replay.
  • Confidence. Low-confidence facts (“I think I’m in Lyon”) sit below high-confidence facts (“I am in Lyon”). The agent can weight them.
  • Consent scopes. The write required memory:write. Reads require memory:read. Cross-tenant reads fail by construction.
  • Regulation tags. [pii] lets the compliance layer route this row to encrypted storage and apply retention rules.

A vector embedding has none of these. It has a number array and (maybe) a metadata payload that you bolted on. To get the equivalent you re-invent every property above, less robustly.

The mistake everyone makes once

The reason most agents end up with vector-only memory is that vectors are easy. Embed everything; the LLM will figure it out at retrieval time. This works in the demo. It fails in production because the LLM can’t actually figure it out — it just confabulates plausible answers from whatever fragments it retrieves, and you don’t notice until a customer asks why the agent thinks they live in two cities.

The fix is not “better RAG.” The fix is to recognize that some things are facts and some things are documents, and to store them differently.

When to use both

A real agent has a memory layer that uses both:

  • Typed memory (UPO + SMO) for the things you can name. User profile, account state, session intent, current document being worked on, recently completed tasks. Anything where (subject, predicate) is a sensible question.
  • Vector store for the things you can’t pre-typify. Notes, emails, documents, code, knowledge-base articles. Anything where “find me similar content” is the right query.

ONTO ships both. Typed assertions go into UPO/SMO; documents go into the vector index. The agent’s prompt loads typed facts at the start of every turn (one query) and pulls in relevant documents on demand (vector search). The system uses each primitive for what it’s good at.

# Typed lookup: O(1) — is this true now?
user = onto.memory.get_profile("alice")
print(user.timezone)            # "UTC"

# Vector search: top-k similar — find content like this
notes = onto.search.query(
    "deployment notes for the auth migration",
    tenant_id="acme",
    top_k=5,
)

Two primitives. Two queries. No confusion about which is for which.

The conflict resolution story

Typed memory enables something vectors can’t: structured conflict resolution.

When the user said “I’m in Lyon” on Monday and “I’m in Berlin” on Wednesday, both assertions point at (alice, current_location). The system detects this conflict — same key, different objects — and creates a Plan Mode task: “user previously said Lyon; now says Berlin. Supersede?” Approve and the Lyon row is marked superseded_by=berlin_row_id. The agent now reads Berlin and ignores Lyon.

Vectors can’t do this. Two near-duplicate embeddings both retrieve. The model gets both and picks one (badly). The contradiction never surfaces.

For documents that should both exist, that’s fine. For facts that should resolve to one current value, it’s not.

The compliance story

Auditors ask three questions about your AI:

  1. What data did the model use?
  2. What did it conclude?
  3. Who approved the action?

Typed memory answers all three by construction. Every assertion has provenance (data source), confidence (conclusion), and [pii]/[phi]/[mnpi] tags (compliance routing). The Plan Mode log answers approval.

Vector-only memory answers none of these directly. You can get to “the model retrieved these chunks” but not to “the model believed this fact at this confidence.”

What you give up

Typed memory has a real cost: schema. You have to define your predicates. You have to write extraction prompts that target them. You have to handle evolution as the schema changes.

This is genuine work. The reason it’s worth doing is that the alternative — storing everything as text and asking the model to derive facts on every call — costs more in tokens, latency, and confusion. Once the schema settles, extraction is automated (set extract=True and the runtime routes things to the right level), and you stop paying for re-derivation.

A small principle

Use typed memory for facts. Use vectors for content. Use both, deliberately. Stop treating embeddings as a universal memory primitive — they’re not, and the bug shows up at exactly the moment your agent has been running long enough to matter.


ONTO’s typed memory model is described in detail on the features page and in the broader memory thesis. The comparison page shows how other SDKs handle this.

Frequently asked questions

Should I throw out my vector store?

No. Vector search is great for retrieving documents, notes, and unstructured content. The mistake is using it for facts you can name — a user's timezone, an account's tier, a customer's preferred channel. Use both: typed memory for facts, vectors for content.

Isn't typed memory just a schema-on-write database?

Yes, with extra features: provenance, decay curves, confidence scores, consent scopes, regulation tags, and conflict resolution. Most of these matter for compliance and long-running agents in ways a vanilla SQL schema doesn't capture.

How big can typed memory get?

ONTO ships with Sled for local deployments and Postgres for production. Millions of assertions is normal. The cost is in the writes (which extraction does for you) and reads — both indexed on (subject, predicate, tenant_id) for fast lookup.

Build with ONTO

The agent SDK where humans drive the state.

Plan Mode, typed memory, per-call consent scopes, and open-weight defaults. Open source under MIT or Apache-2.0.