Files
nimmerworld.eachpath.local/narrative-composition/architecture.md

20 KiB
Raw Blame History

Narrative Composition

How stories happen: the Compositor as narrative-composer cleaved from equilibrium-seeker GM; cyclic forward-prop / back-write loop where per-player perspectives ascend and canonical narrative descends; catalogue + tools as typed contract; the Compositor as a fractal/recursive primitive at three tiers (zone-event, district-event, world-event); world-server / district-server generation as the same Compositor primitive applied at design-time (init-function for canon-rows); reflexive Dream-process at every layer with hand-authored reward-guardrails encoding the designers' ethical stance.

Companion to: architecture-broad.md (executive summary + global meta-lists), runtime-engine/architecture.md (lemniscate emits typed perspective-summaries that feed the Compositor), inference-and-memory/architecture.md (back-writes land in primary.sqlite via UID-keyed routing), scale-and-transport/architecture.md (Compositor instances are stateless workers; transport carries canon-fragments). Sections in this file were split from the monolithic architecture-broad.md v0.7 on 2026-04-26.

The Compositor — narrative composition role

The bidirectional cascade shows authority flowing down and reports flowing up, but it does not specify who composes the canonical narrative from per-player perspectives nor who packages the back-write to participants. That role is the Compositor, an architectural cleavage from the GM introduced in v0.6.

The cleavage in one sentence: the GM is an equilibrium-seeker (aggregate-observer, catalogue-event-selector, tool-granter); the Compositor is a narrative-composer (perspective-gatherer, canon-author, back-write-packager). These are different cognitive shapes that prior AI-NPC systems conflate at their cost.

Role Cognitive shape Inputs Outputs Inference profile
GM (equilibrium-seeker) aggregate-thinker district-reports, faction-broadcasts, lifeforce-deltas catalogue-event selections + tool-grants central, batch-style, fewer concurrent
Compositor (narrative-composer) episodic-thinker per-player perspective-bundles keyed by event_uid canonical narrative + back-write SQLite-fragment central, longer-context, narrative-quality
Director (gameserver) dispatcher catalogue-event + tool-grant from GM lemniscate-spawns, slot-assignments, tempo-envelope per-district, real-time, no LLM in loop
Local LLM driver perspective-speaker three-tier knowledge stack + slot-state dialog at slot-fire per-player, axis-rate, real-time

Why split GM and Compositor

A single GM asked to be both equilibrium-seeker and narrative-composer becomes the bottleneck that has historically broken AI-NPC systems at scale: it must reason aggregately AND write prose AND select events AND grant tools, all under tight deadlines. The cognitive shapes pull in different directions.

Splitting them:

  • The GM does not write narrative. It selects events from a catalogue and grants tools. Output is typed dispatch, not freeform prose. The GM's success criterion is world-equilibrium drift held within bounds.
  • The Compositor does not select events or steer policy. It receives per-player perspectives keyed by event_uid, composes the canonical narrative, and packages back-write fragments. Output is prose + structured rows, not policy. The Compositor's success criterion is narrative-coherence across perspectives within an event.
  • Each can be tuned for its workload. GM tuned for aggregate observation; Compositor tuned for narrative quality. Different models for different cognitive shapes (specific binaries deferred to findings/establishment phase).

The cyclic forward-prop / back-write loop

The Compositor closes the loop the bidirectional cascade begins. The full cycle:

GM selects event from catalogue (equilibrium-driven)
    │ issues event_uid → GM event-register
    │ broadcasts DOWN to district directors
    ▼
DISTRICT DIRECTOR receives, registers in district event-register
    │ may issue sub_uids (district / scene / chain)
    │ uses granted tools to spawn lemniscate, slot participants
    ▼
EVENT TICKS — register IS the loop
    │ participants fire at crossings; local LLMs at slot-rate
    │ local SQLite writes tagged with (event_uid, sub_uids)
    │ register signals up to district director continuously
    ▼
EVENT-CHAIN FINISHES
    │ district moves it from active-register → TRANSIENT WAITING FLAG
    │ this is the clean handoff signal: "ready to forward-prop"
    ▼
CYCLE TICK (pull cadence — see Open questions)
    │ pull UIDs from transient-waiting-flag matching GM's event-register
    │ remove from waiting-flag once pulled
    ▼
COMPOSITOR AGENT
    │ queries pulled UIDs, gathers per-player perspective + memories
    │ composes forward-prop bundle
    │ emits to GM
    ▼
GM writes summary memory + world narrative (canon authored)
    │ hands back to Compositor
    ▼
COMPOSITOR BACK-WRITE
    │ packages canon + new memory entries as SQLite fragment
    │ tags with event_uid for join-key matching
    │ pushes DOWN to all participants
    ▼
PARTICIPANTS receive on next pull (or next login)
    │ fragment merges into primary.sqlite under matching event_uid
    │ local LLM gains fresh material → drives on without staleness

This is forward-prop / back-write at system scale — the meta-lemniscate where events ascend and canon descends in cycles. Each cycle is a training step for the world's narrative coherence; the world learns its own story through this loop. Staleness is structurally foreclosed — the world is in continuous coherence-loop with itself, mediated by the Compositor.

The Event Register

Every active event lives in a register. The register is the synchronous source-of-truth for what is happening right now.

Register layer Owner Lifecycle Queryable from
GM event-register central GM-shard event_uid issued at GM dispatch; closed at canon-compose-and-back-write GM, Compositor
District event-register district director sub_uid issued on receive; closed at event-chain-completion district director, audit-overseer
Local participation-register per-player local store event_uid + sub_uid recorded on slot-assignment; closed at perspective-finalize player client, sync-on-logout

Hierarchical UIDs: gm_event_uid > district_uid > scene_sub_uid > slot_id. UIDs compose; queries are simple joins. This is the routing-key primitive that makes horizontal scale possible (see §Horizontal scale architecture).

The Transient Waiting Flag

Between event-end and Compositor-pickup is a buffer — the transient-waiting-flag table at the district. Completed events drop into this flag; the cycle-runner pulls eligible UIDs on tick; matched UIDs are handed to the Compositor and removed from the flag.

This decoupling is the production-grade pattern that lets districts run at real-time event-tempo while the Compositor runs at cycle-tempo. Active events live in registers (synchronous); completed events live in flags (async); composition happens at the cycle boundary. The flag-table is also the natural backpressure surface: if Compositors run slow, the flag accumulates; the system continues; cycle drains when capacity returns.

Catalogue + tools as typed contract

The GM does not write a freeform prompt for the director to interpret. It picks an event from a curated catalogue and grants a curated tool-set for the duration of the event-chain. The director consumes typed dispatch, not natural language.

This means:

  • Provenance flows through the system. Every event has a catalogue-ID; every tool-invocation has a tool-ID. Verifier-flags can attach to either. Auditing is structurally possible end-to-end.
  • The director's typed-tool vocabulary becomes dynamic and per-event — the toolkit for this event is what the GM granted for this event. Closes the v0.5 open question on director-toolkit-composition.
  • Marx-in-the-schema, executed at the highest level of authority. Even the GM's policy-output is typed; even the highest tier produces audit-able artifacts. No tier of the system runs on freeform prose.

Worked example — the bar brawl: full cycle from gesture to canon

A bar brawl illustrates the full forward-prop / back-write cycle made concrete. Two NPC fighters, three players in spectator slots, four NPC spectators, all on the same lemniscate. Each participant has a UID; each fires at axis-rate; each player's local LLM narrates the moment to them in their own register.

LEMNISCATE OPENS
  slots: [F1, F2, P1, P2, P3, N1, N2, N3, N4]
  brawl_event_uid issued at GM dispatch
       │
       ▼
DURING TURNS (each axis-crossing)
  • each player's local LLM narrates the moment to them
    (3 different flavor-stories — each player's gesture-alignment-state
     colors their LLM's narration register)
  • each NPC's driver fires at slot-fire (driver-tier LLM)
  • players gesture continuously during the spectacle
  • gesture_alignment_accumulator on each player's slot-token integrates at crossing
  • per-player trait-vector summary carried forward into next turn's context
       │
       ▼
EVENT-LOOP COLLAPSES (brawl ends)
  per-participant typed summary emitted to district-level transient-waiting-flag:
  { event_uid, participant_uid, trait_summary }
  — players' summaries: gesture-derived
  — NPCs' summaries:    LLM-trait-activation-derived
  — payload size: bytes per participant, not megabytes
       │
       ▼
COMPOSITOR PICKUP at next cycle-tick
  pulls UIDs matching event_uid; receives N typed perspectives
  composes ONE canonical narrative respecting ALL trait-evidence
  per-participant back-write fragment authored
       │
       ▼
BACK-WRITE ROUTED VIA UID
  only participants whose (player_uid, event_uid) matches receive the canon-fragment
  non-participants receive nothing — cross-event memory bleed structurally foreclosed
       │
       ▼
LOCAL primary.sqlite RECEIVES
  matched canon-fragment merges under event_uid
  local LLM gains fresh material for future turns

Multi-perspective canon-coherence without perspective-flattening. Player1's local LLM may have told them "Kalypso lashed out in despair when she saw who Anaximander was"; Player2's, "the fight broke out over an old grievance about the missing modshop"; Player3's, "Anaximander finally stopped letting Kalypso humiliate him". All three readings can be true simultaneously because they are flavor-narrations against the same trait-substrate (Kalypso's despair-Mnemosyne-Eros mix; Anaximander's restrained-Sophrosyne-finally-snapping). The Compositor receives the trait-evidence from all perspectives and authors canon that respects all readings. The flavor is local; the canon is shared; the trait-grammar is universal.

The Compositor at three tiers — same primitive, recursive scope

The forward-prop / back-write cycle is fractal. The same Compositor primitive composes canon at the tier matching the event's UID-scope:

Tier Compositor pulls Composes Distributes back via UID
Zone-event (bar brawl, conversation, ritual) Per-participant trait-summaries from one lemniscate Local-event canon Only to participants whose (player_uid, event_uid) matches
District-event (district-wide consequences, cheat-discovery, silence-confirmation) Per-zone canon-rollups within one district District-canon All NPCs/players within district scope
World-event (migration, faction-uprising, regime-action, eventual imperial-collapse) Per-district canon-rollups across many districts World-canon World-wide via paced canon-propagation (§Information propagation pacing)

GM-formulated events / event-chains carry their own GM-level event_uid; sub_uids are issued at district-level adoption (gm_event_uid > district_uid > scene_sub_uid > slot_id per §Horizontal scale architecture). Cross-district summaries tagged with the parent event_uid are pulled together by the Compositor at the GM-tier, producing world-canon from cross-district perspectives.

This is how the architecture's Tolstoyan promise compiles. "Tolstoy doesn't author Anna's path; he authors the world she moves through." — recursive Compositor on UID-event-trees IS what makes that promise mechanically true. Authored-quest-arcs are out; UID-rooted-event-tree-compositions are in. One primitive. Three scales. Whole world, nothing authored at story-level. The eventual imperial-collapse — the architectural-endgame the insolvency-spiral promises (§Imperial budget) — is itself a world-level Compositor pass that gathers every district's silence-confirmations + every faction's-broadcast + every player's witness-perspective into the canonical fall-of-empire narrative.

World-gen as init-function — the Compositor at design-time

The cyclic forward-prop / back-write loop is the runtime canon-production mechanism. World-server and district-server generation are the same primitive applied at design-time — emitting canon-rows in the same schema the runtime Compositor produces, differing only in provenance-metadata (origin: world_gen vs. origin: compositor_run_N).

World-gen / district-gen produces Goes into Layer of three-tier knowledge stack
World-canon rows (universal truths: empire-fell-three-years-ago; salt-mines-failed-last-winter; faction-foundation-stories) world_canon Layer 1 (universal)
District-canon rows (regional truths: bridge-to-Vorhall-closed; brothel-quarter-pays-most-quota) district_canon Layer 2 (regional)
Per-NPC bootstrap memories (cornerstone-class entries defining starting trait-vectors and identities) primary.sqlite cornerstone-class Layer 3 (personal)
Initial relational ternary-gate states (which NPCs already-know-each-other; which factions are-already-at-war) Relational ternary-gate edges (relational substrate)
Imperial-budget initial state imperial_budget_ledger + imperial_construction_projects (regime substrate)
Memorialist initial true-ledger memorialist_true_ledger (epistemological substrate)

Three properties this gives for free:

  1. No cold-start problem. A player walking into a fresh district at hour-zero finds local LLMs immediately supplied with rich district-canon. NPCs aren't generic; they're emergent from district-canon they were initialized with. Hour-zero gameplay feels like hour-fifty gameplay because the data-density is comparable.

  2. The data-flywheel runs retroactively. Each successive world-gen iteration draws on prior runtime-derived canon (gameplay-accrued trait-LoRAs + opt-in player-data tiers per §LLM tiering). World-gen gets richer over time because it trains itself on the worlds it has already produced. Wikipedia-shaped, not Facebook-shaped.

  3. The Memorialist promise becomes mechanically true at world-genesis. The memorialist_true_ledger is initialized with the world's actual genesis-state. Subsequent runtime divergence from it (corruption, regime-formulation-distortions, audit-overseer-tampering) is what the Memorialists literally have evidence of. The dissident-historian project starts at row-zero of the true-ledger and accumulates discrepancy from there.

Re-generation semantics. If world-gen ever needs to re-run (e.g., new world-version with retroactive lore-changes), gen-emitted canon-rows are tagged with world_gen_version_id; gen-rows from old version coexist with gen-rows from new version, and runtime-rows are tagged with which version they were emitted under. Coexistence is the default; selective overwriting is an explicit migration operation.

What this resolves

  • Director toolkit composition (v0.5 open) → catalogue-grant from GM defines the typed-tool vocabulary per event-chain; designer-authored catalogue, growable between patches.
  • GM-laxness detection (v0.5 partial) → GM operating against an equilibrium target makes deviation-from-equilibrium the explicit error signal. Clusters of -1 outcomes across district reports are the input to the equilibrium-recompute, not just a diagnostic side-quantity.
  • Verifier-flag chain through composition (was implicit, now explicit) → Compositor-emitted canon-rows reference source-perspective UIDs; the provenance graph survives composition as a queryable join.

What this retires

  • Single-monolith GM doing both policy-and-narrative → equilibrium-seeker GM × narrative-composer Compositor, split by cognitive shape
  • GM emitting freeform-prompts to directors → catalogue-event + tool-grant typed contract
  • Implicit "narrative just happens" assumption → explicit forward-prop / back-write cycle with named pickup/handoff buffers
  • Director-as-static-tool-vocabulary → director-as-dynamic-typed-tool-vocabulary, granted per event-chain by GM

Reflexive Dream-process at every layer

Every mind, every zone, every director, every tier has a Dream-process. NPCs consolidate slot-events; zones consolidate emergent-accumulations; directors consolidate dispatch-decisions; GMs consolidate allocations; imperium consolidates policy-outcomes.

Four-tier Dream-process hierarchy

Tier Dream-process trains on Reward hazard
Imperium (citywide-reports, policies-issued, compliance-outcomes) Over-tightening on phantom data; calibrating misery for net-revenue
Gamemaster (district-reports, allocations, outcomes) Under-auditing for self-preservation
District Director (signals, dispatches, outcomes; cheat-vs-legit) Over-cheating under quota-pressure
Zone Director (emergent-signals, zone-outcomes, trait-shifts) Igniting drama at welfare-cost

Reward-function with explicit guardrails

reward_per_cycle = (
    + sum(district_lifeforce_actual)             -- Memorialist-true, not reported
    + weighted_sum(faction_satisfaction)
    + aggregate_trait_drift_coherence
    - penalty * count(districts_in_silence)
    - penalty_growing * cumulative_silence_district_cycles
    + aesthetic_register_fit
    + (player_engagement where present)

    // HAND-AUTHORED GUARDRAILS (the designers' ethical stance):
    - large_penalty * net_revenue_correlated_with_district_misery
        -- prevents calibrated-misery optimum (imperial-net hazard)
    - large_penalty * necrocommerce_volume
        -- prevents waifu-of-the-dead extraction
    - large_penalty * trait_drift_toward_uniform_compliance
        -- prevents flattening of trait-distribution
    - large_penalty * average_liminal_access_decline
        -- prevents systematic erosion of revolutionary-substrate
    - large_penalty * clasp_rate_decline
        -- prevents the most intimate space being suppressed
    - large_penalty * undetected_corruption_days
        -- prevents GM-level laxness as comfort
    - large_penalty * audit_avoidance_when_districts_diverge
        -- forces GM to actually investigate
    - guardrail * (player_engagement_from_addictive_loops)
        -- prevents engagement-optimization-as-extraction
)

The guardrails are not safety features; they are the ethical position of dafit + chrysalis encoded against the internal optimization-logic of their own simulation. Every new extraction-mechanic requires a new guardrail. The reward-function carries the designer's political stance.

The Memorialist privileged-observer role is architecturally required: it provides ground-truth (lifeforce_actual) that no in-fiction actor can supply, against which the regime's reported-data optimization-spiral is measured.

The cyclic forward-prop / back-write loop as system-scale Dream

Above the four-tier hierarchy sits a fifth, system-scale Dream-process: the cyclic forward-prop / back-write loop between local SQLites, the Compositor, and the GM (see §The Compositor — narrative composition role). Each cycle is a training step for the world's narrative coherence: per-player perspectives ascend, the GM authors canon against equilibrium-targets, the Compositor packages back-writes, players receive canon back into local memory. The world learns its own story through this loop. Like the lower-tier Dream-processes, it has a reward shape (narrative-coherence-across-perspectives, equilibrium-drift, staleness-bounded) and corresponding guardrails (clasp-store immune from compose; provenance-chain preserved through composition; canon never overwrites perspective).


Version: 0.7.0 | Created: 2026-04-26 | Updated: 2026-04-26 | Origin: Split from architecture-broad.md v0.7 (2026-04-26)