NPC Memory System¶
The NPC Memory System gives NPCs persistent memory of interactions, relationship tracking, and a knowledge graph. Together, these subsystems enable NPCs to remember players, form opinions, and reference past events in conversation.
Overview¶
The memory system consists of three interconnected subsystems:
| Subsystem | Purpose | Storage |
|---|---|---|
| MemoryService | Stores episodic and semantic memories per NPC-player pair | npc_memories collection |
| RelationshipManager | Tracks trust, respect, and friendliness between NPCs and players | npc_relationships collection |
| KnowledgeManager | Subject-predicate-object knowledge graph with confidence scores | npc_knowledge collection |
These feed into the EnrichedPromptBuilder, which injects relevant context into NPC dialogue prompts so AI responses reflect what the NPC "knows."
graph LR
A[Player Conversation] --> B[ExtractionJobQueue]
B --> C[MemoryExtractor]
C --> D[MemoryService]
C --> E[RelationshipManager]
F[World Events] --> G[KnowledgeObservationSystem]
G --> H[KnowledgeManager]
H --> I[GossipSystem]
D --> J[EnrichedPromptBuilder]
E --> J
H --> J
J --> K[NPC Dialogue Response]
Configuration¶
Enable the memory system with the MAID_MEMORY_ environment variables:
# Enable/disable the entire memory system
MAID_MEMORY_ENABLED=true
# Extraction pipeline
MAID_MEMORY_EXTRACTION_QUEUE_SIZE=100
MAID_MEMORY_EXTRACTION_TIMEOUT_MS=30000
MAID_MEMORY_EXTRACTION_RPM_PER_NPC=10
MAID_MEMORY_EXTRACTION_DAILY_TOKENS_PER_NPC=50000
# Memory bounds
MAID_MEMORY_MEMORY_BOUNDS_EPISODIC=200
MAID_MEMORY_CACHE_MAX_ENTRIES=1000
MAID_MEMORY_CACHE_TTL_SECONDS=300
# Context injection
MAID_MEMORY_CONTEXT_PROVIDER_TIMEOUT_MS=40
MAID_MEMORY_TOTAL_CONTEXT_BUDGET_TOKENS=2000
# Decay and consolidation
MAID_MEMORY_DECAY_CYCLE_TICKS=50
MAID_MEMORY_DECAY_RATE=0.01
MAID_MEMORY_CONSOLIDATION_CYCLE_TICKS=100
# Gossip (knowledge spreading)
MAID_MEMORY_GOSSIP_BUDGET_PER_TICK=3
MAID_MEMORY_GOSSIP_COOLDOWN_TICKS=10
Note
The memory system is disabled by default. Set MAID_MEMORY_ENABLED=true to activate it.
When the Classic RPG content pack loads, it checks whether engine.context_orchestrator
is None. If so, it creates a minimal ContextOrchestrator with only the
KnowledgeContextProvider registered, so knowledge graph entries (populated by
KnowledgeObservationSystem and gossip) still appear in NPC prompts.
Warning
MAID_MEMORY_DECAY_CYCLE_TICKS and MAID_MEMORY_DECAY_RATE are defined in settings
but MemoryDecaySystem uses its own constructor defaults (run_every_ticks=50,
internal decay rates per MemoryType). The startup code does not currently pass
these settings to the decay system. Similarly, MAID_MEMORY_GOSSIP_* settings are
not passed to GossipSystem — ClassicRPGContentPack instantiates it with constructor
defaults only.
Memory Creation: The Extraction Pipeline¶
Memories are created automatically when players converse with NPCs:
- Conversation ends — Player uses
endconversationor times out. - ExtractionJobQueue picks up the conversation transcript.
- MemoryExtractor (LLM-powered) analyzes the conversation and produces
episodic memories only — specific events ("Player asked about the dragon").
It returns
relationship_updates=[]; relationship deltas are not computed here. - ExtractionPipeline stores results and computes downstream updates:
- Memories →
MemoryService.store() - Relationship deltas → computed via
RelationshipManager.compute_deltas_from_memories(), then applied withRelationshipManager.update() - Cache invalidation →
MemoryCache.invalidate()called directly by the pipeline
- Memories →
# Example of what the extractor produces
from maid_stdlib.memory.extraction import ExtractionResult
from maid_stdlib.memory.models import EpisodicMemory, MemoryImportance
ExtractionResult(
memories=[
EpisodicMemory(
npc_id="npc_elder",
player_id="player_123",
content="Player revealed they are searching for the lost amulet",
importance=MemoryImportance.HIGH,
tags=["quest", "amulet", "revelation"],
emotional_valence=0.3,
),
],
relationship_updates=[], # RelationshipDelta objects
extraction_time_ms=142.5,
tokens_used=320,
)
Note
The extractor only returns memories directly. Relationship deltas are computed
downstream by the ExtractionPipeline, which maps memory tags and valence to
relationship dimension updates via RelationshipManager.compute_deltas_from_memories().
Memory Bounds and Eviction¶
Each NPC-player pair has limits:
- Episodic memories: Up to 200 (configurable via
MAID_MEMORY_MEMORY_BOUNDS_EPISODIC) - Semantic memories: Up to 50 (constructor default in
MemoryService, not in settings)
When limits are exceeded, MemoryService.enforce_bounds() evicts memories using a composite score of importance, decay, and age.
Relationship Dimensions¶
Relationships are tracked along three dimensions:
| Dimension | Range | Description |
|---|---|---|
| Trust | -100.0 to 100.0 | How much the NPC trusts the player |
| Respect | -100.0 to 100.0 | How much the NPC respects the player |
| Friendliness | -100.0 to 100.0 | How warmly the NPC regards the player |
These combine into a disposition score that determines the relationship tier:
HOSTILE— NPC actively dislikes the playerUNFRIENDLY— NPC is cold or dismissiveNEUTRAL— Default starting pointFRIENDLY— NPC is warm and helpfulALLIED— NPC considers the player a close friendDEVOTED— NPC is deeply loyal to the player
How Relationships Change¶
Relationships update through the extraction pipeline. The RelationshipManager.compute_deltas_from_memories() method maps memory tags and valence scores to relationship deltas:
# A memory tagged "hostile" with negative valence
# → decreases trust (-8) and friendliness (-5)
# A memory tagged "deceptive" with negative valence
# → decreases trust (-10)
# A memory tagged "helpful" with positive valence
# → increases trust slightly
# → increases friendliness slightly
Dramatic tier shifts (e.g., FRIENDLY → HOSTILE) are signaled via RelationshipTierChangedEvent. The MemoryEventBridge._on_relationship_tier_changed handler listens for these events and emits a StorySignalEvent for dramatic transitions, which can trigger quest generation.
Knowledge Graph¶
The knowledge graph stores facts as subject-predicate-object triples:
KnowledgeEntry(
subject="dragon",
predicate="lives_in",
object="mountain cave",
confidence=0.9,
source_npc_id="npc_elder",
source_type="observed",
ttl_seconds=7200, # Expires after 2 hours
propagation_count=0, # How many times this was gossiped
)
Key properties:
- Confidence (0.0–1.0): Decreases with each gossip propagation
- TTL: Knowledge expires after a set time (stale knowledge is cleaned up)
- Source tracking: Knows which NPC originally learned this fact
- Deduplication: Duplicate entries are merged, keeping higher confidence
Knowledge is created by:
- KnowledgeObservationSystem — Observes world events (combat, movement, item interactions). Available in
maid_stdlib.knowledge.observationbut not registered by default — content packs must register it explicitly. - Admin commands —
@knowledge add - GossipSystem — Propagates knowledge between NPCs (see Gossip System Guide)
Admin Commands¶
@npcmemory — View and Manage Memories¶
@npcmemory <npc_id> # List all memories for NPC
@npcmemory <npc_id> <player_id> # List memories for NPC-player pair
@npcmemory clear <npc_id> # Clear all NPC memories
@npcmemory clear <npc_id> <player_id> # Clear memories for specific pair
@npcmemory add <npc_id> <player_id> episodic "Met at the tavern"
@npcmemory add <npc_id> <player_id> semantic "Player is a blacksmith"
Note
The type argument accepts any string. "semantic" creates a semantic memory;
any other value (including "episodic") creates an episodic memory.
@relationship — View and Set Relationships¶
@relationship <npc_id> <player_id> # View relationship details
@relationship set <npc_id> <player_id> trust 50
@relationship set <npc_id> <player_id> respect -30
@relationship set <npc_id> <player_id> friendliness 80
Values are absolute on the -100 to 100 scale (clamped internally).
@knowledge — View and Add Knowledge¶
@knowledge <npc_id> # View NPC's knowledge graph
@knowledge add <npc_id> dragon lives_in "mountain cave"
Integration with NPC Dialogue¶
When a player talks to an NPC, the EnrichedPromptBuilder gathers context from all three subsystems:
┌─────────────────────────────────────────────┐
│ EnrichedPromptBuilder │
├─────────────────────────────────────────────┤
│ 1. Base prompt (personality, instructions) │
│ 2. Memory context (recent interactions) │
│ 3. Relationship context (tier, disposition) │
│ 4. Knowledge context (relevant facts) │
│ 5. {reputation} placeholder replacement │
│ (injected by EnrichedPromptBuilder) │
└─────────────────────────────────────────────┘
The ContextOrchestrator queries each provider with a token budget and timeout:
- MemoryContextProvider — Retrieves recent and important memories for the NPC-player pair
- RelationshipContextProvider — Provides the relationship tier and dimension scores
- KnowledgeContextProvider — Retrieves relevant knowledge entries
The total context budget (default 2000 tokens) is split across providers. If a provider times out, the prompt proceeds without that section.
Example Enriched Prompt¶
You are Elena, the village elder. You are wise and cautious.
ADDITIONAL CONTEXT:
[MEMORY] You remember that this player helped defend the village last week.
They mentioned searching for a cure for their sister's illness.
[RELATIONSHIP] Current relationship tier: FRIENDLY. Trust 70/100, respect 60/100,
friendliness 80/100. Interactions: 12.
[KNOWLEDGE] You know that: the healer moved to the eastern forest;
a rare herb grows near the old ruins; bandits have been seen on the north road.
Memory Consolidation¶
Over time, episodic memories are consolidated into semantic memories by the MemoryConsolidationSystem:
- Periodic scan — Runs every
MAID_MEMORY_CONSOLIDATION_CYCLE_TICKSticks - Identifies candidates — Episodic memories with high access frequency (above the access threshold)
- LLM summarization — Groups related episodic memories and produces a semantic summary
- Storage — Creates a new semantic memory, marks episodic originals as consolidated
This mimics how human memory works: specific episodes fade into general impressions over time.
Memory Decay¶
The MemoryDecaySystem gradually reduces memory decay_factor:
- Runs every 50 ticks by default (constructor default
run_every_ticks=50) - Reduces
decay_factorbased on per-MemoryTypedecay rates (constructor defaults) - Frequently accessed memories decay slower (access count is a factor)
- Below-threshold memories become eviction candidates
Note
The decay system uses its own constructor defaults, not the MAID_MEMORY_DECAY_*
settings values. The startup code instantiates it without passing settings overrides.
Cache Architecture¶
For performance, the memory system uses a multi-layer cache:
- MemoryCache — In-memory LRU cache with TTL (configurable size and expiry)
- CacheRefreshSystem — Periodic refresh every 60 seconds for active NPCs (refreshes memory and relationship adapters, not the MemoryCache directly)
- Event-driven invalidation —
ExtractionPipelineinvalidates theMemoryCachedirectly after storing new memories (MemoryEventBridgeonly emits events such asStorySignalEvent; it does not touch the cache)
This ensures that NPC dialogue doesn't block on database queries while still reflecting recent interactions.
Startup and Wiring¶
The memory system is initialized by maid_stdlib.memory.startup:
# Simplified startup flow (see maid_stdlib.memory.startup for full version)
memory_service = MemoryService(document_store)
relationship_manager = RelationshipManager(document_store)
knowledge_manager = KnowledgeManager(document_store) # event_bus is optional
memory_cache = MemoryCache(max_entries=1000, ttl_seconds=300)
orchestrator = ContextOrchestrator(provider_timeout_ms=40)
orchestrator.register_provider(
name="memory", provider=MemoryContextProvider(memory_service, cache=memory_cache),
priority=1, default_budget_pct=0.5,
)
orchestrator.register_provider(
name="relationship", provider=RelationshipContextProvider(relationship_manager),
priority=2, default_budget_pct=0.2,
)
orchestrator.register_provider(
name="knowledge", provider=KnowledgeContextProvider(knowledge_manager),
priority=3, default_budget_pct=0.3,
)
engine.context_orchestrator = orchestrator
The Classic RPG content pack then wires the EnrichedPromptBuilder to use this orchestrator for all NPC dialogue.
See Also¶
- Gossip System Guide — How knowledge spreads between NPCs
- NPC Dialogue Guide — AI-powered NPC conversation system
- AI Configuration — Provider and budget configuration