7.3 KiB
Data Models
Persistent and transient data shapes in the Mardonar Encounter Engine. Generated 2026-06-19.
The bot's data lives in three places: Redis (transient session state), the filesystem (data/, runtime artifacts), and the GraphMCP-backed graph — Neo4j, accessed through GraphMCP JSON-RPC. The bot itself does not query Neo4j directly.
Encounter spec (YAML → Zod → TypeScript)
Defined by EncounterSpecSchema in src/spec/loader.ts. Loaded by /encounter start <spec-name>. Stored in SessionState.spec.
{
encounterId: string, // unique ID — encounter session key in Redis
title: string, // display name in Discord embeds
tone?: string, // "tense" | "comedic" | ... optional flavor block
setting: {
location: string,
mood: string, // multi-line OK
ambientNpcs: string, // multi-line OK
},
openingNarrative: string, // multi-line; can reference {{nameKey}} placeholders
npcs: [{ // 1–5 entries
id: string, // unique stable ID
name: string,
nameKey?: string, // placeholder for randomizable substitution
role: string,
persona: string, // multi-line
memoryKey?: string, // if set, memory is loaded from / written to graph
}],
goals: {
hidden: boolean, // default true
primary: [{ id: string, label: string }], // min 1
secondary: [{ id: string, label: string }],
},
sportsmanshipRules: string[],
skillChecks: Record<string, number | string>, // grouped as <name>_dc / <name>_skill / <name>_note
randomizable?: [{ // optional
key: string,
source?: 'graphmcp' | 'vocabulary',
category?: string, // e.g. "names.dwarf.female"
query: string, // free-text query
fallback: string, // always available
}],
dmNotes?: string,
tools?: string[], // active tool plugin names; empty/undefined = all
}
tone and tools are read by the harness but not in the Zod schema (see architecture.md §9 for the schema-vs-types drift).
SessionState (Redis)
Stored as JSON under key session:{threadId}. Schema in src/types/index.ts:
{
encounterId: string,
threadId: string, // Discord thread snowflake
guildId: string,
spec: EncounterSpec,
players: Record<discordId, Player>,
history: ChatMessage[], // pinned + sliding mix
phase: 'open' | 'active' | 'resolved',
heldMessages: HeldMessage[], // for unregistered players
outcome?: string, // goal ID when resolved
outcomeSummary?: string,
npcMemories?: Record<npcId, string>, // injected into system prompt
resolvedContext?: Record<key, string>, // canonical session facts (context_recall)
pendingSkillCheck?: {
player: string,
prompt: string,
dc: number,
messageId: string, // Discord message ID of the dice embed
modifier?: number,
skill?: string,
advantage?: boolean,
disadvantage?: boolean,
},
pendingSkillCheckAttempts?: number,
createdAt: number,
updatedAt: number,
}
ChatMessage
{
role: 'system' | 'user' | 'assistant',
content: string,
pinned?: boolean, // never trimmed by contextAssembler
timestamp: number,
}
System messages are emitted by the harness for tool results, filter corrections, and join events. Assistant messages contain the LLM's narrative.
Player
{
discordId: string,
dndName: string,
pronouns?: string, // populated from characterRegistry if set
}
pronouns is added on first appearance in an encounter thread if the player has a characterRegistry profile.
Character profile (characterRegistry)
{
discordId: string,
guildId: string,
dndName: string,
pronouns?: string,
characterClass?: string,
race?: string,
level?: number,
backstory?: string,
foundryActorUuid?: string, // link to Foundry VTT actor
inventory?: unknown[], // populated from /character view
spells?: unknown[], // populated from /character view
// ... additional Foundry-derived fields
}
GraphMCP graph
The bot does not directly define the graph schema — it consumes whatever GraphMCP returns. The conceptual model based on the GraphMCP client types and the legacy design doc:
(:NPC {id, name, persona_summary, memory: [], last_seen_encounter})
-[:APPEARED_IN]->
(:Encounter {id, title, resolved, outcome_id, created_at})
-[:HAS_EVENT]->
(:EncounterEvent {timestamp, type, description})
-[:FEATURED]->
(:Entity {name, kind})
(:Player {discord_id, dnd_name})
-[:PARTICIPATED_IN]->
(:Encounter)
The bot writes to the graph via log_encounter (one encounter node + participants). It reads NPC memory via query_as_npc and the broader corpus via semantic_search.
File system (data/)
data/
├── tally.json // { [specName]: { runs: number, lastRun: ISO8601 } }
└── summaries/
└── {encounterId}-{ISO8601-with-dashes}.txt
// human-readable per-encounter summary
// header: Encounter, ID, Thread, Date, Outcome, Players
// body: free-text Summary
tally.json is rewritten atomically on each encounter start. Summary files are append-only.
Tool call payloads
// What the LLM emits
type ToolCallBlock = {
tool: ToolName,
args: Record<string, unknown>,
}
// What the harness parses back from the LLM response
type LLMResponse = {
narrative: string,
toolCall?: ToolCallBlock,
rawTokensUsed?: number,
}
Tool names:
The ToolName type in src/types/index.ts is string (a string alias), not a
discriminated union — the actual set of valid tools is enforced at runtime by
the plugin registry (src/harness/toolRegistry.ts, populated by side-effect
imports in src/harness/tools/index.ts). tests/unit/specsToolsConsistency.test.ts
catches drift between the registry and specs/*.yaml tools: lists.
Currently registered tools (src/harness/tools/):
| Name | File | Purpose |
|---|---|---|
skill_check_emit |
skillCheckEmit.ts |
Posts a skill-check embed and updates pendingSkillCheck |
encounter_resolve |
encounterResolve.ts |
Writes the encounter summary and archives the thread |
context_recall |
contextRecall.ts |
Returns canonical facts from resolvedContext |
goal_register |
goalRegister.ts |
Adds a dynamic goal mid-encounter |
foundry_lookup |
foundryLookup.ts |
Live VTT actor data |
foundry_reward |
foundryReward.ts |
XP / item grant to a VTT actor |
Removed in earlier refactors: skill_check_resolve, event_log_append, npc_memory_read, npc_memory_write — see docs/architecture.md §9. Their work is now handled by the per-encounter event log + GraphMCP log_encounter and query_as_npc RPC methods.
The four *_resolve / *_read / *_write entries are dead in the current implementation — replaced by GraphMCP log_encounter and other RPC calls. They should be removed from the type union (or actually re-implemented) to avoid confusion.
Context budget (compile-time const)
src/types/index.ts:
export const CONTEXT_BUDGET = {
SYSTEM: 4_000,
PINNED: 2_000,
HISTORY: 118_000,
SAFETY: 3_500,
TOTAL: 128_000,
} as const;
Used by contextAssembler and sessionManager to enforce the trimming policy.