# God Mode + World State — Minimal v1 Design Specification **Date:** 2026-04-20 **Builds on:** `2026-03-26-narrative-layer-design.md` (sections 5–6) **Target:** Author-controlled story intervention + world grounding **Scope:** 3 God Mode actions + 3 World State features, file-based only --- ## 1. Problem The Narrative Layer generates prose from OASIS simulation output, but the author has no way to intervene mid-story or ground scenes in a coherent world. This is a creative platform, not a passive spectator tool — authors need levers. **v1 goal:** Ship the three highest-leverage intervention levers and the three highest-leverage world grounding primitives, without modifying the OASIS simulation engine. --- ## 2. Scope ### In scope **God Mode interventions (3):** 1. **Inject world event** — append a user-described event to the world event log; subsequent prose generations reference it 2. **Modify character emotional state** — directly set emotion values in `characters.json` 3. **Kill character** — mark a character as `status: "dead"`; filtered from future translations **World State (3):** 1. **Locations** — named places with descriptions; characters can have a current `location` 2. **World rules** — list of background rules / constraints (genre, laws, era) 3. **Event log** — chronological record of world-level events, populated automatically by God Mode event injections ### Explicitly out of scope (deferred to v2+) - Agent-side event propagation (the OASIS simulation doesn't "see" injected events — only the prose layer does) - World rule changes mid-simulation via OASIS prompt injection - Factions, resources, timeline branching - Force action, resurrect character, time skip ### Why file-based only The existing OASIS subprocess has no `INJECT_CONTEXT` IPC command. Adding one requires touching the OASIS runner scripts, which introduces simulation coupling we don't need for creative storytelling. v1 God Mode is a **story-layer intervention system** — it shapes what the author reads, not what agents do. The author injects an earthquake; the next prose paragraph starts "A tremor ran through the city…" The agents keep posting about whatever they were already posting about. This is a feature, not a bug: it means authors can introduce "unreliable narrator" events that exist only in the prose. --- ## 3. Architecture ### 3.1 New files **Backend services:** | File | Responsibility | |---|---| | `backend/app/services/narrative/world_state.py` | CRUD for world_state.json (locations, rules, event log) | | `backend/app/services/narrative/god_mode.py` | 3 intervention handlers; writes to world_state and characters | | `backend/tests/test_world_state.py` | Unit tests for world CRUD | | `backend/tests/test_god_mode.py` | Unit tests for interventions | **Backend API (modifies existing):** | File | Change | |---|---| | `backend/app/api/narrative.py` | +6 endpoints (3 world, 3 god mode) | **Backend translator (modifies existing):** | File | Change | |---|---| | `backend/app/services/narrative/narrative_translator.py` | Extend prompt to surface world rules, recent events, character locations; filter dead characters | **Frontend:** | File | Responsibility | |---|---| | `frontend/src/api/narrative.js` | +6 named exports for new endpoints | | `frontend/src/views/WorldBuilderView.vue` | Locations + rules + event log UI | | `frontend/src/views/GodModeView.vue` | 3 action forms | | `frontend/src/router/index.js` | +2 routes | ### 3.2 Data model New file per simulation: `uploads/simulations/{sim_id}/narrative/world_state.json` ```json { "sim_id": "sim_abc123", "rules": [ "The kingdom is in civil war", "Magic is feared but not forbidden", "Winter will arrive in 10 rounds" ], "locations": { "iron_tower": { "id": "iron_tower", "name": "The Iron Tower", "description": "A brutal spire of black stone at the city's heart." }, "market": { "id": "market", "name": "The Old Market", "description": "Narrow stalls under fraying canvas. Always crowded." } }, "event_log": [ { "id": "evt_001", "round": 3, "type": "god_mode_injection", "description": "A stranger arrived at the market, carrying a sealed letter.", "injected_at": "2026-04-20T14:22:00Z" } ] } ``` **Character schema extension** (`characters.json`) — adds two optional fields: ```json { "id": "1", "name": "Elena", "emotional_state": {...}, "status": "alive", // NEW — "alive" | "dead" "location": "iron_tower" // NEW — optional, references world_state.locations } ``` Existing characters without these fields default to `status: "alive"` and no location — backward compatible. ### 3.3 API endpoints All under `/api/narrative/*` prefix (reusing existing `narrative_bp`). | Method | Path | Purpose | |---|---|---| | `GET` | `/world/` | Return full world_state.json | | `POST` | `/world//rules` | Set world rules (body: `{rules: [string]}`) | | `POST` | `/world//locations` | Upsert a location (body: `{id, name, description}`) | | `POST` | `/godmode//inject-event` | Inject a world event (body: `{description, round?}`) | | `POST` | `/godmode//modify-emotion` | Set emotion values (body: `{character_id, emotions: {anger: 0.8, ...}}`) | | `POST` | `/godmode//kill` | Mark character dead (body: `{character_id}`) | --- ## 4. God Mode handlers ### 4.1 Inject event ```python def inject_event(sim_dir: str, description: str, round_num: int | None = None) -> dict: """Append a new event to world_state.event_log. If round_num is None, uses current round (last translated beat's round + 1, or 1 if no beats yet). """ ``` - Generates unique `id = evt_{n}` where n is `len(event_log) + 1` - Returns the event dict ### 4.2 Modify emotion ```python def modify_emotion(sim_dir: str, character_id: str, emotions: dict[str, float]) -> dict: """Overwrite specified emotion values for a character. Clamps to [0.0, 1.0]. Only modifies emotions named in the input dict; unspecified emotions keep their current values. Unknown emotion keys are silently ignored. """ ``` - Reads `characters.json`, finds character by `id`, applies overwrites - Raises `ValueError("character not found")` if `character_id` doesn't match - **Audit logging:** appends a `{"type": "god_mode_emotion_change", ...}` entry to `world_state.event_log` so the intervention is visible in the world event log UI ### 4.3 Kill character ```python def kill_character(sim_dir: str, character_id: str) -> dict: """Mark character as dead. Future translations ignore their actions.""" ``` - Sets `status = "dead"` on the target character - Translation pipeline filters dead characters from the characters list passed to LLM prompt, and ignores any actions with matching agent names - **Auto-appends a death event** to `world_state.event_log`: `{"type": "god_mode_death", "description": "{name} has died.", "round": current_round}` where `current_round` is defined as "last translated beat's round + 1, or 1 if no beats yet" (same rule `inject_event` uses). This guarantees the LLM knows the character is gone rather than silently omitting them — preventing "character vanishes mid-story" narrative bugs. - Raises `ValueError("character not found")` if `character_id` doesn't match ### 4.4 Concurrency note God Mode writes to `characters.json` while background translation may also be reading/writing it. This is a classic read-modify-write race: if `translate_round` is mid-LLM-call when the user POSTs `/kill`, the subsequent save may overwrite the kill. **For v1 (single-user, 10–50 user scale), this is an accepted limitation, not a bug.** Authors are expected to intervene between rounds, not during. This constraint is documented in §10 non-goals. File-locking can be added in a follow-up if needed. --- ## 5. Translator prompt extension ### 5.0 Integration — how the translator loads world state `translate_round` is extended to load `world_state.json` internally via a new `WorldStateStore.load(sim_dir)` (parallel to how it uses `StoryStore.load_characters()` today). If the file doesn't exist, load returns an empty world (`{"rules": [], "locations": {}, "event_log": []}`) — existing simulations without a world state continue to work unchanged. **Event round semantics:** every event in `event_log` has a `round` field, but this is **metadata only** (used for UI display and prompt formatting as `"(Round N)"`). `event_log` is append-only — the last element in the list is always the newest. The translator always surfaces the *last 3 events by insertion order*, regardless of their round. `event.round` is never used as a filter. **Lookup key discipline:** characters are keyed by both `id` (stable string like `"1"`) and `name` (display string like `"Elena"`). The canonical conventions are: - God Mode API endpoints always take `character_id` (the `id` field) in request bodies. - Internal handlers resolve `character_id → name` once and use `name` downstream for alive/dead filtering and action-log matching (since `actions.jsonl` stores `agent_name`, not id). - Location `id` (`"iron_tower"`) is stored on characters as `character.location`. The translator resolves `id → location.name` once when building the prompt — characters in the prose prompt read as "Elena (at The Iron Tower, feeling: anger=0.3, trust=0.1)". Current prompt has substitution fields: `{tone}`, `{characters}`, `{actions}`, `{previous}`. Add 3 new fields: `{world_rules}`, `{world_events}`, `{world_locations}`. **Prompt injection safety:** user-supplied strings (rules, event descriptions, location names/descriptions) are free-text and may contain `{` or `}`. Before rendering with `str.format()`, these strings are escaped: `{` → `{{`, `}` → `}}`. This prevents `KeyError` from stray braces in author content. **New prompt sections inserted before "Events this round":** ``` World grounding: Rules: {world_rules} Recent events: {world_events} Known locations: {world_locations} ``` - `{world_rules}` is a bulleted list joined with `; ` - `{world_events}` is the last 3 events from `event_log`, each as `"(Round N) description"` - `{world_locations}` is a `name — description` list, capped at 5 The prompt also gets one new instruction in the CINEMATIC DETAIL section: ``` - If a character has a known location, root the scene there. - If a recent world event is listed, weave it in OR acknowledge its aftermath — do not ignore it. ``` ### 5.1 Filtering dead characters In `translate_round`, before building `characters` and `actions` lists: ```python alive = {c["name"] for c in characters if c.get("status", "alive") != "dead"} characters = [c for c in characters if c["name"] in alive] actions = [a for a in actions if a.get("agent_name") in alive] ``` Dead characters are invisible to the LLM — no prose is generated about them unless the event log contains their death (which is auto-populated by `kill_character` — see §4.3). The alive-filter applies only to the roster and action list passed into the prompt; there is no separate "observing" inference in the committed code today, so no other code paths need adjustment. **Backward compatibility:** `create_initial_character()` is updated to set `status: "alive"` on every new character. Existing saved characters without a `status` field are treated as alive via the `c.get("status", "alive")` default. --- ## 6. Frontend ### 6.1 GodModeView.vue Three card-style forms, visually distinct: ``` ┌──────────────────────────────────┐ │ ⚡ Inject World Event │ │ [textarea: event description] │ │ [input: round (optional)] │ │ [Inject button] │ │ Last 3 events shown below │ └──────────────────────────────────┘ ┌──────────────────────────────────┐ │ 💭 Modify Character Emotions │ │ [select: character dropdown] │ │ [6 sliders: anger/fear/joy/...] │ │ [Apply button] │ └──────────────────────────────────┘ ┌──────────────────────────────────┐ │ ☠ Kill Character │ │ [select: character dropdown] │ │ [Confirm checkbox] [Kill button]│ │ Warning: removes from future │ │ story. Cannot be undone in v1. │ └──────────────────────────────────┘ ``` Uses existing `CharacterCard.vue` for selection preview. ### 6.2 WorldBuilderView.vue Three sections: 1. **Rules** — editable textarea, one rule per line, Save button 2. **Locations** — list of existing locations + "Add location" form 3. **Event log** — read-only scrollable list (for now; events are added only via God Mode) ### 6.3 Routes Added to `frontend/src/router/index.js`: ```javascript { path: '/godmode/:simulationId', name: 'GodMode', component: GodModeView, props: true }, { path: '/world/:simulationId', name: 'World', component: WorldBuilderView, props: true } ``` ### 6.4 Navigation A small cross-view nav is added to **all three views** (`StoryTimelineView`, `GodModeView`, `WorldBuilderView`) as a shared header strip — the links show the current route as active: ``` Story | God Mode | World ``` Implementation: extracted into a small `` component (not a separate file unless it grows — inline in each view is fine for v1) to avoid app-level layout changes. ### 6.5 Kill confirmation UX The spec upgrades the Kill Character form beyond a checkbox: the user must **type the character's name** to enable the Kill button. This matches the GitHub-style pattern for irreversible destructive actions and eliminates misclicks. The input is case-insensitive and whitespace-trimmed. --- ## 7. Error handling - **Missing character**: handler raises `ValueError("character not found")`; API returns 404 with `{"error": "character not found"}` - **Missing simulation**: API returns 404 (reuses existing pattern from Task 7) - **Invalid emotion name**: silently ignored (extra emotion keys don't break anything; they just don't affect state) - **Inject event before translation starts**: event is stored with `round: 0` and included in the first beat's prompt - **Invalid round number on inject_event**: API validates `round` is a non-negative integer (or null); otherwise returns 400 --- ## 8. Testing **Unit tests** (`tests/test_world_state.py`): - `test_world_state_empty_on_fresh_sim` - `test_add_location_persists` - `test_set_rules_replaces_previous` - `test_append_event_auto_ids` **Unit tests** (`tests/test_god_mode.py`): - `test_inject_event_appends_to_log` - `test_modify_emotion_clamps_and_preserves_others` - `test_kill_character_sets_status` - `test_kill_character_not_found_raises` **Integration** (extend `test_narrative_e2e.py`): - Inject an event between round 1 and round 2, verify the event appears in the round-2 prompt context (assert via `mock_llm.call_args[0][0]`) - Kill a character between rounds, verify their actions are skipped in the next translation - Set world rules + locations, verify they appear in the prompt **Prompt-inclusion unit test** (in `test_narrative_translator.py`): - Patch `call_llm`, call `translate_round` with a populated world_state, assert the captured prompt contains each rule, each event description, and each location name. This prevents silent regressions in §5 formatting. --- ## 9. User contribution points Same learning-mode pattern as before — two places the author's creative judgment matters more than an LLM's guess. ### 9.1 Event injection prompt enforcement When God Mode injects an event, the prose prompt will include it in "Recent events." The prompt currently says "weave it in OR acknowledge its aftermath — do not ignore it." User decides **how strong** this instruction should be: - **Soft**: "consider referencing the event if it fits" - **Medium**: current default — "weave it in OR acknowledge its aftermath" - **Hard**: "the opening line of this passage MUST reference the most recent world event" Marked as `EVENT_ENFORCEMENT_STRENGTH` module-level constant in `narrative_translator.py`. Note: this is a **deployment-global** setting in v1, not per-simulation. Per-sim overrides are a v2 concern (would move this to `world_state.json`). ### 9.2 Location schema Whether locations need fields beyond `name + description`. Options: - **Minimal** (default): just `name + description` - **Cinematic**: add `atmosphere` (a mood phrase like "oppressive silence, dust motes in shafts of light") - **Temporal**: add `time_of_day` ("dusk", "midnight") - **Full**: all of the above Marked clearly in `world_state.py` as scaffolded default, with a TODO comment pointing to user contribution. --- ## 10. Non-goals - Mid-simulation OASIS prompt injection (deferred to v2) - Resurrection, time skip, forced action - Faction system, resource system - Location transitions (characters moving between locations during a round) — v1 locations are static per-character - Bulk intervention (applying to multiple characters at once) - Undo / history — God Mode actions are one-way in v1 - **No delete endpoints** for locations, rules (they're replaced in bulk via the POST endpoint), or events. An author who mis-types must manually edit `world_state.json`. - **Concurrent write safety** — authors should intervene between rounds, not during (see §4.4). No file locking in v1. - **Event log rotation** — `event_log` grows unbounded. Fine at expected v1 scale (tens of events per sim); pruning can be added in v2. - **Per-simulation event enforcement strength** — `EVENT_ENFORCEMENT_STRENGTH` is deployment-global in v1 (§9.1).