Back openDesk Edu for a sovereign, open-source education β every vote counts.
Vote nowYour AI agent knows that Sarah prefers Python over JavaScript. But when asked why, it cannot tell you. Was it from her GitHub activity? A conversation three months ago? A project she mentioned once?
This is the memory-persona validity gap: personas stored as flat profiles detached from the events that justify them. The result: persona drift, unexplainable preferences, and retrieval that misses the evidence.
PGMem (Persona-Memory Graph) closes this gap by connecting event and persona nodes through typed provenance and evidence edges. Every persona signal is traceable to the events that support or revise it.
Existing memory systems suffer from two related but distinct problems:
Problem: Personas are stored as flat summaries, disconnected from the events that created them.
# Current approach (flat profile)
persona = {
"preferred_language": "Python",
"experience_level": "senior",
"interests": ["ML", "graph databases"]
}
# Where did this come from? Unknown.
# Is it still valid? Unknown.
# What events support it? Unknown.
Consequence: When new evidence contradicts the persona, the system cannot reconcile the conflict. The persona drifts.
Problem: Memory retrieval ignores persona context. Queries retrieve events, but don't prioritise persona-relevant signals.
# User query: "What projects should I recommend?"
# Current retrieval: Fetch recent events
# Missing: Filter by user's known interests (Python, ML, graphs)
Consequence: Recommendations are generic, not personalised. The system "forgets" what it knows about the user.
PGMem structures memory as a heterogeneous graph with three node types and four edge types:
βββββββββββββββββββ
β Event Node β
β (timestamped) β
ββββββββββ¬βββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Evidence β β Provenance β β Revision β
β Edge β β Edge β β Edge β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Persona β β Persona β β Persona β
β Signal A β β Signal B β β Signal C β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
Node types:
Edge types:
At query time, PGMem performs evidence-based expansion:
def retrieve_personalised(query, user_id):
# Step 1: Identify query-relevant persona seeds
seeds = query_match_persona(query) # e.g., "Python" β preferred_language
# Step 2: Expand along evidence edges
evidence = graph.expand(seeds, edge_type="evidence")
# Step 3: Rank by evidential validity
ranked = rank_by_validity(evidence) # Stronger evidence = higher rank
# Step 4: Filter out revision-contradicted signals
valid = filter_revisions(ranked) # Remove signals with revision edges
return valid
The key: retrieval is persona-aware. It doesn't just fetch events β it fetches events weighted by persona relevance and evidential strength.
PGMem was evaluated across three benchmarks with small language model backbones (7B-13B parameter class):
| Benchmark | Task | Baseline | PGMem Improvement |
|---|---|---|---|
| PersonaChat | Dialogue personalisation | 0.62 F1 | +18.4% |
| MemBench | Memory retrieval accuracy | 0.71 EM | +22.7% |
| LongMem | Long-context memory (100+ turns) | 0.58 EM | +31.2% |
Key findings:
PGMem can run on:
Recommended schema (Neo4j):
// Create event node
CREATE (e:Event {
id: $event_id,
timestamp: $timestamp,
content: $content,
user_id: $user_id
})
// Create persona signal
CREATE (p:PersonaSignal {
id: $signal_id,
trait: $trait,
value: $value,
confidence: $confidence
})
// Link with evidence edge
CREATE (e)-[:SUPPORTS {strength: $strength}]->(p)
// Add provenance
CREATE (p)-[:FROM_SOURCE {source_type: $source}]->(e)
Common queries:
// Get all evidence for a persona trait
MATCH (p:PersonaSignal {trait: "preferred_language"})<-[:SUPPORTS]-(e:Event)
RETURN e.content, e.timestamp
ORDER BY e.timestamp DESC
// Find revision conflicts
MATCH (p:PersonaSignal)<-[:SUPPORTS]-(e1:Event)
MATCH (p)-[:REVISION]->(e2:Event)
RETURN p, e1, e2
// Rank evidence by validity score
MATCH (p:PersonaSignal)<-[s:SUPPORTS]-(e:Event)
RETURN p.trait, p.value, s.strength AS validity
ORDER BY validity DESC
PGMem adds graph traversal overhead but reduces LLM token waste:
For 100K events: ~15 MB graph storage, 100ms query latency
Here's the minimal architecture:
class PGMem:
def __init__(self, graph_store, extractor, ranker):
self.graph = graph_store # Neo4j client
self.extractor = extractor # LLM for persona extraction
self.ranker = ranker # Scoring model for evidence
def ingest_event(self, event):
# Step 1: Store event
event_id = self.graph.create_event(event)
# Step 2: Extract persona signals
signals = self.extractor.extract(event.content)
# Step 3: Link with evidence edges
for signal in signals:
self.graph.link_evidence(event_id, signal)
# Step 4: Check for revisions
existing = self.graph.find_conflicting_signals(signal)
for existing_signal in existing:
self.graph.add_revision(event_id, existing_signal)
def retrieve(self, query, user_id):
# Step 1: Find persona seeds
seeds = self.graph.query_persona_seeds(query, user_id)
# Step 2: Expand along evidence
evidence = self.graph.expand_evidence(seeds)
# Step 3: Rank by validity
ranked = self.ranker.score(evidence)
# Step 4: Filter revisions
valid = self.graph.filter_revisions(ranked)
return valid
PGMem reveals three trends:
Flat memory summaries are dead. Future systems will require provenance tracking for every persona signal.
Vector stores alone cannot capture structured relationships between events and traits. Graph-native memory will become standard for personalisation.
Personas evolve. Systems that track revisions and resolve conflicts will outperform static profile systems.
PGMem solves the memory-persona validity gap by:
For personalisation systems, the implication is clear: your memory must be traceable. Flat profiles drift. Graph-connected memory persists.