A memory turn's read is a SPARQL query
Not a bespoke four-source fan-out with a phrase/word-overlap retry loop — a single graph-pattern query does the same class of retrieval in one language built for it.
Every AI agent memory system answers the same two questions: where does a fact live, and how does the right one find its way back into the next conversation? Neo4j just shipped a hosted graph service built for this. This article runs the identical scenario inline against an RDF database — OpenLink Virtuoso plus agent-rdf-memory — with working sample data and live SPARQL queries.
Neo4j's Developer Blog article Your AI Agent Forgets Everything. So We Taught It to Remember — With a Graph. introduces @neo4j-labs/nams-ai-provider, a Vercel AI SDK community provider over the hosted Neo4j Agent Memory Service. It is a programmer-oriented guide — and across six of twelve dimensions examined here, it converges with the position demonstrated via OpenLink Virtuoso plus agent-rdf-memory, a harness used dogfood-style to produce this article.
Two systems, one design question: what is a relationship, and where does it live? NAMS stores memory in two phases — first the raw text, then, if extraction is on, a second API call to persist the structure a language model proposes from it: a person, an organization, the relationship between them. On the hosted service today the first two land and the third does not — Entities land; edges do not, on the hosted path, today, in the article's own words. An RDF database cannot fail this way: an edge is not an optional REST endpoint — it is the predicate position of every triple, asserted in the same motion as the entities it connects. NAMS treats extraction and relationship-writing as two operations; a triple store treats them as one that cannot be split.
NAMS makes you a programmer before it makes you a rememberer. Every choice — which mode to run, when to extract, how a hook enforces a call — is written in TypeScript, against one SDK, for one provider. Add a second memory backend and you write it all again, from scratch, in a second provider package.
An RDF database asks nothing of you but the fact itself. Assert it once, in RDF, over open, ordinary protocols — HTTP, SPARQL, WebDAV — and it is there for whoever asks next: this session's SessionStart hook, a SPARQL client typed into a browser, a harness nobody has built yet. Swap the harness and only the asker changes. The fact never moves, because it was never nailed to the thing that first stored it. That is what loose coupling over an open standard buys you — and no SDK integration, however elegant, bolts it on after the fact.
These are not caption text — each is a modelled schema:DefinedTerm in the companion RDF, surfaced here as the argument's actual spine.
Not a bespoke four-source fan-out with a phrase/word-overlap retry loop — a single graph-pattern query does the same class of retrieval in one language built for it.
Not a two-step extract-then-store pipeline — structured data is authored directly at write time, with no separate extraction model call and no risk of a failed or degraded extraction.
It is the predicate position of every triple. A triple without a predicate is not a triple, so an RDF database cannot have NAMS's disclosed 'entities land, edges do not' limitation by construction.
The retrieval guarantee NAMS hand-built from prepareStep/onFinish and the checkpoint agent-rdf-memory hand-built from its SessionStart gate and Memory Write Rule are both harness-level mechanisms, not prompt suggestions a model can shrug off — they simply clamp different points in the request lifecycle. NAMS's hooks constrain the tool loop step by step, holding toolChoice:'required' until a specific call fires. agent-rdf-memory's hook fires once, at session start, guaranteeing memory context loads before the model's first response.
setEntityFeedback(), getEntityHistory(), and mergeEntities() are three separate API surfaces in NAMS; in an RDF database they are three SPARQL query/update shapes over the same store — an ordinary triple, a SELECT ordered by a timestamp predicate, and a DELETE/INSERT WHERE.
| Dimension | NAMS (@neo4j-labs/nams-ai-provider)Vercel AI SDK Provider over Hosted Memory API | Virtuoso + agent-rdf-memoryRDF Database, Harness-Level Memory Protocol |
|---|---|---|
| Edges: optional REST endpoint or predicate positiondivergent | rest-endpoint-missingTyped relationships (WORKS_AT, PREFERS, USES) are extracted and an attempt is made to write each one, but the hosted REST API has no relationship-write endpoint yet — writes are skipped, warned once, then suppressed. | predicate-is-mandatoryA triple without a predicate is not a triple. In an RDF database the edge is not a separate write path that can be missing — it is the middle term of every statement, present by construction. |
| Relevance selection: bespoke fan-out or SPARQLdivergent | four-source-fanout-two-passretrieveMemories() fans out across four sources in parallel, dedupes by content, ranks by relevance score or source priority, caps at maxMemories, and retries a failed direct-phrase search with a word-overlap pass over significant words. | single-sparql-queryA graph pattern with OPTIONAL/UNION plus a text index (e.g. Virtuoso's bif:contains) does structured lookup and phrase-variance matching in one query language, because SPARQL was built for exactly that class of retrieval problem. |
| Entity-shaped at write time or after an extraction passdivergent | post-hoc-extractionStored text is turned into typed nodes with a confidence score via a dedicated extractionModel LLM call; off by default, and falls back to a flat entity if extraction fails. | authored-structured-at-writeRule-triggered writes (the Memory Write Rule) author structured Turtle directly — schema:Person, schema:HowToStep, etc. — no mining step, no risk of failed or degraded extraction. |
| Reliability guarantee: code-level hook or harness-level gateconvergent | sdk-hooks-unskippableenforceQueryMemory() (a prepareStep hook forcing toolChoice:'required' after graceSteps) and ensureMemoryStored() (an onFinish hook persisting the turn if store_memory was never called) are code-level SDK hooks the tool loop literally cannot skip. | sessionstart-gate-plus-write-ruleThe SessionStart hook/9-step retrieval protocol is a structural gate outside model discretion; the Memory Write Rule is the write-side analog of ensureMemoryStored(). Both are harness-level mechanisms; they clamp different points in the request lifecycle. NAMS's hooks constrain the tool loop step by step, holding toolChoice:'required' until a specific call fires. agent-rdf-memory's hook fires once, at session start, guaranteeing memory context loads before the model's first response. |
| Correction and history: bespoke REST verbs or query shapesdivergent | mergeentities-gethistory-verbsmergeEntities() and getEntityHistory() are dedicated REST-shaped API methods, each its own endpoint with its own contract. | delete-insert-where-and-select-order-by'Fix just that' is a plain SPARQL 1.1 DELETE/INSERT WHERE against a stable IRI. History is prov:generatedAtTime plus schema:dateCreated/dateModified ordering, queryable with an ordinary SELECT — no special endpoint for either. |
| Confidence: proprietary field or ordinary tripledivergent | setentityfeedback-api-fieldsetEntityFeedback() and the extraction confidence score are bespoke fields on the entity-graph API, not part of a general triple model. | plain-predicate-or-rdf-star-annotationConfidence is modelled as an ordinary predicate on a named assertion, or as an RDF-star annotation ({| :confidence 0.9 |}) on the base triple directly — agent-rdf-memory already has live-verified RDF-1.2/RDF-star support. |
| Meta-entity contamination guard: same discipline, different attack surfaceconvergent | isCommonNoun-structural-guardA structural guard (a proper noun differs from its own lowercase form, plus a self-type-name check) prevents meta-entities like 'long-term memories' [Concept] from being minted out of the model's own answer and outranking real entities. Exposure comes from LLM-driven extraction over arbitrary generated text, including the model's own answers. | rule-triggered-narrower-not-zeroWrites are rule-triggered by explicit phrases ('remember this', 'going forward'), not mined from every assistant utterance — a narrower attack surface than an always-on extractor, but not a zero one: a badly-triggered rule could still write junk. Not a claim of immunity. |
| Fail-safe degrade: same invariant, live proof pointconvergent | warning-not-thrown-error'If memory breaks, the model call still succeeds' — every retrieval/persistence failure degrades to a logged warning, never a thrown error in the request path, backed by a 63-test Vitest suite. | sparql-bootstrap-fallback-to-filesystemThe exact same invariant, demonstrated live: this document's own generating session's SessionStart hook attempted SPARQL bootstrap against two local endpoints, both returned 401, and the harness fell back to filesystem reads automatically — the model call proceeded without interruption. |
| Cross-session, cross-model continuityconvergent | store-not-context-windowMemory outlives the model: swap GPT for Claude for Gemini, history comes with you, because it lives in the store, not the context window. | sessions-dir-already-multi-modelNot an aspiration — agent-rdf-memory's sessions/ directory already spans Claude, DeepSeek, Grok, GPT, and GLM sessions against the same RDF store. |
| Multi-tenancy: a parameter, not a design projectconvergent | scope-userid-parameterMulti-tenancy is scope: { userId } — a parameter rather than a design project. | graph-iri-per-sessionSame claim, same mechanism class: GRAPH IRI scoping per user/session is how RDF databases work natively — this document's own sample dataset demonstrates it with two session-scoped named graphs. |
| Schema-free write vs required extraction calldivergent | extraction-model-call-requiredEven at its simplest, entity-shaped storage still needs an extractionModel LLM call per stored memory; the flat-entity fallback only triggers on extraction failure, not as a design choice to skip it. | triples-with-zero-upfront-schemaTriples can be asserted with zero upfront schema; SHACL/OWL shapes layer on later without migration. A lower floor than NAMS, which needs a model call to reach even its simplest stored form. |
| Memory-provider swap: a config line either wayconvergent | one-provider-binding-among-severalNAMS is one binding among the AI SDK's five listed memory providers (Letta, Mem0, Supermemory, Hindsight, MongoDB) — none graph-backed before this package — selected the same way any other provider is selected. | thin-binding-not-architectureConvergent, not a point of disagreement: both systems treat 'which store' as a thin binding, not an architecture decision. |
A worked sample mirroring the source article's own retrieval example. Session 1 asserts Alex is based in Bangalore, with a confidence-scored assertion recording the provenance timestamp; session 2 corrects it to Delhi and stores a reasoning trace explaining why — the fourth retrieval source NAMS fans out to, modelled here as an ordinary typed entity rather than a bespoke memory kind.
schema:Person · works for TechCorp (schema:worksFor) · current schema:homeLocation isDelhi
schema:Organization · the WORKS_AT-equivalent target via schema:worksFor
Old value asserted in session 1 (confidence 0.72, 2026-09-10), corrected in session 2 (confidence 0.95, 2026-09-16) — both timestamped via prov:generatedAtTime
Two :ConversationSession entities, each :scopedToGraph a distinct DAV named-graph IRI — multi-tenancy and cross-session continuity as a GRAPH parameter
The agent's own past reasoning, stored as a first-class :ReasoningTrace entity — NAMS's fourth fan-out source, here just another typed triple
Tool descriptions are hints, not constraints — nothing in a tool definition stops a model from finishing a turn without ever calling query_memory. NAMS closes that gap with hooks that control the tool loop directly, not with a stronger prompt.
enforceQueryMemory() is a prepareStep hook. Until query_memory has appeared among the turn's executed tool calls, every step in the loop is held at toolChoice:'required' — the model has no path to a step that isn't a tool call.
graceSteps defaults to 3. Once that many steps pass without a query_memory call, the hook stops waiting and forces the next step directly to query_memory.
As soon as query_memory runs once in the turn, every toolChoice constraint from enforceQueryMemory() drops for the remainder of the loop — the hook has done its one job.
prepareStep can't act once the loop has already ended, so the write-side guarantee runs on a different hook: onFinish, which fires after the model has emitted its final text for the turn.
If store_memory was never called during the turn, ensureMemoryStored() persists the turn itself — the assistant's final text, stored as an interaction rather than a fact — and reports {stored: true}. If the model did call it, the hook reports {stored: false, reason: 'already-stored'} and does nothing further.
prepareStep and onFinish are primitives the AI SDK already ships. NAMS's own article states that any provider in the ecosystem could adopt the same pattern — the guarantee is a design choice, not a NAMS-specific capability.
NAMS's problem is optionality — query_memory is a tool the model can simply never call. agent-rdf-memory never faces that problem, because loading memory was never the model's decision to begin with: the harness initializes it automatically at session start, before the model produces a single token, and the loaded context stays available to the calling agent for the rest of the session, drawn on wherever it is relevant.
In Claude Code this is a SessionStart hook; in Codex-family harnesses, an AGENTS.md entry point. Either way it is code the calling harness executes before the model's first response — not text sitting in the model's own context that it might read or skip.
The hook attempts a SPARQL bootstrap against the configured endpoint first. On failure, it falls back to a fixed file-read sequence: core.ttl, preferences.ttl, ontology.ttl, index.ttl, then SPARQL-driven context selection where available.
The hook checks the most recent session file for a recorded loading-method preference first, so the elicitation happens once per environment, not on every session start.
The write side has no relationship to the SessionStart hook. Specific trigger phrases in user input — 'remember this', 'going forward', 'that's a rule' — require a write to preferences.ttl or howto/*.ttl as valid Turtle.
The SessionStart hook's read side is unskippable at the session boundary. The Memory Write Rule's write side has no forcing function analogous to onFinish — it depends on the model recognizing the trigger phrase and acting on it in that turn. This is the one place in the whole comparison where NAMS's mechanism is structurally stronger, stated directly, not softened.
A Vercel AI SDK community provider backed by the hosted Neo4j Agent Memory Service (NAMS). One client/API key gives access to three integration modes — Provider, Middleware, and Tools — plus optional entity extraction and a four-source retrieval fan-out.
That the hosted REST API has no relationship-write endpoint yet. The extractor produces typed relationships (WORKS_AT, PREFERS, USES) and attempts to write each one, but those writes are skipped, warned once, then suppressed. Entities land; edges do not, on the hosted path, today.
Because an edge is not an optional REST endpoint in RDF — it is the predicate position of every triple. A triple without a predicate is not a triple, so there is no separate 'edge write' step that can be present or absent.
It solves a real problem — structured lookup plus phrase-variance matching — but SPARQL already solves that same problem class in one query language: a graph pattern with OPTIONAL/UNION plus a text index does what the two-pass phrase/word-overlap retry does, without a bespoke retrieval algorithm.
NAMS mines typed nodes out of stored sentences via an extractionModel LLM call, off by default, with a flat-entity fallback on failure. agent-rdf-memory's rule-triggered writes author structured Turtle directly at write time — no mining step, no extraction failure mode.
Structurally yes, mechanically no. Both are checkpoints outside model discretion, but they clamp different points in the request lifecycle. NAMS's hooks are unskippable interpreter-level code inside the AI SDK's own tool loop, constraining every step until a specific call fires. agent-rdf-memory's SessionStart hook fires once, at session initialization, guaranteeing memory context loads before the first response — it does not constrain tool choice turn by turn the way NAMS's hooks do.
A narrower one, not a zero one. NAMS's exposure comes from LLM-driven extraction over arbitrary generated text, including the model's own answers. agent-rdf-memory's writes are rule-triggered by explicit phrases rather than mined from every utterance — narrower, but a badly-triggered rule could still write junk.
This document's own generating session. Its SessionStart hook attempted SPARQL bootstrap against two local endpoints, both returned 401, and the harness fell back to filesystem reads automatically — the exact 'if memory breaks, the model call still succeeds' invariant NAMS built a 63-test Vitest suite to prove, observed happening live.
mergeEntities() and getEntityHistory() are dedicated REST endpoints in NAMS. In SPARQL, correction is a DELETE/INSERT WHERE against a stable IRI, and history is an ordinary SELECT ordered by prov:generatedAtTime — three REST methods collapse into two query shapes.
No — both model it, differently. NAMS exposes setEntityFeedback() as a bespoke API field. This document models the same idea as an ordinary predicate on a named assertion, and additionally as a direct RDF-star annotation on the base triple.
It mirrors the source article's own Alex/TechCorp/Bangalore-Delhi retrieval example as asserted RDF: a Person, an Organization, an employment predicate, an old-then-corrected location with provenance timestamps, a confidence-scored assertion, two session-scoped named graphs, and a reasoning-trace memory item.
Not yet. Every query is GRAPH-scoped to the DAV path this Turtle file would occupy once uploaded. Until an explicit, confirmed upload happens, the GRAPH IRI is a projected target, not a live location — the queries will return 0 rows.
The hosted memory backend behind @neo4j-labs/nams-ai-provider, offering durable storage, retrieval, and optional entity extraction over a REST API.
The community-provider framework NAMS integrates with. Its own memory guide lists five providers, none graph-backed, before this package.
nams.languageModel(...) — wraps a model so memory is invisible to the caller: guaranteed, but not observable in the UI stream.
nams.wrap(model, scope) — intercepts calls to layer memory onto an existing model integration without changing the tool surface.
Two Zod-validated tools, query_memory and store_memory, that the model calls itself — visible in the UI stream, but skippable unless a guarantee hook forces the call.
The dedicated LLM call NAMS uses to turn stored text into typed entity nodes with a confidence score. Off by default; falls back to a flat entity on failure.
retrieveMemories() querying four sources in parallel — long-term/entities, current conversation, cross-session, and reasoning traces — then deduping and ranking the merged results.
A direct phrase search first; if it returns nothing, a retry using the query's significant words (original case + Title Case), merged and ranked by word overlap.
An AI SDK primitive run before each tool-loop step; enforceQueryMemory() uses it to hold toolChoice:'required' until query_memory has executed.
An AI SDK primitive run after a turn completes; ensureMemoryStored() uses it to persist the turn if store_memory was never called.
name === name.toLowerCase() && name !== name.toUpperCase() — a proper noun differs from its own lowercase form. Used, plus a self-type-name check, to stop meta-entities being minted from the model's own generated answers.
An IRI-identified set of RDF triples within an RDF database, letting scope (e.g. a session, a user) be attached to a group of statements without a bespoke multi-tenancy design.
Syntax for annotating a base triple directly (e.g. {| :confidence 0.9 |}) without reifying it as a separate named resource — agent-rdf-memory has live-verified RDF-1.2/RDF-star support against its own loaded graphs.
The standard query-language shape for correcting an assertion in place — the SPARQL counterpart to a bespoke mergeEntities()/correction REST endpoint.
An RDF store adding a fourth (graph) position to the subject-predicate-object triple, enabling named-graph scoping — the mechanism behind treating multi-tenancy as a GRAPH parameter.
Interactive graph visualization derived from the companion RDF. Click nodes to resolve, drag to explore. Graph data embedded from companion RDF at generation time.
Query this knowledge graph on URIBurner. The editor opens on the canonical SAMPLE entity-type summary (DAV named graph). Every query below returns 0 rows until this document's Turtle file has been uploaded to that DAV path — not yet performed in this session.
Reproduced verbatim from the companion RDF. Execute loads the query into the workbench below and runs it live.
PREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?pIri ?p ?oIri ?o
FROM <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
WHERE {
:alex ?pIri ?o .
BIND(?pIri AS ?p)
OPTIONAL { FILTER(isIRI(?o)) BIND(?o AS ?oIri) }
}
ORDER BY ?pPREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
WITH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
DELETE { :alex schema:homeLocation ?oldLocation }
INSERT { :alex schema:homeLocation :delhi }
WHERE { :alex schema:homeLocation ?oldLocation }PREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
PREFIX prov: <http://www.w3.org/ns/prov#>
SELECT ?assertionIri ?assertion ?value ?generatedAt
FROM <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
WHERE {
?assertionIri a schema:PropertyValue ; schema:propertyID "schema:homeLocation" ;
schema:name ?assertion ; schema:value ?value ; prov:generatedAtTime ?generatedAt .
}
ORDER BY ?generatedAtPREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
CONSTRUCT { ?p ?predIri ?o }
FROM <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
WHERE {
:alexDuplicate owl:sameAs :alex .
?p ?predIri ?o .
FILTER(?p = :alex || ?p = :alexDuplicate)
}PREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?assertionIri ?assertion ?value ?confidence
FROM <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
WHERE {
?assertionIri a schema:PropertyValue ; schema:name ?assertion ; schema:value ?value ;
:confidence ?confidence .
}
ORDER BY DESC(?confidence)PREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?entityIri ?entity ?typeIri
FROM <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.ttl>
WHERE {
?entityIri a ?typeIri ; schema:name ?entity .
BIND(STRAFTER(STR(?typeIri), "#") AS ?typeLocalName)
FILTER(LCASE(?entity) = LCASE(?typeLocalName))
}PREFIX : <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1.html#>
PREFIX schema: <http://schema.org/>
SELECT ?session ?location
WHERE {
{
GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1-session1.ttl> {
BIND("session1" AS ?session) BIND("Bangalore" AS ?location)
}
} UNION {
GRAPH <https://linkeddata.uriburner.com/DAV/demos/daas/neo4j-agent-memory-graph-vs-virtuoso-agent-rdf-memory-meshup-claude_sonnet_5-1-session2.ttl> {
BIND("session2" AS ?session) BIND("Delhi" AS ?location)
}
}
}
ORDER BY ?session