TL;DR
Manta's agents reason over a per-tenant knowledge graph: an in-memory RDF store that runs inference to plan and act on a business's behalf. For an agent like that, "memory" must be correct on every turn and support reasoning, not just recall: a bar plain similarity search never clears. Mature memory stacks already know it: metadata filters, hybrid search, importance tiering, and temporal-graph approaches like Zep/Graphiti and Mem0 give memory structure and a lifecycle.
We take that instinct further: a memory here is a first-class RDF node, typed, scoped, and (for something structured like a target or budget) a subclass with strongly-typed properties. Because it obeys the same ontology, shapes, and rule engine as the business data beside it, the graph can infer over it: a stored performance target joins against the live KPI to derive whether it's being met, with no model call. The differentiator isn't "we use a graph." It's that the graph can infer over memory and join it against live facts, not merely store and fetch it.
1. Correct context, not just recalled context
An agent that only answers questions can get away with fuzzy memory. One that takes actions on a business cannot: once software can spend money, pause a campaign, or message a customer, "mostly remembered the context" isn't good enough.
Plain nearest-neighbor retrieval over embedded history fails in three compounding ways:
- Similarity isn't relevance. "Never spend more than $40k/month on Meta" matters to every budgeting decision, forever, but isn't lexically close to "draft me three headlines," so recall silently drops it exactly when it matters. Fixed by metadata filters and typed facts: pin
type = constraintand it's no longer a similarity bet. - Text can't be deterministically reasoned over. A budget stored as a sentence is inert; answering "are we over budget right now?" means re-parsing it and re-joining it to spend on every turn, with no guarantee two turns agree. Not fixed by typed storage alone. See below.
- Chunks have no lifecycle. They don't supersede or expire; change a target from ROAS 3.0 to 2.5 and the old chunk sits in the index contradicting the new one. Fixed by validity intervals and supersession: the move Zep / Graphiti and Mem0 already made.
#1 and #3 are solved problems in mature stacks. What isn't solved is #2: a structured, lifecycle-aware store still only stores and fetches; it doesn't compute. Our answer is to make structure the primary representation, not a filter bolted onto retrieval: a memory as a typed node in the same graph, under the same rules, as the business data it's about, so the graph doesn't just know a budget cap exists, it joins that cap against live spend and keeps deriving a status, with no model call. The rest of this piece is that mechanism.
2. Memory as knowledge, not text
We already run a per-customer knowledge graph: an in-memory RDF store and reasoner (RDFox) that materializes a business's campaigns, ad accounts, orders, and KPIs (roles and candidates, in our recruiting vertical), then runs rules over it to derive facts and drive the agent's planning loop.
Memory lives in that same graph, not in a separate vector service alongside it, as nodes speaking the same ontology as everything else:

Once a memory is a graph citizen, the graph treats it like any other fact: constrain it with a shape, infer from it, join it against live business data, scope it, expire it, supersede it, audit it, none of which a text chunk in an index can do.
3. The shape of a memory
Two typing dimensions matter. Kind is a closed set: preference, fact, objective, context (auto-captured, cross-conversation). It's coarse on purpose, just driving how a memory is grouped.
Subclass is open: a memory can additionally be a domain class subclass of the base :Memory type (PerformanceTargetMemory, BudgetConstraintMemory, BrandVoiceMemory), carrying its own strongly-typed, shape-validated properties. This is what turns a memory from prose into a record you can compute with:
:Memory_a1f9 a :BudgetConstraintMemory ;
rdfs:label "Meta monthly cap" ;
:description "Don't let Meta spend exceed $40k this month." ;
:memoryScope "company" ;
:memoryImportance 5 ;
:budgetPlatform "meta" ; # typed, enumerated
:budgetAmount 40000 ; # typed, numeric
:memorySource :OperatorEntry . # provenance, requiredEvery memory also carries an importance (1–5, the recall dial: top tiers always in context, middle on demand, bottom excluded), a required source, a confidence, an optional expiry, and once replaced, a pointer to its successor. Base and subclass properties split cleanly: one rule in §7 makes that free; a live status still needs its own join rule per subclass.
4. Scope, precedence, and isolation
A memory's scope (user, team, or company) decides who sees it, who may edit it, and how conflicts resolve, backed by stakeholder links rather than a label:
# What the current user is allowed to see, in a single pass.
SELECT ?m WHERE {
{ ?m :memoryScope "company" }
UNION
{ ?m :memoryScope "user" ; :memoryStakeholder :User_current }
UNION
{ ?m :memoryScope "team" ; :memoryStakeholder ?team .
:User_current :memberOf ?team }
}Writes are authorized the same way: you curate your own memories, shared ones are operator-gated, and you can't supersede one you don't own. Disagreements resolve to the more specific scope (personal > team > company), flagged if accidental: deterministic, not vibes. Scope governs sharing inside a tenant; isolation governs the boundary between them: each customer's graph is a physically separate store, so there's no shared table a query bug could leak across.
5. Getting memories in: capture without ceremony
Memories arrive three ways, all converging on the same validated RDF: only the recorded source differs. The agent fills in a typed memory through a structured tool (kind, scope, importance, optional subclass/properties/expiry), knowing which subclasses exist so "budget cap" becomes a BudgetConstraintMemory, not free text. The system extracts them: once a conversation accumulates enough turns (or goes idle), a background process bundles the unprocessed tail plus related known memories into a durable, retryable workflow that writes results back off the hot path. And people curate them directly; onboarding seeds a new tenant with its first objective, fact, and preference.

Underneath, a write is a sanitized graph update: an insert, or a diff with three-state semantics so a partial edit never clobbers an untouched field, every value escaped.
6. Getting memories out: recall that doesn't flood context
Recall is the harder half: every turn, the agent must show itself enough to be useful without letting a hard constraint drown. The strategy is layered.
- Pre-load the high-value core. Before each response, the agent reads the tenant's memories fresh and keeps the high-importance ones, grouped into labeled blocks (objectives, preferences, context, one per subclass), each tagged with an identifier it can cite to revise that exact memory.
- Rank by similarity inside the eligible set. Ordering within a block is by semantic similarity to the recent conversation: the one place embeddings earn their keep, as a ranking signal over a structurally-filtered set, never the gate. Structure decides what's eligible; similarity decides what's on top.
- Share company memory through the graph, not per user. High-value company memories are injected via the graph's live context (§8), so every user shares one copy instead of duplicating a constraint into everyone's window.
- Fetch the long tail on demand. Mid/low-importance memories aren't pre-loaded: the agent fetches them with an explicit search tool when needed. That's ordinary similarity retrieval, carrying the same risk raised in §1, but only over memories already deemed non-critical.
Why no caching on this read. It's deliberate, not an oversight, and it costs real latency every turn. Cross-conversation memory only works if something written in one chat is visible in another moments later: a stale cache would quietly reintroduce the "old chunk contradicts new fact" failure from §1. We accept the cost because it's cheap to accept: derived statuses are already maintained incrementally (§8), so a fresh read isn't re-running inference, just reading numbers the graph already kept current. Caching would save an already-cheap read at the cost of freshness, the one guarantee that makes shared memory trustworthy.
The result: a hard constraint is present because it's important, not because it happened to resemble the last message.
7. The payoff: subclasses that infer
Some memories aren't facts to recall: they're standing conditions to check against reality. A performance target only means something next to the current KPI, a budget cap only next to current spend. Because these are typed subclasses in the same graph as the live numbers, a rule can join them and keep status current: something an index can't do at all.
It starts with one rule asserting subclass membership directly, keeping the design open-ended:
# An instance of anything that is a subclass of :Memory is a :Memory.
[?m, a, :Memory] :-
[?m, a, ?subclass],
[?subclass, rdfs:subClassOf, :Memory] .
# subClassOf is transitive, so multi-level hierarchies still qualify.
[?a, rdfs:subClassOf, ?c] :-
[?a, rdfs:subClassOf, ?b],
[?b, rdfs:subClassOf, ?c] .With those two rules, any new class under :Memory is immediately a first-class memory: validation, recall, and supersession already match it, no code change needed. A live status still needs its own join rule. A replaced memory should disappear from normal use but stay for audit, expressed as an inferred class:
# A memory stays "active" until something supersedes it.
[?m, a, :ActiveMemory] :-
[?m, a, :Memory],
NOT EXISTS ?newer IN ([?m, :supersededBy, ?newer]) .Here's the join itself: a performance-target memory stores a metric and a goal. Since a store holds exactly one tenant's world (§4), there's a single :Company node the rule matches the active target against:
# Join the (single-tenant) company's live ROAS against an active
# ROAS target and derive whether it is being met.
[?t, :targetStatus, "below"] :-
[?t, a, :PerformanceTargetMemory],
[?t, a, :ActiveMemory],
[?t, :targetMetric, "roas"],
[?t, :targetValue, ?goal],
[?company, a, :Company],
[?company, :liveRoas, ?actual],
FILTER(?actual < ?goal) .A budget-constraint memory works the same way against spend, deriving within_budget / near_limit / over_budget (metrics where lower is better, like cost-per-acquisition, just flip the comparison). Standard RDFox reasoning: stratified negation and FILTER, nothing exotic.

The user said "keep ROAS above 3" once, months ago; that became a PerformanceTargetMemory. Each time KPIs refresh, the graph re-derives whether the target is met: no model call, no re-reading the sentence. By the time the agent loads context, targetStatus = below is already sitting there as a fact.
Worked example. In March a user says "don't let Meta spend go over $40k this month": the agent mints aBudgetConstraintMemory(platformmeta, amount40000, company scope, Critical). Three weeks later a different user, in a different conversation, asks to scale a campaign. Before that chat emits a token, the memory is already in context and, spend having refreshed to $34k overnight, already carriesnear_limit. The agent knows the cap, who set it, and that the account sits at 85% of it: facts it reads, not ones it reconstructs.
8. Live context: memory that keeps re-evaluating
The graph maintains a materialized bundle of what a fresh conversation should start with: KPI snapshot, connected accounts, subscription, high-value company memories. It's assembled by rules, not application code. A memory joins it when active, company-scoped, high-importance, and unexpired (checked against a clock node inside the graph, so the rules enforce it themselves); a guardrail caps how large the bundle may grow. A performance-target memory arrives already carrying its derived status, since inference ran on the same graph. A new conversation opens already knowing both the goal and whether it's met.
Two properties keep this trustworthy: materialization is incremental (a spend update recomputes only the affected facts, so status is never stale), and writes are serialized per tenant, so concurrent memory writes and pipeline updates can't corrupt one another.
9. Conflict, supersession, and audit
Because goals and facts change, replacement is a first-class, auditable event. Supersession is a deliberate dual pointer: the write goes on the old memory ("superseded by X"); the inverse is derived:
# The forward pointer is inferred from the reverse one.
[?new, :supersedes, ?old] :-
[?old, :supersededBy, ?new].The asymmetry is intentional: the derived pointer vanishes if the old memory is deleted, but "superseded by" persists, so history survives cleanup. Three mechanisms catch conflicts: automatic dedup (same label + scope updates in place), semantic conflict detection (a background model flags what a new memory would replace: auto-captured context resolves silently, anything a human might have meant to keep triggers ask-before-overwrite, since we never silently clobber a human's memory), and explicit replacement when the agent already knows what it's replacing.
Replaced memories are hidden from normal reads but stay in the graph, restorable by an operator (restore refuses if a still-live memory would contradict it). Provenance stays lightweight (source plus supersession chain, not a full formal ontology), since "where did this come from, what did it replace" is the question we actually ask.
10. Trade-offs and honest limitations
- A human is the backstop, not just the model (the biggest trade-off in practice). Importance is set by a human or a model guessing from context; file something Critical as Medium and it silently stops pre-loading. Ambiguous conflicts escalate to an ask-before-overwrite prompt instead of auto-resolving, and shared memories stay operator-gated: deliberate, since we'd rather a human adjudicate than have the system guess wrong on a memory others rely on.
- Eventual, not transactional, freshness. Derived statuses are near-real-time, not point-in-time consistent: right for steering, wrong for a number that must be exact-to-the-second.
- No caching on the read path (§6), on purpose: a latency cost paid every turn to guarantee cross-conversation freshness, justified because incremental materialization keeps that read cheap regardless.
- Embeddings still matter for ordering, not correctness. Similarity ranking can bury a relevant-but-dissimilar memory within an already importance-filtered block: blast radius is ordering, not omission.
- Subclass proliferation is a real cost. A sprawl of near-duplicate types erodes the shared-machinery benefit; keeping the ontology curated is ongoing work.
- Extraction can be wrong, and rules must be written for incrementality: a carelessly shaped join rule forces expensive recomputation instead of the cheap incremental updates the design depends on.
11. Where this goes
The pattern generalizes past marketing: the memory engine (subclass rule, supersession lifecycle, live-context injection) is domain-agnostic; only the subclass ontology changes per vertical. Our recruiting product defines its own memory types (hiring criteria, candidate assessments, sourcing preferences) on the same base, inheriting validation, recall, and supersession for free. Memory also sits upstream of action, shaping the plans the agent proposes and flagging when one would breach a stored constraint. A memory that reasons is just knowledge, kept current.
Further reading: RDFox: high-performance RDF store & reasoner · RDFox reasoning & rules · W3C RDF 1.1 Concepts · W3C SHACL (Shapes Constraint Language) · W3C PROV-O: The PROV Ontology · Datalog & stratified negation, Foundations of Databases (Abiteboul, Hull, Vianu) · Retrieval-Augmented Generation (Lewis et al., 2020) · Generative Agents (Park et al., 2023) · Zep: a temporal knowledge graph for agent memory (2025) · Mem0: memory layer for agents

