Agent Memory Patterns
MemoryArchitectureTutorialBest Practices
Implement persistent memory systems for AI agents with proven architectural patterns.
Agent Memory Patterns
Overview
Agent memory is crucial for building AI agents that remember context, learn from interactions, and maintain continuity across sessions. This tutorial explores proven patterns for implementing persistent memory in AI agents.
Memory Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Memory Layers │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Short-Term │───▶│ Working │───▶│ Long-Term │ │
│ │ (Context) │ │ Memory │ │ Memory │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Session │ │ Task │ │ Vector │ │
│ │ Context │ │ State │ │ Store │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Pattern 1: Hierarchical Memory
Implementation
# src/memory/hierarchical.py
from dataclasses import dataclass, field
from typing import Any
from datetime import datetime
@dataclass
class MemoryEntry:
content: str
category: str
importance: float # 0-1
timestamp: datetime
metadata: dict = field(default_factory=dict)
embeddings: list[float] = field(default_factory=list)
class HierarchicalMemory:
"""Multi-level memory system."""
def __init__(self):
# Short-term: Last N interactions
self.short_term: list[MemoryEntry] = []
self.short_term_limit = 20
# Working: Current task context
self.working: dict[str, Any] = {}
# Long-term: Persistent storage
self.long_term: list[MemoryEntry] = []
def add(self, content: str, category: str, importance: float = 0.5):
"""Add to appropriate memory level."""
entry = MemoryEntry(
content=content,
category=category,
importance=importance,
timestamp=datetime.now()
)
# Add to short-term
self.short_term.append(entry)
if len(self.short_term) > self.short_term_limit:
self.short_term.pop(0)
# If important, promote to long-term
if importance > 0.7:
self.long_term.append(entry)
self._consolidate_long_term()
return entry
def retrieve(self, query: str, level: str = "all") -> list[MemoryEntry]:
"""Retrieve relevant memories."""
results = []
if level in ["short_term", "all"]:
results.extend(self._search_short_term(query))
if level in ["long_term", "all"]:
results.extend(self._search_long_term(query))
return sorted(results, key=lambda e: e.importance, reverse=True)
def _search_short_term(self, query: str) -> list[MemoryEntry]:
"""Simple keyword search in short-term."""
query_lower = query.lower()
return [
e for e in self.short_term
if query_lower in e.content.lower()
]
def _search_long_term(self, query: str) -> list[MemoryEntry]:
"""Semantic search in long-term (requires embeddings)."""
# Use vector similarity
query_embedding = generate_embedding(query)
scores = cosine_similarity(query_embedding, [e.embeddings for e in self.long_term])
return [
entry for entry, score in zip(self.long_term, scores)
if score > 0.7
]
def _consolidate_long_term(self):
"""Merge similar memories to prevent bloat."""
# Group by category and merge similar entries
...
Pattern 2: Entity-Based Memory
Implementation
# src/memory/entity.py
from collections import defaultdict
from dataclasses import dataclass, field
@dataclass
class Entity:
name: str
entity_type: str # person, project, concept, etc.
attributes: dict[str, Any]
relationships: list[str] = field(default_factory=list)
last_seen: datetime = field(default_factory=datetime.now)
mention_count: int = 0
class EntityMemory:
"""Memory organized around entities."""
def __init__(self):
self.entities: dict[str, Entity] = {}
self.relationships: dict[tuple[str, str], list[str]] = defaultdict(list)
def extract_entities(self, text: str) -> list[Entity]:
"""Extract entities from text using LLM."""
prompt = f"""Extract entities from this text.
Text: {text}
Return JSON with entities and their types:
- person: People mentioned
- project: Projects or tasks
- concept: Technical concepts
- organization: Companies or groups
"""
# Call LLM to extract entities
...
def update_entity(self, name: str, updates: dict):
"""Update or create entity."""
if name not in self.entities:
self.entities[name] = Entity(
name=name,
entity_type=updates.get('type', 'unknown'),
attributes={}
)
entity = self.entities[name]
entity.attributes.update(updates.get('attributes', {}))
entity.last_seen = datetime.now()
entity.mention_count += 1
def add_relationship(self, entity1: str, entity2: str, relation: str):
"""Add relationship between entities."""
self.relationships[(entity1, entity2)].append(relation)
self.relationships[(entity2, entity1)].append(f"related_to_{relation}")
def get_entity_context(self, entity_name: str) -> dict:
"""Get full context for an entity."""
entity = self.entities.get(entity_name)
if not entity:
return {}
# Find related entities
related = []
for (e1, e2), relations in self.relationships.items():
if e1 == entity_name:
related.append((e2, relations))
return {
'entity': entity,
'related_entities': related,
'context_prompt': self._build_context_prompt(entity, related)
}
def _build_context_prompt(self, entity: Entity, related: list) -> str:
"""Build context prompt for LLM."""
lines = [f"About {entity.name} ({entity.entity_type}):"]
for attr, value in entity.attributes.items():
lines.append(f"- {attr}: {value}")
if related:
lines.append("\nRelated to:")
for name, relations in related:
lines.append(f"- {name}: {', '.join(relations)}")
return "\n".join(lines)
Pattern 3: Time-Weighted Memory
Implementation
# src/memory/temporal.py
from datetime import datetime, timedelta
import math
class TemporalMemory:
"""Memory with time-based decay and recency weighting."""
def __init__(
self,
half_life_days: float = 30.0,
recency_weight: float = 0.3,
importance_weight: float = 0.7
):
self.entries: list[MemoryEntry] = []
self.half_life = timedelta(days=half_life_days)
self.recency_weight = recency_weight
self.importance_weight = importance_weight
def add(self, entry: MemoryEntry):
"""Add entry with timestamp."""
entry.timestamp = datetime.now()
self.entries.append(entry)
def get_weight(self, entry: MemoryEntry) -> float:
"""Calculate time-weighted importance."""
# Recency factor (decays over time)
age = datetime.now() - entry.timestamp
recency_score = math.exp(-age / self.half_life)
# Combined score
return (
self.recency_weight * recency_score +
self.importance_weight * entry.importance
)
def retrieve(self, query: str, k: int = 10) -> list[MemoryEntry]:
"""Retrieve top-k memories by weighted score."""
# Filter by semantic similarity
candidates = self._semantic_search(query)
# Score by time-weighted importance
scored = [
(entry, self.get_weight(entry))
for entry in candidates
]
# Return top-k
scored.sort(key=lambda x: x[1], reverse=True)
return [entry for entry, _ in scored[:k]]
def prune(self, min_weight: float = 0.1):
"""Remove low-weight entries."""
self.entries = [
e for e in self.entries
if self.get_weight(e) >= min_weight
]
Pattern 4: Memory Graph
Implementation
# src/memory/graph.py
from collections import defaultdict
class MemoryGraph:
"""Graph-based memory with relationships."""
def __init__(self):
self.nodes: dict[str, dict] = {}
self.edges: dict[tuple[str, str], dict] = {}
def add_node(self, node_id: str, node_type: str, properties: dict):
"""Add a memory node."""
self.nodes[node_id] = {
'id': node_id,
'type': node_type,
'properties': properties,
'created': datetime.now()
}
def add_edge(self, from_id: str, to_id: str, relation: str, weight: float = 1.0):
"""Add relationship between nodes."""
self.edges[(from_id, to_id)] = {
'relation': relation,
'weight': weight,
'created': datetime.now()
}
def traverse(self, start_id: str, max_depth: int = 3) -> list[dict]:
"""Traverse graph from starting node."""
results = []
visited = set()
def dfs(node_id: str, depth: int, path: list):
if depth > max_depth or node_id in visited:
return
visited.add(node_id)
results.append({
'node': self.nodes[node_id],
'depth': depth,
'path': path + [node_id]
})
# Follow outgoing edges
for (from_id, to_id), edge in self.edges.items():
if from_id == node_id:
dfs(
to_id,
depth + 1,
path + [node_id]
)
dfs(start_id, 0, [])
return results
def find_related(self, node_id: str, relation_type: str = None) -> list[dict]:
"""Find all nodes related to a given node."""
related = []
for (from_id, to_id), edge in self.edges.items():
if relation_type and edge['relation'] != relation_type:
continue
if from_id == node_id:
related.append({
'node': self.nodes[to_id],
'relation': edge['relation'],
'direction': 'outgoing'
})
elif to_id == node_id:
related.append({
'node': self.nodes[from_id],
'relation': edge['relation'],
'direction': 'incoming'
})
return related
Pattern 5: Memory Consolidation
Implementation
# src/memory/consolidation.py
from difflib import SequenceMatcher
class MemoryConsolidator:
"""Consolidate and summarize memories."""
def __init__(self, similarity_threshold: float = 0.8):
self.threshold = similarity_threshold
def find_similar(self, entries: list[MemoryEntry]) -> list[list[MemoryEntry]]:
"""Group similar entries."""
groups = []
used = set()
for i, entry in enumerate(entries):
if i in used:
continue
group = [entry]
used.add(i)
for j, other in enumerate(entries[i+1:], i+1):
if j in used:
continue
similarity = self._similarity(entry.content, other.content)
if similarity > self.threshold:
group.append(other)
used.add(j)
if len(group) > 1:
groups.append(group)
return groups
def _similarity(self, a: str, b: str) -> float:
"""Calculate text similarity."""
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
def consolidate_group(self, group: list[MemoryEntry]) -> MemoryEntry:
"""Merge similar entries into one."""
# Use LLM to summarize
contents = [e.content for e in group]
prompt = f"""Summarize these similar memories into one:
{chr(10).join(f'{i+1}. {c}' for i, c in enumerate(contents))}
Summary:"""
summary = call_llm(prompt)
# Keep highest importance and most recent
best = max(group, key=lambda e: (e.importance, e.timestamp))
return MemoryEntry(
content=summary,
category=best.category,
importance=best.importance,
timestamp=datetime.now(),
metadata={'consolidated_from': [e.timestamp for e in group]}
)
def run_consolidation(self, memory: HierarchicalMemory) -> int:
"""Run consolidation on long-term memory."""
similar_groups = self.find_similar(memory.long_term)
original_count = len(memory.long_term)
for group in similar_groups:
consolidated = self.consolidate_group(group)
memory.long_term.remove(group[0]) # Remove first
for entry in group[1:]:
memory.long_term.remove(entry)
memory.long_term.append(consolidated)
return original_count - len(memory.long_term)
Complete Memory System
Integration Example
# src/memory/system.py
class AgentMemorySystem:
"""Complete memory system combining all patterns."""
def __init__(self):
self.hierarchical = HierarchicalMemory()
self.entity = EntityMemory()
self.temporal = TemporalMemory()
self.graph = MemoryGraph()
self.consolidator = MemoryConsolidator()
def process_interaction(self, user_input: str, agent_response: str):
"""Process a complete interaction."""
# 1. Extract entities
entities = self.entity.extract_entities(user_input)
for entity in entities:
self.entity.update_entity(entity.name, {
'type': entity.entity_type,
'attributes': {'last_interaction': datetime.now()}
})
# 2. Add to hierarchical memory
self.hierarchical.add(
content=f"User: {user_input}\nAgent: {agent_response}",
category="conversation",
importance=self._calculate_importance(user_input, agent_response)
)
# 3. Add to temporal memory
self.temporal.add(MemoryEntry(
content=agent_response,
category="agent_output",
importance=0.5,
timestamp=datetime.now()
))
# 4. Update graph
self._update_memory_graph(user_input, agent_response)
def get_context(self, query: str) -> str:
"""Get relevant context for a query."""
# Retrieve from all memory systems
hierarchical_results = self.hierarchical.retrieve(query)
temporal_results = self.temporal.retrieve(query)
# Merge and deduplicate
all_results = self._merge_results(
hierarchical_results,
temporal_results
)
# Build context prompt
context_lines = ["Relevant context from memory:\n"]
for i, entry in enumerate(all_results[:5], 1):
context_lines.append(f"{i}. {entry.content}")
return "\n".join(context_lines)
def _calculate_importance(self, user_input: str, agent_response: str) -> float:
"""Calculate importance of interaction."""
# High importance indicators
high_importance_keywords = [
'important', 'urgent', 'critical', 'deadline',
'remember', 'save', 'favorite', 'error', 'bug'
]
input_lower = user_input.lower()
if any(kw in input_lower for kw in high_importance_keywords):
return 0.9
# Question about preferences
if '?' in user_input and any(
kw in input_lower for kw in ['like', 'prefer', 'want', 'need']
):
return 0.8
return 0.5
def _update_memory_graph(self, user_input: str, agent_response: str):
"""Update memory graph with new relationships."""
# Extract entities and create graph nodes/edges
...
def _merge_results(self, *results_lists) -> list[MemoryEntry]:
"""Merge results from multiple sources, deduplicating."""
seen = set()
merged = []
for results in results_lists:
for entry in results:
key = entry.content[:100] # Use first 100 chars as key
if key not in seen:
seen.add(key)
merged.append(entry)
return merged
def run_maintenance(self):
"""Periodic maintenance tasks."""
# Prune old memories
self.temporal.prune()
# Consolidate similar memories
self.consolidator.run_consolidation(self.hierarchical)
Best Practices
1. Memory Budget
class BoundedMemory:
"""Memory with size limits."""
MAX_ENTRIES = 1000
MAX_BYTES = 10_000_000 # ~10MB
def add(self, entry: MemoryEntry):
# Check limits before adding
if len(self.entries) >= self.MAX_ENTRIES:
self._evict_oldest()
self.entries.append(entry)
def _evict_oldest(self):
"""Remove oldest entries to make room."""
self.entries.sort(key=lambda e: e.timestamp)
self.entries = self.entries[-self.MAX_ENTRIES:]
2. Selective Storage
def should_store(user_input: str, agent_response: str) -> bool:
"""Decide if interaction should be stored."""
# Don't store trivial exchanges
trivial_patterns = [
'hello', 'hi', 'thanks', 'thank you',
'goodbye', 'bye', 'okay', 'ok'
]
if any(p in user_input.lower() for p in trivial_patterns):
return False
# Store if it contains new information
if '?' in user_input or any(
kw in user_input.lower() for kw in ['what', 'how', 'why', 'explain']
):
return True
return False
3. Privacy Considerations
class PrivateMemory:
"""Memory with privacy controls."""
SENSITIVE_PATTERNS = [
r'\b\d{3}-\d{2}-\d{4}\b', # SSN
r'\b\d{16}\b', # Credit card
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', # Email
]
def sanitize(self, text: str) -> str:
"""Remove sensitive information."""
import re
for pattern in self.SENSITIVE_PATTERNS:
text = re.sub(pattern, '[REDACTED]', text)
return text
def add(self, entry: MemoryEntry):
entry.content = self.sanitize(entry.content)
# Then add to memory
Resources
- Mem0: https://mem0.ai/
- AgentMemory: https://agentmemory.dev/
- Zep: https://www.getzep.com/
- LangChain Memory: https://python.langchain.com/docs/modules/memory/
Last updated: May 2026
