MicroFish/docs/superpowers/specs/2026-04-20-godmode-worldsta...

374 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# God Mode + World State — Minimal v1 Design Specification
**Date:** 2026-04-20
**Builds on:** `2026-03-26-narrative-layer-design.md` (sections 56)
**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/<sim_id>` | Return full world_state.json |
| `POST` | `/world/<sim_id>/rules` | Set world rules (body: `{rules: [string]}`) |
| `POST` | `/world/<sim_id>/locations` | Upsert a location (body: `{id, name, description}`) |
| `POST` | `/godmode/<sim_id>/inject-event` | Inject a world event (body: `{description, round?}`) |
| `POST` | `/godmode/<sim_id>/modify-emotion` | Set emotion values (body: `{character_id, emotions: {anger: 0.8, ...}}`) |
| `POST` | `/godmode/<sim_id>/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, 1050 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 `<SimNav :simId="simId" />` 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).