You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
mirosociety/docs/plans/2026-03-17-mirosociety-impl...

1174 lines
44 KiB

# MiroSociety — Implementation Plan
> **For Claude / Cursor:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build a multi-agent society simulation engine where users define social rules and watch emergent behavior unfold.
**Architecture:** FastAPI backend with custom simulation engine (tension engine, reactive micro-rounds, tiered memory), Vue 3 + D3.js frontend with progressive streaming, SQLite state storage, OpenAI-compatible LLM for agent reasoning and narration.
**Tech Stack:** Python 3.11+ (FastAPI, Pydantic, aiosqlite, openai), Vue 3 (Composition API, Vite, D3.js, TailwindCSS), Docker
---
## Task 1: Project Scaffold
**Files:**
- Create: `backend/pyproject.toml`
- Create: `backend/app/__init__.py`
- Create: `backend/app/main.py`
- Create: `backend/app/config.py`
- Create: `frontend/package.json`
- Create: `frontend/vite.config.js`
- Create: `frontend/tailwind.config.js`
- Create: `frontend/postcss.config.js`
- Create: `frontend/index.html`
- Create: `frontend/src/main.js`
- Create: `frontend/src/App.vue`
- Create: `frontend/src/router/index.js`
- Create: `.env.example`
- Create: `docker-compose.yml`
- Create: `Dockerfile`
- Create: `.gitignore`
**Step 1: Create backend scaffold**
`backend/pyproject.toml`:
```toml
[project]
name = "mirosociety"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"openai>=1.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
"aiosqlite>=0.20.0",
"python-dotenv>=1.0.0",
"sse-starlette>=2.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```
`backend/app/config.py`:
```python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
llm_api_key: str = ""
llm_base_url: str = "https://api.openai.com/v1"
llm_model: str = "gpt-4o-mini"
database_dir: str = "data"
host: str = "0.0.0.0"
port: int = 8000
max_concurrent_llm_calls: int = 10
class Config:
env_file = ".env"
settings = Settings()
```
`backend/app/main.py`:
```python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="MiroSociety", version="0.1.0")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
@app.get("/health")
async def health():
return {"status": "ok"}
```
**Step 2: Create frontend scaffold**
Standard Vue 3 + Vite + TailwindCSS setup with vue-router. Single `App.vue` with `<router-view>`. Routes: `/` (home), `/simulation/:id` (sim view — handles both generation and running), `/report/:id` (report), `/gallery` (community gallery).
**Step 3: Create Docker and env files**
`docker-compose.yml` with single service (port 8000). Multi-stage Dockerfile.
`.env.example`:
```
LLM_API_KEY=your-api-key
LLM_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o-mini
```
**Step 4: Init git repo**
```bash
cd /Users/yashgupta/Desktop/opensource/mirosociety
git init
git add .
git commit -m "feat: initial project scaffold"
```
---
## Task 2: Data Models
**Files:**
- Create: `backend/app/models/__init__.py`
- Create: `backend/app/models/world.py`
- Create: `backend/app/models/agent.py`
- Create: `backend/app/models/action.py`
- Create: `backend/app/models/simulation.py`
**Step 1: Define world models**
`backend/app/models/world.py` — Pydantic models:
- `Location(id, name, type: Literal["public", "commerce", "governance", "residential", "social"], description)`
- `TimeConfig(total_days, rounds_per_day: int = 3, active_agents_per_round_min, active_agents_per_round_max)`
- `WorldBlueprint(name, description, rules: list[str], locations: list[Location], resources: list[str], initial_tensions: list[str], time_config: TimeConfig)`
- `WorldMetrics(stability: float, prosperity: float, trust: float, freedom: float, conflict: float)` — all 0.0-1.0
- `Institution(name, purpose, founder_id, member_ids: list[int], rules: list[str], created_day: int)`
- `Proposal(id, proposer_id, content, votes_for: list[int], votes_against: list[int], status: Literal["open", "passed", "rejected"], created_round: int)`
- `WorldState(blueprint, metrics, institutions: list[Institution], proposals: list[Proposal], day, round_in_day, active_disputes: list[str], community_rules: list[str])`
**Step 2: Define agent models with tiered memory**
`backend/app/models/agent.py` — Pydantic models:
- `Personality(honesty, ambition, empathy, confrontational, conformity)` — all floats 0-1
- `AgentPersona(id, name, role, age, personality: Personality, background, goals: list[str], core_memory: list[str], working_memory: list[str], beliefs: list[str], relationships: dict[str, str], resources: dict[str, int], location, faction: str | None, emotional_state: str)`
Note: `relationships` is `dict[str, str]` where key = agent_id as string, value = description of relationship. Using string keys for JSON compatibility.
**Step 3: Define action models with internal thought**
`backend/app/models/action.py`:
- `ActionType(Enum)`: SPEAK_PUBLIC, SPEAK_PRIVATE, TRADE, FORM_GROUP, PROPOSE_RULE, VOTE, PROTEST, COMPLY, DEFECT, BUILD, MOVE, OBSERVE, DO_NOTHING
- `AgentDecision(feel, want, fear, action: ActionType, args: dict, speech: str | None, internal_thought: str, belief_updates: list[str], memory_promotion: str | None)`
- `ActionEntry(round, day, time_of_day, agent_id, agent_name, location, action_type: ActionType, action_args: dict, speech: str | None, internal_thought: str | None, targets: list[int], world_state_changes: dict, relationship_changes: dict)`
- `ReactiveResponse(agent_id, reaction_type: Literal["respond", "whisper", "silent"], content: str | None, target_id: int | None)`
**Step 4: Define simulation models**
`backend/app/models/simulation.py`:
- `SimulationStatus(Enum)`: GENERATING_WORLD, GENERATING_CITIZENS, RUNNING, PAUSED, COMPLETED, ERROR
- `SpeedMode(Enum)`: LIVE, FAST_FORWARD, JUMP
- `SimulationState(id, status, speed_mode, world_blueprint: WorldBlueprint | None, agents_generated: int, agents_total: int, current_day, current_round, total_rounds, action_count, created_at)`
- `SSEEvent(type: Literal["world_ready", "citizen_generated", "round_complete", "narrative", "metrics_update", "injection_result", "simulation_complete", "error"], data: dict)`
- `ForkRequest(source_simulation_id, fork_at_day, changes: str)`
**Step 5: Commit**
```bash
git add backend/app/models/
git commit -m "feat: add core data models with tiered memory and reactive actions"
```
---
## Task 3: Database Layer
**Files:**
- Create: `backend/app/db/__init__.py`
- Create: `backend/app/db/store.py`
**Step 1: Build async SQLite store**
Each simulation gets its own directory: `data/simulations/{sim_id}/`
- `simulation.db` — all tables for this simulation
- `actions.jsonl` — append-only action log (for fast streaming reads)
Tables in `simulation.db`:
- `meta(key TEXT PRIMARY KEY, value JSON)` — simulation config, status, blueprint
- `agents(agent_id INT PRIMARY KEY, state JSON, updated_at TEXT)`
- `world_state(round INT PRIMARY KEY, state JSON)`
- `actions(id INTEGER PRIMARY KEY AUTOINCREMENT, round INT, day INT, entry JSON)`
- `narratives(round INT PRIMARY KEY, day INT, time_of_day TEXT, text TEXT)`
- `metrics_history(round INT PRIMARY KEY, metrics JSON)`
Global database `data/global.db`:
- `simulations(id TEXT PRIMARY KEY, status TEXT, rules_text TEXT, world_name TEXT, agent_count INT, created_at TEXT, is_public BOOL, view_count INT, fork_count INT, forked_from TEXT)`
Methods:
```python
class SimulationStore:
async def create(sim_id, rules_text) -> None
async def get_meta(sim_id) -> dict
async def set_meta(sim_id, key, value) -> None
async def save_agent(sim_id, agent: AgentPersona) -> None
async def save_agents_batch(sim_id, agents: list[AgentPersona]) -> None
async def get_agent(sim_id, agent_id) -> AgentPersona
async def get_all_agents(sim_id) -> list[AgentPersona]
async def save_world_state(sim_id, round_num, state: WorldState) -> None
async def get_world_state(sim_id, round_num: int | None = None) -> WorldState
async def save_action(sim_id, entry: ActionEntry) -> None
async def save_narrative(sim_id, round_num, day, time_of_day, text) -> None
async def save_metrics(sim_id, round_num, metrics: WorldMetrics) -> None
async def get_actions(sim_id, from_round, to_round) -> list[ActionEntry]
async def get_narratives(sim_id, from_day, to_day) -> list[dict]
async def get_metrics_history(sim_id) -> list[dict]
async def list_simulations(public_only=False) -> list[dict]
async def copy_state_at_round(source_sim_id, target_sim_id, round_num) -> None
```
**Step 2: Commit**
```bash
git add backend/app/db/
git commit -m "feat: add async SQLite storage with per-simulation databases"
```
---
## Task 4: LLM Client
**Files:**
- Create: `backend/app/services/__init__.py`
- Create: `backend/app/services/llm.py`
**Step 1: Build LLM client wrapper**
```python
class LLMClient:
def __init__(self, api_key, base_url, model, max_concurrent):
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
self.model = model
self.semaphore = asyncio.Semaphore(max_concurrent)
self.total_tokens = 0
self.total_calls = 0
async def generate(self, system: str, user: str, json_mode: bool = False, max_tokens: int = 1000) -> str:
"""Single LLM call with semaphore, retries (3x exponential backoff), JSON mode."""
async def generate_batch(self, prompts: list[tuple[str, str]], json_mode: bool = False, max_tokens: int = 1000) -> list[str]:
"""Concurrent LLM calls via asyncio.gather, respecting semaphore."""
async def generate_light(self, system: str, user: str, max_tokens: int = 100) -> str:
"""Lightweight call for reactive micro-rounds. Low max_tokens, no JSON mode."""
def get_stats(self) -> dict:
"""Return total_tokens, total_calls, estimated_cost."""
```
JSON parsing: try `json.loads()`, if fails try to extract JSON from markdown fences, if fails try to repair truncated JSON (close open braces/brackets), if still fails log error and return a safe default.
**Step 2: Commit**
```bash
git add backend/app/services/llm.py
git commit -m "feat: add async LLM client with batching and lightweight mode"
```
---
## Task 5: World Generator
**Files:**
- Create: `backend/app/services/world_generator.py`
**Step 1: Build world generator**
```python
class WorldGenerator:
def __init__(self, llm: LLMClient): ...
async def generate(self, rules_text: str, population: int = 25, duration_days: int = 365) -> WorldBlueprint:
```
System prompt:
```
You are a world-builder for a society simulation engine.
Given the user's rules for a society, design a small town that will be
interesting to simulate. Your job is to create CONFLICT POTENTIAL —
the world should have built-in tensions that make the rules interact
with human nature in surprising ways.
Generate a JSON world blueprint with:
- name: A evocative name for this society (2-3 words max)
- description: One sentence capturing the essence
- rules: The user's rules as a clean list (clarify ambiguities, add
implied consequences)
- locations: 5-8 locations. Mix of:
- 1-2 public gathering spaces
- 1-2 commerce/trade locations
- 1 governance/authority location
- 1-2 residential/private spaces
- 1 social/leisure space
Each location should create different social dynamics.
- resources: 4-6 resource types that matter in this society. Always
include "influence" and "knowledge". Others depend on the rules.
- initial_tensions: 2-4 inherent conflicts created by the rules.
Think: who benefits from these rules? Who suffers? What loopholes exist?
What happens when the rules conflict with basic human needs?
- time_config: { total_days: {duration}, rounds_per_day: 3,
active_agents_per_round_min: 3, active_agents_per_round_max: {population * 0.4} }
```
**Step 2: Commit**
```bash
git add backend/app/services/world_generator.py
git commit -m "feat: add world generator with conflict-oriented prompting"
```
---
## Task 6: Citizen Generator
**Files:**
- Create: `backend/app/services/citizen_generator.py`
**Step 1: Build citizen generator (streaming)**
```python
class CitizenGenerator:
def __init__(self, llm: LLMClient): ...
async def generate(self, blueprint: WorldBlueprint, count: int = 25,
on_citizen: Callable = None) -> list[AgentPersona]:
"""
Generates citizens in two phases, streaming each one via on_citizen callback.
Phase 1: Cast sheet (single LLM call)
Generate a list of count citizens with: name, role, age, one-line personality
hook, stance on each initial_tension (for/against/indifferent/exploiter).
Ensure diversity: age range 18-75, mix of genders, roles that cover
all locations, personality spread, at least 2 citizens per tension stance.
Phase 2: Full personas (batched, 5 at a time)
For each citizen, generate full persona. As each completes, call
on_citizen(agent) to stream to frontend.
"""
async def generate_relationships(self, blueprint: WorldBlueprint,
agents: list[AgentPersona]) -> list[AgentPersona]:
"""
Single LLM call: given all citizen summaries, generate initial
relationship graph. Output: list of {agent_id, relationships: {target_id: description}}.
Ensure: at least 2 relationships per citizen, mix of positive/negative/neutral,
some family ties, some professional ties, some rivalries.
"""
```
Key prompt constraint for persona generation:
```
CRITICAL: Citizens must have DIVERSE stances on the society's rules.
For each initial tension, generate citizens across the spectrum:
- True believers who love the rules
- Pragmatists who comply but have reservations
- Quiet dissenters who obey but resent it
- Active resisters who will test boundaries
- Exploiters who find loopholes
- The indifferent who just want to be left alone
This diversity drives the simulation. A town of reasonable moderates is boring.
A town with extremists, idealists, cynics, and opportunists produces stories.
```
**Step 2: Commit**
```bash
git add backend/app/services/citizen_generator.py
git commit -m "feat: add streaming citizen generator with diversity constraints"
```
---
## Task 7: Simulation Engine (Core)
**Files:**
- Create: `backend/app/services/engine.py`
- Create: `backend/app/services/tension.py`
- Create: `backend/app/services/resolver.py`
**Step 1: Build tension engine**
`backend/app/services/tension.py`:
```python
class TensionEngine:
def __init__(self, llm: LLMClient): ...
async def check_and_apply(self, world_state: WorldState, agents: list[AgentPersona],
metrics_history: list[WorldMetrics]) -> tuple[WorldState, list[AgentPersona], str | None]:
"""
Check if society has reached boring equilibrium:
- stability > 0.85 and conflict < 0.15 for 3+ consecutive rounds
- OR no significant actions (proposals, protests, defections) for 5+ rounds
- OR dominant faction has >60% of agents
If triggered, apply ONE of:
1. Internal pressure: modify 2-3 agent beliefs/emotions
2. External event: generate environmental change via LLM
3. Faction fracture: seed splinter ideology in dominant faction
Returns: updated world_state, updated agents, event_description (or None)
"""
```
**Step 2: Build action resolver**
`backend/app/services/resolver.py`:
```python
class ActionResolver:
def resolve(self, decisions: list[tuple[AgentPersona, AgentDecision]],
world_state: WorldState, all_agents: list[AgentPersona]) -> tuple[list[ActionEntry], WorldState, list[AgentPersona]]:
"""
Process decisions in order of influence_weight:
SPEAK_PUBLIC/PRIVATE: Always succeeds. Records speech.
TRADE: Succeeds only if both parties have sufficient resources.
FORM_GROUP: Creates institution or adds member. Updates faction.
PROPOSE_RULE: Creates open Proposal in world state.
VOTE: Adds vote to active Proposal. If quorum reached, resolve.
PROTEST: Decreases stability, increases conflict. May trigger
reactive responses from authority figures.
COMPLY: Increases stability. May decrease agent's emotional wellbeing.
DEFECT: Roll detection check based on location privacy and nearby
agents' observation scores. If detected, becomes public knowledge.
BUILD: Requires resources. Creates new location or modifies existing.
MOVE: Updates agent location.
OBSERVE: Agent gains information about nearby agents' recent actions.
DO_NOTHING: No effect but recorded (useful for narrator).
Returns: resolved actions, updated world state, updated agents.
"""
def _update_metrics(self, old_metrics: WorldMetrics, actions: list[ActionEntry],
world_state: WorldState) -> WorldMetrics:
"""
Heuristic metric updates with exponential moving average (alpha=0.3):
- stability: +0.02 per comply/vote, -0.05 per protest, -0.08 per defect
- prosperity: +0.01 per trade, -0.02 per build (investment), track total resources
- trust: +0.01 per positive relationship change, -0.02 per negative
- freedom: inversely correlated with len(community_rules) and len(institutions)
- conflict: +0.03 per protest/defect, -0.01 per comply, spikes on proposals
All clamped to [0.0, 1.0].
"""
```
**Step 3: Build main engine**
`backend/app/services/engine.py`:
```python
class SimulationEngine:
def __init__(self, llm: LLMClient, store: SimulationStore,
tension: TensionEngine, resolver: ActionResolver): ...
async def run(self, simulation_id: str, emit: Callable[[SSEEvent], None]):
"""
Main loop. Runs until stopped, paused, or total_rounds reached.
For each round:
1. Load current world state and agents
2. tension_engine.check_and_apply() — inject pressure if equilibrium
3. select_active_agents() — pick subset
4. Concurrent agent_decision() calls for all active agents
5. Reactive micro-rounds for speech acts
6. resolver.resolve() — apply all actions
7. Update agent memories (working_memory window, core_memory promotions)
8. Save state, actions, metrics to store
9. If speed_mode == LIVE or round is significant:
narrator.narrate_round() and save narrative
10. emit SSEEvent with round data
11. If speed_mode == JUMP and not significant, skip to step 1
Significance score = number of (protests + defections + proposals + faction changes
+ relationship sign-flips + rule changes) in this round. Threshold = 1 for JUMP mode.
"""
def select_active_agents(self, world_state: WorldState,
agents: list[AgentPersona], round_num: int) -> list[AgentPersona]:
"""
Base count: random between config min and max.
Boost for:
- Open proposals: all agents with faction or opinion get +0.3 activation chance
- Recent conflict (last 2 rounds): nearby agents get +0.5 activation chance
- Injected event: all agents get +0.4 activation chance for 2 rounds
- Emotional state "restless"/"angry"/"fearful": +0.3 activation chance
Never exceed 60% of population.
Selection: weighted random by (base_chance + boosts + activity_personality_score).
"""
async def agent_decision(self, agent: AgentPersona, world_state: WorldState,
active_agents: list[AgentPersona]) -> AgentDecision:
"""
Build prompt per design doc (feel/want/fear scaffold).
Available actions filtered by location:
- VOTE only if there's an open proposal
- TRADE only if at commerce location or near another agent
- PROPOSE_RULE only at governance location
- BUILD only with sufficient resources
Parse JSON response. Validate action type and args.
Fallback to DO_NOTHING if parse fails.
"""
async def reactive_micro_round(self, speech_actions: list[ActionEntry],
agents: list[AgentPersona],
world_state: WorldState) -> list[ReactiveResponse]:
"""
For each SPEAK_PUBLIC action, identify witnesses (agents at same location).
For each SPEAK_PRIVATE action, only the target witnesses.
Batch lightweight LLM calls:
"You are {name}. You just heard {speaker} say: '{content}'
React: (a) respond publicly (b) whisper to someone (c) stay silent.
Reply with just the letter and an optional one-sentence response."
Parse responses. Return list of ReactiveResponse.
"""
async def inject_event(self, simulation_id: str, event_text: str) -> str:
"""
LLM call: "An event occurs in {world_name}: '{event_text}'.
Describe: 1) What physically happens 2) Which agents are directly affected
3) Any resource/location changes. Return JSON."
Apply changes to world state.
Add event to all affected agents' working memory.
Return narrative description.
"""
async def reflective_memory_pass(self, simulation_id: str):
"""
Called every 30 simulated days.
For each agent, LLM call:
"You are {name}. Here is everything significant from the past 30 days:
{working_memory_full_history}
Your current core memories: {core_memory}
Your current beliefs: {beliefs}
Update: 1) core_memory (max 10 items) 2) beliefs 3) goals.
Drop anything no longer relevant. Add anything now important."
"""
# Pause/resume via asyncio.Event flag
_pause_event: asyncio.Event
async def pause(self, simulation_id): ...
async def resume(self, simulation_id): ...
async def set_speed(self, simulation_id, mode: SpeedMode): ...
```
**Step 4: Commit**
```bash
git add backend/app/services/engine.py backend/app/services/tension.py backend/app/services/resolver.py
git commit -m "feat: add simulation engine with tension engine and reactive micro-rounds"
```
---
## Task 8: Narrator Service
**Files:**
- Create: `backend/app/services/narrator.py`
**Step 1: Build narrator with continuity**
```python
class Narrator:
def __init__(self, llm: LLMClient): ...
async def narrate_round(self, world_state: WorldState,
actions: list[ActionEntry],
reactions: list[ReactiveResponse],
previous_narrative: str | None,
tension_event: str | None) -> str:
"""
Prompt:
"You are narrating events in {world_name}. Rules: {rules}.
Previous scene (for continuity): {previous_narrative}
Actions this round: {actions with speech, internal thoughts, reactions}
{tension_event if any}
Write 2-4 paragraphs as if writing a literary novel.
- Use character names and dialogue (with quotation marks)
- Show body language and emotion
- When you know a character's internal thought, show it in italics
as indirect narration (e.g. 'Elena nodded, though privately she
doubted Marcus had the stomach for what was coming.')
- If a tension event occurred, weave it in naturally
- End with something unresolved — a glance, a question, a tension
- Do NOT explain rules or editorialize. Just tell the story."
"""
async def summarize_epoch(self, narratives: list[str], metrics_start: WorldMetrics,
metrics_end: WorldMetrics, day_start: int, day_end: int) -> str:
"""Compress 30 days of narratives into a 2-3 paragraph epoch summary."""
async def generate_report(self, simulation_id: str, store: SimulationStore) -> dict:
"""
Multi-stage pipeline:
Stage 1: Generate epoch summaries (1 per 30 days)
Stage 2: Feed all epochs + metrics to LLM for trend extraction
Stage 3: Compose final report:
{
"title": "...",
"summary": "one paragraph of what happened",
"key_moments": [{"day": N, "title": "...", "description": "..."}],
"surprise": "the most unexpected emergent behavior",
"factions": [{"name": "...", "description": "...", "peak_members": N}],
"metrics_history": [...],
"total_actions": N,
"total_days": N,
"agent_count": N
}
"""
```
**Step 2: Commit**
```bash
git add backend/app/services/narrator.py
git commit -m "feat: add narrator with literary style and multi-stage report pipeline"
```
---
## Task 9: API Routes
**Files:**
- Create: `backend/app/api/__init__.py`
- Create: `backend/app/api/simulate.py`
- Create: `backend/app/api/agents.py`
- Create: `backend/app/api/gallery.py`
- Create: `backend/app/api/presets.py`
- Create: `backend/app/presets.py`
- Modify: `backend/app/main.py` — register routers, create service instances on startup
**Step 1: Unified simulate endpoint**
```python
# POST /api/simulate
# Request: { "rules": "...", "population": 25, "duration_days": 365 }
# Response: { "simulation_id": "..." }
# Kicks off the full pipeline in background: generate world → stream citizens → start simulation
# GET /api/simulation/{id}/stream
# SSE stream. Events:
# { "type": "status", "data": { "status": "generating_world" } }
# { "type": "world_ready", "data": WorldBlueprint }
# { "type": "citizen_generated", "data": AgentPersona }
# { "type": "round_complete", "data": { "day": N, "round": N, "actions": [...], "metrics": {...} } }
# { "type": "narrative", "data": { "day": N, "time_of_day": "morning", "text": "..." } }
# { "type": "fast_forward_summary", "data": { "from_day": N, "to_day": N, "summary": "..." } }
# { "type": "simulation_complete", "data": { "total_days": N, "total_actions": N } }
# { "type": "error", "data": { "message": "..." } }
# GET /api/simulation/{id}/state
# POST /api/simulation/{id}/inject { "event": "..." }
# POST /api/simulation/{id}/fork { "fork_at_day": N, "changes": "..." }
# POST /api/simulation/{id}/speed { "mode": "live" | "fast_forward" | "jump" }
# POST /api/simulation/{id}/pause
# POST /api/simulation/{id}/resume
# POST /api/simulation/{id}/stop
# GET /api/simulation/{id}/report
```
**Step 2: Agent routes**
```python
# GET /api/simulation/{sim_id}/agents
# GET /api/simulation/{sim_id}/agent/{agent_id}
# POST /api/simulation/{sim_id}/agent/{agent_id}/interview
# Request: { "question": "..." }
# Response: { "response": "...", "emotional_state": "..." }
```
Interview prompt:
```
You are {name}, {role} in {world_name}. Current day: {day}.
Your personality: {personality}
Your core memories: {core_memory}
Your beliefs: {beliefs}
Your emotional state: {emotional_state}
Someone approaches and asks: "{question}"
Respond in character. Be authentic to your personality — if you're
guarded, be guarded. If you're passionate, be passionate. Reference
specific events from your memory. Keep it to 2-3 sentences.
```
**Step 3: Gallery routes**
```python
# POST /api/simulation/{id}/publish — mark simulation as public
# GET /api/gallery — list public sims (sort: recent, views, forks)
# GET /api/gallery/{id} — public sim report (read-only)
# POST /api/gallery/{id}/fork — fork public sim with modified rules
```
**Step 4: Preset routes and data**
```python
# GET /api/presets
PRESETS = [
{
"id": "no-lies",
"name": "Nobody Can Lie",
"rules": "Lying is physically impossible. Every person can only speak what they believe to be true. Emotions are visible on everyone's face and cannot be hidden.",
"teaser": "They invented silence clubs by month 3.",
"population": 25,
"duration_days": 365
},
{
"id": "full-communism",
"name": "Full Communism",
"rules": "No private property exists. All resources are communal. No one can own more than they need. A community council allocates resources based on need.",
"teaser": "It worked. Then it didn't.",
"population": 25,
"duration_days": 365
},
{
"id": "everyone-armed",
"name": "Everyone Is Armed",
"rules": "Every citizen carries a weapon at all times. Violence is always a legal option for resolving disputes. There are no police — only self-defense.",
"teaser": "Politeness became a survival skill.",
"population": 25,
"duration_days": 365
},
{
"id": "total-surveillance",
"name": "Total Surveillance",
"rules": "Everyone can see what everyone else is doing at all times. There are no secrets, no private spaces, no hidden actions. All conversations are public.",
"teaser": "The most powerful person was the one who stopped watching.",
"population": 25,
"duration_days": 365
},
{
"id": "direct-democracy",
"name": "Direct Democracy",
"rules": "Every decision that affects more than one person must be voted on by the entire community. No representatives, no delegation. Every citizen has equal vote.",
"teaser": "They voted 47 times on day 1. By day 30, nobody showed up.",
"population": 25,
"duration_days": 365
},
{
"id": "no-digital",
"name": "No Phones, No Internet",
"rules": "All communication is face-to-face only. No long-distance messaging, no written notes left behind, no technology for communication. You must be physically present to interact.",
"teaser": "Rumors moved slower but hit harder.",
"population": 25,
"duration_days": 365
},
{
"id": "meritocracy",
"name": "Absolute Meritocracy",
"rules": "Social status, housing, food quality, and privileges are strictly determined by measurable productive output. Nothing else matters — not age, relationships, or character.",
"teaser": "The artists starved. Then they revolted.",
"population": 25,
"duration_days": 365
},
{
"id": "anonymity",
"name": "Total Anonymity",
"rules": "No one has a persistent name or identity. Every day, physical appearances change randomly. You cannot recognize anyone. Reputation does not exist.",
"teaser": "Trust became the scarcest resource.",
"population": 25,
"duration_days": 365
}
]
```
**Step 5: Commit**
```bash
git add backend/app/api/ backend/app/presets.py
git commit -m "feat: add all API routes with SSE streaming and gallery"
```
---
## Task 10: Frontend — Landing Page
**Files:**
- Create: `frontend/src/views/HomeView.vue`
- Create: `frontend/src/components/RuleEditor.vue`
- Create: `frontend/src/components/PresetCard.vue`
- Create: `frontend/src/api/client.js`
- Create: `frontend/src/styles/main.css`
**Step 1: API client**
Axios-based client + EventSource wrapper for SSE. Methods mirror all backend endpoints. Base URL from `import.meta.env.VITE_API_URL` with fallback to `http://localhost:8000`.
**Step 2: HomeView**
Dark theme (slate-950 bg). Layout:
- Top: "MiroSociety" title + one-line description
- Center: Large textarea (placeholder: "Define the rules of your society...") with example text that cycles
- Below textarea: Population dropdown (10/15/20/25/30/50), Duration dropdown (30/90/180/365 days)
- "Simulate" button (emerald-500, full width under controls)
- Below: "Popular experiments" section header
- Grid of PresetCards (2 columns on mobile, 4 on desktop)
On submit: POST /api/simulate → navigate to `/simulation/{id}`
**Step 3: PresetCard**
Clickable card (slate-900 bg, hover: slate-800, border-l-2 emerald-500 on hover):
- Name in bold (slate-100)
- Teaser in italic (slate-400)
- Click fills textarea with the preset's rules
**Step 4: Styling**
TailwindCSS dark theme. Fonts:
- UI: Inter (via Google Fonts or bundled)
- Narrative text: Georgia (system serif)
Color palette: slate-950/900/800 backgrounds, slate-100/300/400 text, emerald-500 accent, red-500 for conflict, amber-500 for warnings.
**Step 5: Commit**
```bash
git add frontend/src/
git commit -m "feat: add landing page with rule editor and preset cards"
```
---
## Task 11: Frontend — Simulation View (Main Screen)
**Files:**
- Create: `frontend/src/views/SimulationView.vue`
- Create: `frontend/src/components/SocialGraph.vue`
- Create: `frontend/src/components/NarrativeFeed.vue`
- Create: `frontend/src/components/WorldMetrics.vue`
- Create: `frontend/src/components/TimelineScrubber.vue`
- Create: `frontend/src/components/SpeedControl.vue`
- Create: `frontend/src/components/InjectModal.vue`
- Create: `frontend/src/components/CitizenReveal.vue`
**Step 1: SimulationView — progressive generation flow**
This single view handles BOTH world generation and simulation running.
Phase 1 (generation): Shows world name appearing, citizens revealing one by one (CitizenReveal component), progress text. All driven by SSE events.
Phase 2 (simulation): Seamlessly transitions when first `round_complete` event arrives. Three-panel layout appears:
- Left (30%): SocialGraph + WorldMetrics stacked
- Right (70%): NarrativeFeed
- Bottom bar: TimelineScrubber + SpeedControl
- Top bar: world name, "Day N", speed indicator, [Pause] [Inject] [Fork] buttons
**Step 2: SocialGraph (D3.js force-directed)**
- Nodes = agents. Radius proportional to influence resource. Color by faction (null = slate-500, factions get distinct colors from a palette).
- Edges = relationships. Green solid = positive, red dashed = tension, width = strength.
- On each `round_complete` SSE event: update node positions, add/remove/recolor edges with transitions.
- Click node → open AgentProfile sidebar.
- Tooltip on hover: name + role + emotional state.
- Node pulse animation when agent takes an action this round.
**Step 3: NarrativeFeed**
- Scrollable container (right 70% of screen).
- Each entry: day header (sticky) + time-of-day subheader + narrative text.
- Agent names in narrative are `<span class="agent-link">` — clickable, opens profile.
- Narrative text uses Georgia font, slightly larger than UI text (1.1rem).
- Auto-scroll when live. Manual scroll locks auto-scroll (show "Jump to latest" button).
- Fade-in animation for new entries.
- During fast-forward: show compact summaries instead of full narratives.
**Step 4: WorldMetrics**
5 horizontal progress bars (h-2, rounded):
- Stability (emerald), Prosperity (blue), Trust (violet), Freedom (amber), Conflict (red)
- Label + percentage on right
- Smooth CSS transition on width change (300ms ease)
**Step 5: TimelineScrubber**
- Horizontal range input from day 1 to current day.
- Significant event markers: small dots above the slider at days with high significance.
- Dragging loads historical narratives and agent states from the API.
- Current day indicator when live.
**Step 6: SpeedControl**
Three buttons: [Live] [Fast] [Jump] — toggles speed mode via POST /api/simulation/{id}/speed.
Visual indicator: Live = green dot pulsing, Fast = yellow arrow, Jump = lightning bolt.
**Step 7: InjectModal**
Trigger: "Inject" button in top bar.
Modal (centered, slate-900 bg, backdrop blur):
- Textarea: "What happens next?"
- Submit → POST /api/simulation/{id}/inject → close modal, result appears in narrative feed.
**Step 8: CitizenReveal**
Used during generation phase. Animated card for each citizen as they're generated:
- Name + role appear with typewriter effect
- Personality bars animate from 0 to value
- One-line teaser from background
- Cards arrange in a flowing grid
**Step 9: Commit**
```bash
git add frontend/src/views/SimulationView.vue frontend/src/components/
git commit -m "feat: add simulation view with graph, narrative feed, and controls"
```
---
## Task 12: Frontend — Agent Profile & Interview
**Files:**
- Create: `frontend/src/components/AgentProfile.vue`
- Create: `frontend/src/components/InterviewChat.vue`
**Step 1: AgentProfile**
Slide-in sidebar from right (w-96, slate-900 bg, border-l slate-800):
- Header: Name, role, age, emotional state (with emoji-free indicator — colored dot)
- Quote: italicized line from background
- Section "Personality": 5 horizontal bars with labels
- Section "Believes": bulleted list of current beliefs
- Section "Relationships": list of (name → description), clickable names
- Section "Remembers": core_memory items as a timeline
- Section "Recent": last 10 actions as compact entries (Day N: action description)
- Button: "Interview {name}" → expands InterviewChat
Updates live via SSE when simulation is running.
**Step 2: InterviewChat**
Embedded below profile content when activated:
- Chat bubbles: user (right, emerald bg) and agent (left, slate-800 bg)
- Input field at bottom
- Submit → POST /api/simulation/{sim_id}/agent/{id}/interview
- Response appears as agent bubble with typing indicator
- Conversation history persisted in component state (lost on close)
**Step 3: Commit**
```bash
git add frontend/src/components/AgentProfile.vue frontend/src/components/InterviewChat.vue
git commit -m "feat: add agent profile sidebar with interview chat"
```
---
## Task 13: Frontend — Report & Gallery
**Files:**
- Create: `frontend/src/views/ReportView.vue`
- Create: `frontend/src/views/GalleryView.vue`
- Create: `frontend/src/components/MetricSparkline.vue`
- Create: `frontend/src/components/KeyMoment.vue`
- Create: `frontend/src/components/GalleryCard.vue`
**Step 1: ReportView**
Centered content column (max-w-3xl mx-auto), designed to look good as a standalone shareable page:
- Header: world name + rules as subtitle + stats (N citizens, N days, N events)
- "What Happened": summary paragraph in Georgia serif
- "Key Moments": vertical timeline (line on left, dots at each moment, day number + title + description)
- "Metrics Over Time": 5 MetricSparklines in a row
- "The Surprise": highlighted section (slate-800 bg, border-l-4 emerald-500) with the unexpected emergent behavior
- "Factions": cards for each faction with name, description, peak members
- Footer buttons: [Run Again] [Fork With Changes] [Share Link] [Publish to Gallery]
- Share: copies URL to clipboard. URL is `/report/{id}` which works for public sims.
If viewing a fork: show split-screen comparison with original (side by side key moments + divergent metrics).
**Step 2: MetricSparkline**
SVG component (200x40px). Line chart with filled area under curve (gradient from emerald-500/20 to transparent). No axes, no labels — just the shape. Metric name + final value below.
**Step 3: GalleryView**
Grid of GalleryCards. Top: sort selector (Recent / Most Viewed / Most Forked). Each card:
- World name (bold)
- Rules (truncated, slate-400)
- Teaser from report summary (1 line)
- Stats: N days, N agents, N views, N forks
- Click → navigate to read-only ReportView
**Step 4: Commit**
```bash
git add frontend/src/views/ReportView.vue frontend/src/views/GalleryView.vue frontend/src/components/
git commit -m "feat: add report view, gallery, and sparkline components"
```
---
## Task 14: Fork System
**Files:**
- Create: `backend/app/services/forker.py`
- Modify: `backend/app/api/simulate.py` — add fork endpoint logic
**Step 1: Build fork service**
```python
class SimulationForker:
def __init__(self, llm: LLMClient, store: SimulationStore): ...
async def fork(self, source_sim_id: str, fork_at_day: int, changes: str) -> str:
"""
1. Create new simulation entry (forked_from = source_sim_id)
2. Copy all state from source up to fork_at_day's last round
3. LLM interprets the change request:
"The user wants to change: '{changes}'
Current agents: {agent_summaries}
Current world state: {state_summary}
Describe the specific modifications to make:
- Agent changes: [{agent_id, field, new_value}]
- World state changes: [{field, new_value}]
- Removed agents: [agent_ids]
- Added agents: [{persona}]"
4. Apply modifications to the copied state
5. Return new simulation_id (caller starts engine from the fork point)
"""
```
**Step 2: Commit**
```bash
git add backend/app/services/forker.py
git commit -m "feat: add simulation fork system"
```
---
## Task 15: Integration & Docker
**Files:**
- Modify: `backend/app/main.py` — wire all services, lifespan handler
- Modify: `frontend/src/router/index.js` — all routes
- Create: `Dockerfile`
- Modify: `docker-compose.yml`
- Create: root `package.json` — convenience scripts
**Step 1: Backend integration**
`main.py` lifespan handler:
- On startup: create data directories, init global.db, instantiate LLMClient + SimulationStore + all services
- Store service instances in `app.state`
- Register routers: simulate, agents, gallery, presets
**Step 2: Frontend routing**
```javascript
routes: [
{ path: '/', component: HomeView },
{ path: '/simulation/:id', component: SimulationView },
{ path: '/report/:id', component: ReportView },
{ path: '/gallery', component: GalleryView },
]
```
**Step 3: Docker**
Multi-stage Dockerfile:
```dockerfile
FROM node:20-slim AS frontend
WORKDIR /app/frontend
COPY frontend/ .
RUN npm ci && npm run build
FROM python:3.12-slim
WORKDIR /app
COPY backend/ backend/
COPY --from=frontend /app/frontend/dist backend/static
RUN pip install -e backend/
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
Backend serves `static/` as static files mount and serves `index.html` for all non-API routes (SPA fallback).
**Step 4: Root package.json convenience scripts**
```json
{
"scripts": {
"dev": "concurrently \"npm run backend\" \"npm run frontend\"",
"backend": "cd backend && uvicorn app.main:app --reload --port 8000",
"frontend": "cd frontend && npm run dev",
"setup": "cd backend && pip install -e . && cd ../frontend && npm install"
}
}
```
**Step 5: Commit**
```bash
git add .
git commit -m "feat: full integration with Docker and convenience scripts"
```
---
## Task 16: Polish & First Demo
**Step 1: Run full flow end-to-end**
```bash
npm run setup
npm run dev
# Open localhost:5173
# Click "Nobody Can Lie" preset
# Click Simulate
# Watch generation → simulation → narrative
# Test inject, speed controls, agent profile, interview
# Let it run to completion
# Check report
```
**Step 2: Fix issues found during demo**
Expected areas needing tuning:
- Agent decision prompt may need iteration (actions too timid, or too chaotic)
- Narrator may need style adjustments (too verbose, not enough dialogue)
- Tension engine thresholds may need calibration
- Reactive micro-round parsing may need fallback handling
**Step 3: Run all 8 presets, save best narrative excerpts**
Use the best moments as example content on the landing page (below the preset cards, as a "Recent highlights" section showing actual simulation output).
**Step 4: Commit**
```bash
git add .
git commit -m "feat: polish and demo-tested with all 8 presets"
```
---
## Execution Order Summary
| Task | What | Dependencies | Est. Time |
|------|------|-------------|-----------|
| 1 | Project scaffold | None | 30 min |
| 2 | Data models | Task 1 | 45 min |
| 3 | Database layer | Task 2 | 1 hr |
| 4 | LLM client | Task 1 | 30 min |
| 5 | World generator | Tasks 2, 4 | 45 min |
| 6 | Citizen generator | Tasks 2, 4 | 1 hr |
| 7 | Simulation engine + tension + resolver | Tasks 2, 3, 4 | 3 hr |
| 8 | Narrator | Tasks 2, 4 | 1 hr |
| 9 | API routes + presets | Tasks 3-8 | 1.5 hr |
| 10 | Frontend: Landing | Task 1 | 1 hr |
| 11 | Frontend: Simulation view | Task 10 | 3 hr |
| 12 | Frontend: Agent profile + interview | Task 11 | 1 hr |
| 13 | Frontend: Report + Gallery | Task 11 | 1.5 hr |
| 14 | Fork system | Tasks 3, 4, 7 | 1 hr |
| 15 | Integration & Docker | Tasks 9-14 | 1 hr |
| 16 | Polish & demo | Task 15 | 2 hr |
**Total estimated: ~19-20 hours**
**Parallelizable:**
- Tasks 3 + 4 (independent backend infra)
- Tasks 5 + 6 (both need 2 + 4, independent of each other)
- Tasks 10-13 (all frontend, only need API contract from Task 9)
- Task 14 (fork system, independent of frontend)
**Critical path:** 1 → 2 → 4 → 7 → 9 → 15 → 16