Files
lore-engine/docs/08-architecture.md
Kaysser Kayyali 0f939b3bdb docs(adr): 0008 — graph backend is Neo4j (overrides Cognee's Kuzu default)
Cognee's default graph DB is Kuzu (PR #1022, June 2025). We override
to Neo4j for battle-testedness + Java UDFs. The time model
(time_in_window/overlap, era tree, current token) now ships as a
Neo4j Java UDF — queryable inline in Cypher, the contract was_true_at
depends on. Kuzu has no Java UDF mechanism, so on Kuzu the time model
would have been app-layer Python called outside the query. The POC's
pure-Python port becomes the reference impl + test oracle.

Resolves the 'verify which backend' hedge that was never executed:
  - 08-architecture.md: storage line, diagram, time-model impl, hosting
  - 22-cognee-boundary.md: storage ownership
  - CONTEXT.md: Cognee entry pinned to Neo4j
  - 00-overview.md: ADR index

Post-cognify consistency hook (Q10 second half): Cognee exposes no
built-in hook. 04-consistency.md updated — live sweep runs as a final
Task in run_custom_pipeline (post-ingest, <100ms); nightly full sweep
runs as external cron. Both write the same :ConsistencyRun shape.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 22:55:21 -04:00

20 KiB

08 — Architecture

The Lore Engine is a domain layer on top of Cognee, a self-hosted, MIT-licensed knowledge-graph framework. Cognee provides the storage abstraction (Neo4j + vector index — pinned by ADR 0008, overriding Cognee's Kuzu default), the LLM-based extraction pipeline, the embedding pipeline, and the agent-native remember/recall/forget API. The Lore Engine adds a typed high-fantasy ontology, a temporal model, a consistency engine, NPC knowledge scoping, and a TypeTemplate polymorphic extension system on top.

Cognee is the plumbing; the Lore Engine is the domain layer that the world-builder cares about.

System diagram

                            ┌─────────────────────────────────────┐
                            │         World-Builder Authoring      │
                            │   markdown · YAML · dialogue JSON    │
                            └──────────────┬──────────────────────┘
                                           │
              ┌────────────────────────────┼────────────────────────────┐
              │                            │                            │
              ▼                            ▼                            ▼
      ┌───────────────┐         ┌────────────────────┐         ┌────────────────────┐
      │ prose path    │         │ structured path    │         │ dialogue path      │
      │ (LLM extract) │         │ (YAML parse)       │         │ (HTTP POST)        │
      └───────┬───────┘         └─────────┬──────────┘         └─────────┬──────────┘
              │                           │                              │
              ▼                           ▼                              ▼
       cognee.add()              cognee.add() + Lore              cognee.add()
       cognee.cognify()          Engine parser                   + NPC scope
              │                           │                              │
              └───────────────────────────┼──────────────────────────────┘
                                           │
                                           ▼
                              ┌─────────────────────────┐
                              │     Cognee Storage      │
                              │  ─────────────────────  │
                              │  Neo4j (graph)          │
                              │  + Vector store         │
                              │  + Postgres (metadata)  │
                              │  ─────────────────────  │
                              │  Cognee manages:        │
                              │  DataPoint, Chunk,      │
                              │  + Lore Engine types:   │
                              │    Person, Faction,     │
                              │    Location, Era,       │
                              │    Lineage, MagicSystem,│
                              │    Item, Setting, Plane,│
                              │    DomainEntity, ...    │
                              └────────────┬────────────┘
                                           │
                                           │  Cypher / cognee.recall()
                                           │
                              ┌────────────▼────────────┐
                              │   Lore Engine extension │
                              │   (in-process with      │
                              │    Cognee MCP server)   │
                              │  ─────────────────────  │
                              │  8 inherited tools      │
                              │  37 new tools           │
                              │  10 consistency tools   │
                              └────────────┬────────────┘
                                           │  JSON-RPC over MCP
                                           │
                              ┌────────────▼────────────┐
                              │   LLM Client            │
                              │   (Claude, gpt-4, etc.) │
                              │   with system prompt    │
                              │   from reasoning harness│
                              └─────────────────────────┘

   Background jobs (Cognee data-pipelines):
   ┌─────────────────────────┐         ┌─────────────────────────┐
   │  consistency-pipeline   │         │  template-watcher        │
   │  (cron, 03:00 daily)    │         │  (file watcher on       │
   │  runs all rules         │         │   ./templates/,         │
   │  materializes nodes     │         │   hot-reload tools)     │
   └─────────────────────────┘         └─────────────────────────┘

The architecture is one process, in-process extensions: Cognee runs its MCP server; the Lore Engine registers as a Cognee data-model extension (typed labels, constraints, indexes) plus a tool extension (the 45 MCP tools). The world-builder never interacts with Cognee directly — the Lore Engine is the surface.

The two background jobs are Cognee data-pipelines (no separate Go services): the consistency pipeline runs the 4 rule categories on a schedule and on demand; the template-watcher watches ./templates/ and re-registers the TypeTemplate-driven tools on change.

Data flow: a question

1. User → LLM Client
   "Did House Vyr rule Valdorn in 340 TA?"

2. LLM Client → LLM (with system prompt + active context)
   LLM picks tool: was_true_at(RULED, "House Vyr", "Valdorn", "3rd_age.year_340")

3. LLM Client → Cognee MCP Server (JSON-RPC POST /mcp)
   {"method": "tools/call", "params": {"name": "was_true_at", "arguments": {...}}}

4. Cognee MCP Server → Lore Engine tool handler → Cognee storage
   The handler composes a Cypher query and executes it through Cognee's graph adapter (which may be Neo4j, Kuzu, or another Cognee-supported backend):
   MATCH (f:Faction {name: "House Vyr"})-[r:RULED]->(l:Location {name: "Valdorn"})
   WHERE time_in_window("3rd_age.year_340", r.valid_from, r.valid_until)
   RETURN r, r.valid_from, r.valid_until, sources

5. Neo4j → MCP Server
   [{valid_from: "3rd_age.year_312", valid_until: "3rd_age.year_360", sources: [...]}]

6. MCP Server → LLM Client (JSON-RPC response)
   {"was_true": true, "valid_from": "...", "valid_until": "...", "sources": [...]}

7. LLM Client → LLM
   Adds the result to its context.

8. LLM → User
   "Yes — House Vyr ruled Valdorn from 312 TA to 360 TA, which covers 340 TA.
    Sources: chronicles-vyr.md."

End-to-end latency target: <500ms for a single-tool call, <2s for a 3-tool chain.

Data flow: a plane question (v1.2)

The plane model (per 17-planes.md) is the new substrate for multi-setting, multi-plane worlds. A question that traverses planes looks like this:

1. User → LLM Client
   "Can Asmodeus reach the Material Plane from the Nine Hells? And what planes
    is the Roland of Mardonari connected to in 430 TA?"

2. LLM Client → LLM (with system prompt + active context)
   LLM picks tools: list_accessible_targets(plane='nine_hells'),
                     entity_planes_at_time(entity='roland_raventhorne', at='3rd_age.year_430')

3. LLM Client → Cognee MCP Server (JSON-RPC POST /mcp)
   Two tool calls, possibly chained.

4. Cognee MCP Server → Lore Engine tool handler → Cognee storage
   Cypher for accessibility:
     MATCH (start:Plane {id: 'mardonari.nine_hells'})-[:ACCESSIBLE_VIA|ADJACENT_TO*1..2]->(target:Plane)
     RETURN DISTINCT target.id, target.kind
   Cypher for time-bounded location:
     MATCH (p:Person {id: 'roland_raventhorne'})-[r:LOCATED_IN]->(plane:Plane)
     WHERE time_in_window('3rd_age.year_430', r.valid_from, r.valid_until)
     RETURN plane.id, plane.kind, plane.name

5. Neo4j → MCP Server
   Combined response: { reachable_planes: [...], rolands_planes_at_430: [...] }

6. MCP Server → LLM Client (JSON-RPC response)

7. LLM Client → LLM
   Adds both results to its context.

8. LLM → User
   "Yes — Plane Shift (a 7th-level spell) connects the Nine Hells to the Material.
    And in 430 TA, Roland was in the Mardonari Material Plane (pottery workshop) and
    had recently returned from a 2-year stint in Voldramir (a Mardonari demiplane)."

Plane relations (REFLECTS, LAYER_OF, ADJACENT_TO, ACCESSIBLE_VIA) make these questions a single Cypher traversal instead of a multi-step string-matching exercise.

Data flow: structured ingestion

1. World-builder writes timeline.yaml, family_tree.yaml, gazetteer.yaml.

2. World-builder → POST /ingest/structured (multipart)
   Files attached, source_type per file.

3. The Lore Engine extension (running in Cognee) routes the upload:
   - Structured YAML files (timeline, family_tree, gazetteer, bestiary, magic_system, culture)
     go through the per-type parser and are merged as Cypher.
   - Prose .md files are forwarded to `cognee.add()` + `cognee.cognify()`
     for LLM-based extraction.

4. Cognee persists the data; the Lore Engine's typed ontology is what
   Cognee stores (the labels and constraints live in the Lore Engine
   data-model extension, registered with Cognee at startup).

5. The consistency pipeline (a Cognee data-pipeline) gets a write event.
   - Runs the relevant anachronism + contradiction checks on the new edges.
   - Materializes any new :Contradiction / :Anachronism / :Orphan nodes.

6. Cognee state is now consistent.
   The next MCP tool call sees the new data.

Services layout (Lore Engine on Cognee)

lore-engine-extension/                [NEW]   the Lore Engine on Cognee
├── lore_engine/                      Cognee data-model extension package
│   ├── __init__.py                   registers labels, constraints, indexes
│   ├── ontology/                     typed labels (Person, Faction, ...)
│   ├── time_model/                   time_in_window, era-tree helpers
│   ├── consistency/                  4 rule categories + 10 starter rules
│   └── templates/                    TypeTemplate validator + registry
│
├── tools/                            the 45 MCP tools, in-process with Cognee
│   ├── lookup.py                     Group 1 — disambiguation
│   ├── entity_context.py             Group 1
│   ├── was_true_at.py                Group 2 — time-aware
│   ├── state_at.py                   Group 2
│   ├── list_lineage.py               Group 3
│   ├── event_chain.py                Group 4
│   ├── lore_about.py                 Group 5
│   ├── consistency_tools.py          Group 6
│   ├── generation_tools.py           Group 7
│   └── worldbuilder_tools.py         Group 8
│
├── pipelines/                        Cognee data-pipelines
│   ├── consistency_pipeline.py       nightly + on-demand rule run
│   └── template_watcher.py           watches ./templates/, hot-reload
│
├── parsers/                          structured YAML ingest
│   ├── timeline.py
│   ├── family_tree.py
│   ├── gazetteer.py
│   ├── bestiary.py
│   ├── magic_system.py
│   └── culture.py
│
└── schema/                           init.cognify / init.cypher
    ├── init.cypher                   constraints, indexes
    └── udfs/                         time_in_window, time_windows_overlap

The Lore Engine extension is one Python package that registers with Cognee at startup. Cognee provides the MCP server, the storage abstraction, the embedding pipeline, and the agent API; the Lore Engine provides the domain types, the time model, the consistency rules, and the 45 tools. There are no separate Go workers; the entire backend is in-process with Cognee.

Schema bootstrap (new Cypher)

The full schema lives in schema/init.cypher (to be generated during the build phase). These constraints and indexes are the Lore Engine data-model extension that Cognee applies on top of its own schema at startup:

// New label constraints
CREATE CONSTRAINT era_slug IF NOT EXISTS FOR (e:Era) REQUIRE e.slug IS UNIQUE;
CREATE CONSTRAINT calendar_name IF NOT EXISTS FOR (c:Calendar) REQUIRE c.name IS UNIQUE;
CREATE CONSTRAINT date_slug IF NOT EXISTS FOR (d:Date) REQUIRE d.slug IS UNIQUE;
CREATE CONSTRAINT lineage_name IF NOT EXISTS FOR (l:Lineage) REQUIRE l.name IS UNIQUE;
CREATE CONSTRAINT culture_name IF NOT EXISTS FOR (c:Culture) REQUIRE c.name IS UNIQUE;
CREATE CONSTRAINT magic_system_name IF NOT EXISTS FOR (m:MagicSystem) REQUIRE m.name IS UNIQUE;
CREATE CONSTRAINT language_name IF NOT EXISTS FOR (l:Language) REQUIRE l.name IS UNIQUE;
CREATE CONSTRAINT deity_name IF NOT EXISTS FOR (d:Deity) REQUIRE d.name IS UNIQUE;
CREATE CONSTRAINT spell_name IF NOT EXISTS FOR (s:Spell) REQUIRE s.name IS UNIQUE;
CREATE CONSTRAINT title_name IF NOT EXISTS FOR (t:Title) REQUIRE (t.name, t.domain) IS UNIQUE;
CREATE CONSTRAINT region_name IF NOT EXISTS FOR (r:Region) REQUIRE r.name IS UNIQUE;
CREATE CONSTRAINT material_name IF NOT EXISTS FOR (m:Material) REQUIRE m.name IS UNIQUE;

// New violation label constraints
CREATE CONSTRAINT anachronism_id IF NOT EXISTS FOR (a:Anachronism) REQUIRE a.id IS UNIQUE;
CREATE CONSTRAINT ontology_violation_id IF NOT EXISTS FOR (o:OntologyViolation) REQUIRE o.id IS UNIQUE;
CREATE CONSTRAINT orphan_id IF NOT EXISTS FOR (o:Orphan) REQUIRE o.id IS UNIQUE;
CREATE CONSTRAINT ontology_rule_id IF NOT EXISTS FOR (r:OntologyRule) REQUIRE r.id IS UNIQUE;
CREATE CONSTRAINT consistency_run_id IF NOT EXISTS FOR (c:ConsistencyRun) REQUIRE c.id IS UNIQUE;

// Composite index for the most common query: "what was X like at T?"
CREATE INDEX relation_time_window IF NOT EXISTS
FOR ()-[r:RULED|CONTROLS|LOCATED_IN|MEMBER_OF|PARTICIPATED_IN|ALLIED_WITH|ENEMY_OF|POSSESSES|SPOUSE_OF|PARENT_OF|WORSHIPS|PRACTICES|SPEAKS|BELONGS_TO|CLAIMS_TITLE]-()
ON (r.valid_from, r.valid_until);

// Index for era-tree traversal
CREATE INDEX era_parent IF NOT EXISTS FOR (e:Era) ON (e.parent_era);
CREATE INDEX era_slug_idx IF NOT EXISTS FOR (e:Era) ON (e.slug);

// Index for violation queries
CREATE INDEX anachronism_flagged IF NOT EXISTS FOR (a:Anachronism) ON (a.flagged);
CREATE INDEX ontology_violation_flagged IF NOT EXISTS FOR (o:OntologyViolation) ON (o.flagged);
CREATE INDEX orphan_flagged IF NOT EXISTS FOR (o:Orphan) ON (o.flagged);

User-defined functions (UDFs)

Two critical UDFs. They live in schema/udfs/:

time_in_window(t, valid_from, valid_until) → bool

The heart of the time model. See 02-time-model.md for the full specification.

// pseudocode, not runnable Cypher
RETURN time_in_window('3rd_age.year_345', '3rd_age.year_340', '3rd_age.year_352');
// → true

RETURN time_in_window('3rd_age.year_360', '3rd_age.year_340', '3rd_age.year_352');
// → false

Implementation notes:

  • Resolves current against the :Now config node.
  • Walks the era tree for parent-era membership.
  • Treats null as open-ended.
  • Implementation: a Neo4j user-defined function (Java) — the graph backend is Neo4j (ADR 0008), so the time model ships as a Java UDF queryable inline in Cypher, not as Python application-layer code. The POC's pure-Python time_in_window port is the reference implementation and test oracle for the UDF.

time_windows_overlap(from_a, until_a, from_b, until_b) → bool

For contradiction detection. Two windows overlap unless one ends before the other starts.

RETURN time_windows_overlap('3rd_age.year_340', '3rd_age.year_360',
                            '3rd_age.year_345', '3rd_age.year_370');
// → true

The time_in_window and time_windows_overlap UDFs are the only ones the engine needs. Everything else is regular Cypher.

Hosting & deployment

The Lore Engine on Cognee is one process that runs the Cognee MCP server, the storage adapter (Neo4j), and the Lore Engine extension. The hosting story is:

  • Cognee — runs in a Docker container, exposes the MCP server on :8000 (or whatever port the deployment chooses), manages the storage adapter. The Lore Engine extension is a Python package installed into the Cognee image.
  • Storage backend — Cognee supports Neo4j 5.x (most common in production), Kuzu (embedded, single-binary), and others via the cognee-neo4j / cognee-kuzu adapters. The Lore Engine does not need to know which one is in use.
  • Postgres (v1.1) — Cognee uses Postgres for metadata (sessions, tasks, the setting table). Single instance, managed by Cognee.
  • LLM provider — Cognee is LLM-provider-agnostic; it calls any OpenAI-compatible endpoint. The world-builder points it at a local model or a hosted one.

The deployment story is: build a Cognee image with the Lore Engine extension installed, point the storage adapter at Neo4j, set the LLM endpoint env vars, run. No new infrastructure on top of Cognee; the Lore Engine is code, not a new service.

Resource budget (Lore Engine delta on Cognee)

Component Memory Notes
Cognee MCP server (base) ~300MB Cognee is a single Python process; this is its baseline.
Lore Engine extension (in-process) ~+80MB The 45 tools, the typed ontology, the consistency rules. No separate process.
Neo4j (additional indexes for typed labels) ~+200MB 19 v1 + 2 v1.2 + 6 v1.1 + 5 consistency labels.
Postgres (v1.1) ~+50MB Single instance, the setting/lore_event/retcon tables.
Consistency pipeline (peak) ~+150MB Short-lived during nightly run.
Total delta over bare Cognee ~480MB Cheap.

The Lore Engine is cheaper to run than the prior GraphMCP-Example stack because the 5+ Go services are gone; everything is in-process with Cognee.

What is intentionally not in this architecture

  • No real-time updates. The consistency engine runs on a schedule and on demand. Streaming consistency (per-write checks) is for a future v2.
  • No LLM in the read path. Tool calls are pure Cypher. Only summarize_chain and narrate_arc call an LLM, and only on the generation side.
  • No external world-data sources. Wikipedia imports, fantasy-name generators, etc. are explicitly out. The engine reasons about the world as defined by its sources, not the world at large.
  • No user authentication. The MCP server is internal. The reasoning harness runs in a trusted context.
  • No graph versioning. Edits overwrite. If you need history, that's a v2 feature (and a real cost in storage and Cypher complexity).

The architecture is a deliberate floor. The features we don't have are features we can add when we have evidence they're needed, not features we can remove once added.