21 KiB
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-index.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-index.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 in scale-and-transport/architecture.md).
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 — see inference-and-memory/architecture.md) |
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 scale-and-transport/architecture.md §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 — see political-register/architecture.md) — 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:
-
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.
-
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 inference-and-memory/architecture.md §LLM tiering). World-gen gets richer over time because it trains itself on the worlds it has already produced. Wikipedia-shaped, not Facebook-shaped.
-
The Memorialist promise becomes mechanically true at world-genesis. The
memorialist_true_ledgeris 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-index.md v0.7 (2026-04-26)