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-19-life-domain-syst...

32 KiB

Life Domain System Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Give every MiroSociety agent a full human lifecycle — backstory, family, financial pressure, health, career, formative events that shape personality — and seed populations from real US Census data.

Architecture: New LifeState model on AgentPersona, new LifeEngine service parallel to TensionEngine, new CensusService for real demographic data, enhanced CitizenGenerator for life history generation, modified decision prompt with life context and action biasing, frontend Life tab and city input.

Tech Stack: Python/Pydantic (models), FastAPI (API), httpx (Census API), Vue 3 (frontend), existing LLMClient for generation.

Design Doc: docs/plans/2026-03-19-life-domain-system-design.md


Task 1: LifeState Data Models

Files:

  • Modify: backend/app/models/agent.py
  • Create: backend/tests/test_life_models.py

Step 1: Write the failing test

# backend/tests/test_life_models.py
from app.models.agent import (
    AgentPersona, Personality, LifeState, FamilyMember,
    FormativeEvent, LifePressure,
)

def test_life_state_defaults():
    ls = LifeState(childhood_summary="Grew up on a farm.", formative_events=[], family=[])
    assert ls.finances == 0.5
    assert ls.career == 0.5
    assert ls.health == 0.5
    assert ls.pressures == []
    assert ls.life_log == []

def test_family_member():
    fm = FamilyMember(name="Maria", relation="spouse", age=34, status="healthy", dependency=0.2)
    assert fm.bond_strength == 0.7  # default

def test_formative_event_trait_modifier():
    fe = FormativeEvent(
        age_at_event=7,
        description="Parents divorced",
        lasting_effect="Distrusts commitment",
        trait_modifier={"conformity": -0.1, "empathy": 0.15},
    )
    assert fe.trait_modifier["conformity"] == -0.1

def test_life_pressure_no_deadline():
    lp = LifePressure(domain="finances", description="Rent overdue", severity=0.6, created_day=1)
    assert lp.deadline_day is None

def test_life_pressure_with_deadline():
    lp = LifePressure(domain="finances", description="Rent overdue", severity=0.6, deadline_day=15, created_day=1)
    assert lp.deadline_day == 15

def test_agent_persona_has_life_state():
    ls = LifeState(childhood_summary="test", formative_events=[], family=[])
    agent = AgentPersona(
        id=0, name="Test", role="Worker", age=30,
        personality=Personality(), background="test",
        life_state=ls,
    )
    assert agent.life_state.finances == 0.5
    assert agent.life_state.childhood_summary == "test"

def test_agent_persona_optional_life_state():
    agent = AgentPersona(
        id=0, name="Test", role="Worker", age=30,
        personality=Personality(), background="test",
    )
    assert agent.life_state is None

Step 2: Run test to verify it fails

Run: cd backend && python -m pytest tests/test_life_models.py -v Expected: FAIL — LifeState, FamilyMember, FormativeEvent, LifePressure not defined

Step 3: Write the models

Add to backend/app/models/agent.py — above AgentPersona:

class FamilyMember(BaseModel):
    name: str
    relation: str
    age: int
    status: str = "healthy"
    dependency: float = Field(default=0.0, ge=0.0, le=1.0)
    bond_strength: float = Field(default=0.7, ge=0.0, le=1.0)

class FormativeEvent(BaseModel):
    age_at_event: int
    description: str
    lasting_effect: str
    trait_modifier: dict[str, float] = Field(default_factory=dict)

class LifePressure(BaseModel):
    domain: str
    description: str
    severity: float = Field(default=0.5, ge=0.0, le=1.0)
    deadline_day: int | None = None
    created_day: int = 0

class LifeState(BaseModel):
    childhood_summary: str
    formative_events: list[FormativeEvent] = Field(default_factory=list)
    family: list[FamilyMember] = Field(default_factory=list)
    finances: float = Field(default=0.5, ge=0.0, le=1.0)
    career: float = Field(default=0.5, ge=0.0, le=1.0)
    health: float = Field(default=0.5, ge=0.0, le=1.0)
    pressures: list[LifePressure] = Field(default_factory=list)
    life_log: list[str] = Field(default_factory=list)

Add life_state field to AgentPersona:

life_state: LifeState | None = None

Step 4: Run test to verify it passes

Run: cd backend && python -m pytest tests/test_life_models.py -v Expected: all PASS

Step 5: Commit

git add backend/app/models/agent.py backend/tests/test_life_models.py
git commit -m "feat: add LifeState, FamilyMember, FormativeEvent, LifePressure models"

Task 2: Life Context Computation (need priority, domain levels, action bias)

Files:

  • Create: backend/app/services/life_context.py
  • Create: backend/tests/test_life_context.py

Step 1: Write the failing tests

# backend/tests/test_life_context.py
from app.models.agent import (
    AgentPersona, Personality, LifeState, FamilyMember,
    FormativeEvent, LifePressure,
)
from app.services.life_context import (
    compute_need_priority, compute_domain_level, compute_action_bias,
    find_relevant_echoes, build_life_prompt_block,
)

def _make_agent(finances=0.5, career=0.5, health=0.5, family=None, pressures=None, formative=None):
    ls = LifeState(
        childhood_summary="Grew up in a small town.",
        formative_events=formative or [],
        family=family or [],
        finances=finances, career=career, health=health,
        pressures=pressures or [],
    )
    return AgentPersona(
        id=0, name="Test", role="Worker", age=30,
        personality=Personality(), background="test", life_state=ls,
    )

def test_need_priority_survival_mode():
    agent = _make_agent(
        finances=0.1,
        pressures=[LifePressure(domain="finances", description="Eviction", severity=0.8, deadline_day=5, created_day=0)],
    )
    result = compute_need_priority(agent, current_day=4)
    assert "SURVIVAL" in result.upper() or "must act" in result.lower()

def test_need_priority_thriving():
    agent = _make_agent(finances=0.9, career=0.8, health=0.9)
    result = compute_need_priority(agent, current_day=10)
    assert "legacy" in result.lower() or "meaning" in result.lower() or "good" in result.lower()

def test_domain_level_desperate():
    assert "desperate" in compute_domain_level("finances", 0.1)

def test_domain_level_thriving():
    assert "thriving" in compute_domain_level("finances", 0.9)

def test_action_bias_financial_desperation():
    agent = _make_agent(finances=0.15)
    bias = compute_action_bias(agent, current_day=10)
    assert "TRADE" in bias
    assert "urgent" in bias["TRADE"].lower() or "need" in bias["TRADE"].lower()

def test_action_bias_high_dependency():
    family = [FamilyMember(name="Aiden", relation="child", age=6, dependency=0.9)]
    agent = _make_agent(family=family)
    bias = compute_action_bias(agent, current_day=10)
    assert "DEFECT" in bias
    assert "family" in bias["DEFECT"].lower()

def test_action_bias_thriving():
    agent = _make_agent(finances=0.85, career=0.8)
    bias = compute_action_bias(agent, current_day=10)
    assert "PROPOSE_RULE" in bias

def test_find_relevant_echoes_authority():
    events = [FormativeEvent(
        age_at_event=10, description="Father punished for questioning the elder",
        lasting_effect="Fears authority and avoids confrontation",
        trait_modifier={"confrontational": -0.1},
    )]
    agent = _make_agent(formative=events)
    echoes = find_relevant_echoes(agent, "A new rule has been proposed by the council leader")
    assert len(echoes) >= 1
    assert "Father" in echoes[0]

def test_find_relevant_echoes_no_match():
    events = [FormativeEvent(
        age_at_event=10, description="Won a singing contest",
        lasting_effect="Believes in self-expression",
        trait_modifier={},
    )]
    agent = _make_agent(formative=events)
    echoes = find_relevant_echoes(agent, "The market is open for trading")
    assert len(echoes) == 0

def test_build_life_prompt_block_has_sections():
    family = [FamilyMember(name="Maria", relation="spouse", age=32)]
    pressures = [LifePressure(domain="finances", description="Rent is overdue", severity=0.6, created_day=0)]
    agent = _make_agent(finances=0.3, family=family, pressures=pressures)
    block = build_life_prompt_block(agent, current_day=10, context="community meeting")
    assert "YOUR LIFE RIGHT NOW" in block
    assert "Maria" in block
    assert "Rent" in block or "rent" in block

Step 2: Run test to verify it fails

Run: cd backend && python -m pytest tests/test_life_context.py -v Expected: FAIL — module app.services.life_context not found

Step 3: Implement life_context.py

Create backend/app/services/life_context.py with functions: compute_need_priority, compute_domain_level, compute_action_bias, find_relevant_echoes, build_life_prompt_block.

See design doc Section 3 for the full logic of each function. Key implementation details:

  • compute_domain_level uses the DOMAIN_LEVELS and DOMAIN_FLAVOR lookup tables
  • compute_action_bias returns dict[str, str] mapping action names to annotation strings
  • find_relevant_echoes pattern-matches formative event lasting_effect against current context keywords
  • build_life_prompt_block assembles all sections into a single string for prompt injection

Step 4: Run test to verify it passes

Run: cd backend && python -m pytest tests/test_life_context.py -v Expected: all PASS

Step 5: Commit

git add backend/app/services/life_context.py backend/tests/test_life_context.py
git commit -m "feat: add life context computation — need priority, domain levels, action bias, echoes"

Task 3: Life Events Engine

Files:

  • Create: backend/app/services/life_engine.py
  • Create: backend/tests/test_life_engine.py

Step 1: Write the failing tests

# backend/tests/test_life_engine.py
import pytest
from unittest.mock import AsyncMock
from app.models.agent import (
    AgentPersona, Personality, LifeState, FamilyMember, LifePressure,
)
from app.models.world import WorldState, WorldBlueprint, TimeConfig, WorldMetrics
from app.services.life_engine import LifeEngine

def _make_world():
    bp = WorldBlueprint(
        name="TestWorld", description="test", rules=["be kind"],
        locations=[], resources=["goods", "money"], initial_tensions=["inequality"],
        time_config=TimeConfig(total_days=30),
    )
    return WorldState(blueprint=bp, day=10)

def _make_agent(id=0, finances=0.5, health=0.5, career=0.5, family=None, pressures=None):
    ls = LifeState(
        childhood_summary="test", formative_events=[], family=family or [],
        finances=finances, career=career, health=health,
        pressures=pressures or [],
    )
    return AgentPersona(
        id=id, name=f"Agent{id}", role="Worker", age=30,
        personality=Personality(ambition=0.5, empathy=0.5), background="test",
        life_state=ls,
    )

def test_tick_pressures_escalate_deadline():
    engine = LifeEngine(llm=AsyncMock())
    agent = _make_agent(pressures=[
        LifePressure(domain="finances", description="Rent", severity=0.5, deadline_day=8, created_day=0),
    ])
    engine._tick_pressures(agent, current_day=10)
    assert agent.life_state.pressures[0].severity > 0.5
    assert agent.life_state.pressures[0].deadline_day is None

def test_tick_pressures_resolve():
    engine = LifeEngine(llm=AsyncMock())
    agent = _make_agent(finances=0.8, pressures=[
        LifePressure(domain="finances", description="Rent", severity=0.3, created_day=0),
    ])
    engine._tick_pressures(agent, current_day=5)
    assert len(agent.life_state.pressures) == 0

def test_tick_pressures_decay_old():
    engine = LifeEngine(llm=AsyncMock())
    agent = _make_agent(pressures=[
        LifePressure(domain="career", description="Mild worry", severity=0.2, created_day=0),
    ])
    engine._tick_pressures(agent, current_day=25)
    assert len(agent.life_state.pressures) == 0

def test_eval_skips_non_interval_rounds():
    engine = LifeEngine(llm=AsyncMock())
    agents = [_make_agent()]
    world = _make_world()
    import asyncio
    result = asyncio.get_event_loop().run_until_complete(
        engine.evaluate(agents, world, round_num=3)
    )
    assert result == []

def test_select_event_respects_weight_factors():
    engine = LifeEngine(llm=AsyncMock())
    agent = _make_agent(finances=0.1)
    event = engine._select_event(agent)
    assert event is not None
    assert event["template"] in engine.CATALOG["finances"]["negative"][0]["template"] or True  # just verifying it returns something

def test_max_pressures_cap():
    engine = LifeEngine(llm=AsyncMock())
    agent = _make_agent(pressures=[
        LifePressure(domain="finances", description=f"p{i}", severity=0.5, created_day=0)
        for i in range(4)
    ])
    event = {"domain_delta": -0.1, "pressure": "New problem", "template": "test"}
    engine._apply_event(agent, event, current_day=10)
    assert len(agent.life_state.pressures) <= 4

Step 2: Run test to verify it fails

Run: cd backend && python -m pytest tests/test_life_engine.py -v Expected: FAIL — module not found

Step 3: Implement LifeEngine

Create backend/app/services/life_engine.py. Key components:

  • CATALOG dict with event templates by domain (finances, career, health, family) × polarity (positive, negative)
  • CASCADE_RULES dict mapping event templates to conditional follow-on effects
  • evaluate() — main entry point, runs every EVAL_INTERVAL rounds
  • _tick_pressures() — escalate/resolve/decay existing pressures
  • _select_candidates() — weighted selection of agents due for events
  • _select_event() — pick from catalog based on agent state and weight factors
  • _personalize_event() — LLM call to turn template into specific narrative
  • _apply_event() — update life domains, create pressures, run cascades
  • _apply_cascades() — check CASCADE_RULES for chain effects

See design doc Section 2 for full logic.

Step 4: Run test to verify it passes

Run: cd backend && python -m pytest tests/test_life_engine.py -v Expected: all PASS

Step 5: Commit

git add backend/app/services/life_engine.py backend/tests/test_life_engine.py
git commit -m "feat: add LifeEngine — probabilistic life events with cascading pressures"

Task 4: Backstory Generator (Life History Generation)

Files:

  • Modify: backend/app/services/citizen_generator.py
  • Create: backend/tests/test_backstory_gen.py

Step 1: Write the failing tests

# backend/tests/test_backstory_gen.py
import pytest
import asyncio
from unittest.mock import AsyncMock, patch
from app.models.agent import AgentPersona, Personality, LifeState
from app.models.world import WorldBlueprint, TimeConfig
from app.services.citizen_generator import CitizenGenerator

def _make_blueprint():
    return WorldBlueprint(
        name="TestWorld", description="A test world", rules=["Be honest"],
        locations=[], resources=["goods", "money"], initial_tensions=["inequality"],
        time_config=TimeConfig(total_days=30),
    )

def _make_agent(id=0, age=35, ambition=0.7, conformity=0.3):
    return AgentPersona(
        id=id, name=f"Agent{id}", role="Worker", age=age,
        personality=Personality(ambition=ambition, conformity=conformity),
        background="A hard worker", goals=["Survive"],
        beliefs=["The system is unfair"],
    )

def test_apply_trait_modifiers_single_event():
    gen = CitizenGenerator(llm=AsyncMock())
    agent = _make_agent(ambition=0.5)
    agent.life_state = LifeState(
        childhood_summary="test",
        formative_events=[{
            "age_at_event": 10, "description": "test",
            "lasting_effect": "test",
            "trait_modifier": {"ambition": 0.15},
        }],
        family=[],
    )
    # Convert dicts to FormativeEvent objects first
    from app.models.agent import FormativeEvent
    agent.life_state.formative_events = [
        FormativeEvent(**e) if isinstance(e, dict) else e
        for e in agent.life_state.formative_events
    ]
    gen._apply_trait_modifiers(agent)
    assert agent.personality.ambition == pytest.approx(0.65, abs=0.01)

def test_apply_trait_modifiers_damping():
    gen = CitizenGenerator(llm=AsyncMock())
    agent = _make_agent(conformity=0.5)
    from app.models.agent import FormativeEvent
    agent.life_state = LifeState(
        childhood_summary="test",
        formative_events=[
            FormativeEvent(age_at_event=7, description="e1", lasting_effect="e1", trait_modifier={"conformity": -0.15}),
            FormativeEvent(age_at_event=14, description="e2", lasting_effect="e2", trait_modifier={"conformity": -0.15}),
            FormativeEvent(age_at_event=20, description="e3", lasting_effect="e3", trait_modifier={"conformity": -0.15}),
        ],
        family=[],
    )
    gen._apply_trait_modifiers(agent)
    # 1st: -0.15, 2nd: -0.15*0.67=-0.10, 3rd: -0.15*0.5=-0.075 => total ~ -0.325
    # But 0.5 - 0.325 = 0.175, should NOT be less than 0 due to clamping
    assert agent.personality.conformity >= 0.0
    # Should be LESS than 0.5 - 0.15 = 0.35 (damping means NOT full -0.45)
    assert agent.personality.conformity > 0.5 - 0.45

def test_enforce_life_diversity_finances():
    gen = CitizenGenerator(llm=AsyncMock())
    agents = []
    for i in range(20):
        a = _make_agent(id=i)
        a.life_state = LifeState(childhood_summary="test", finances=0.2, family=[])
        agents.append(a)
    gen._enforce_life_diversity(agents)
    avg = sum(a.life_state.finances for a in agents) / len(agents)
    assert avg > 0.3  # diversity enforcement pushed some up

def test_num_formative_events_by_age():
    gen = CitizenGenerator(llm=AsyncMock())
    assert gen._num_formative_events(25) == 4
    assert gen._num_formative_events(40) == 5
    assert gen._num_formative_events(60) == 6

Step 2: Run test to verify it fails

Run: cd backend && python -m pytest tests/test_backstory_gen.py -v Expected: FAIL — methods not found on CitizenGenerator

Step 3: Implement backstory generation

Modify backend/app/services/citizen_generator.py:

  • Add LIFE_HISTORY_PROMPT constant (see design doc Section 4)
  • Add _generate_life_histories() async method — batched LLM calls, one per agent
  • Add _parse_life_history() — parse LLM JSON response into LifeState
  • Add _apply_trait_modifiers() — apply formative event trait modifiers with damping
  • Add _apply_all_trait_modifiers() — loop over all agents
  • Add _enforce_life_diversity() — audit and correct distribution bias
  • Add _num_formative_events() — 4 for <30, 5 for <50, 6 for 50+
  • Modify generate() to call _generate_life_histories() after _generate_personas() and before _generate_relationships()

Step 4: Run test to verify it passes

Run: cd backend && python -m pytest tests/test_backstory_gen.py -v Expected: all PASS

Step 5: Commit

git add backend/app/services/citizen_generator.py backend/tests/test_backstory_gen.py
git commit -m "feat: add life history generation with trait modifiers and diversity enforcement"

Task 5: Census Demographic Service

Files:

  • Create: backend/app/services/census.py
  • Create: backend/app/models/demographics.py
  • Create: backend/tests/test_census.py

Step 1: Write the failing tests

# backend/tests/test_census.py
import pytest
import asyncio
from unittest.mock import AsyncMock, patch, MagicMock
from app.models.demographics import DemographicProfile, AgeDistribution, IncomeDistribution
from app.services.census import CensusService

def test_resolve_fips_known_city():
    svc = CensusService(llm=AsyncMock())
    fips = svc._resolve_fips("san francisco", "CA")
    assert fips == ("06", "67000")

def test_resolve_fips_unknown_city():
    svc = CensusService(llm=AsyncMock())
    fips = svc._resolve_fips("randomville", None)
    assert fips is None

def test_resolve_fips_case_insensitive():
    svc = CensusService(llm=AsyncMock())
    fips = svc._resolve_fips("New York", "NY")
    assert fips is not None

def test_demographic_profile_model():
    profile = DemographicProfile(
        city_name="Test City", state="CA", population=100000,
        age=[AgeDistribution(bracket="25_34", percentage=0.22)],
        income=[IncomeDistribution(bracket="50k_75k", percentage=0.2)],
        occupations=[], ethnicity=[],
        median_household_income=75000, poverty_rate=0.12,
        unemployment_rate=0.05, homeownership_rate=0.45,
        median_rent=2000, rent_burden_rate=0.4,
        college_education_rate=0.35, median_age=36.0,
        city_character="A diverse city",
    )
    assert profile.city_name == "Test City"
    assert profile.poverty_rate == 0.12

Step 2: Run test to verify it fails

Run: cd backend && python -m pytest tests/test_census.py -v Expected: FAIL — modules not found

Step 3: Implement demographics model and CensusService

Create backend/app/models/demographics.py:

  • AgeDistribution, IncomeDistribution, OccupationDistribution, EthnicityDistribution — simple bracket + percentage models
  • DemographicProfile — full city profile with all distributions and key stats

Create backend/app/services/census.py:

  • CITY_FIPS dict — top ~100 US cities mapped to (state_fips, place_fips)
  • TABLE_VARIABLES — which Census variables to fetch per profile table (DP03, DP05, DP02)
  • _resolve_fips() — city name → FIPS lookup, case-insensitive, supports optional state
  • _fetch_acs() — single HTTP GET to Census API for one profile table
  • get_profile() — parallel fetch of DP02+DP03+DP05, build DemographicProfile, cache to store
  • _build_profile() — parse Census API response into DemographicProfile
  • _llm_estimate() — fallback for unknown/non-US cities using LLM world knowledge

Step 4: Run test to verify it passes

Run: cd backend && python -m pytest tests/test_census.py -v Expected: all PASS

Step 5: Commit

git add backend/app/models/demographics.py backend/app/services/census.py backend/tests/test_census.py
git commit -m "feat: add CensusService for real city demographics via Census Bureau API"

Task 6: Integrate Life Context into Decision Prompt

Files:

  • Modify: backend/app/services/engine.py (AGENT_DECISION_SYSTEM prompt, _build_agent_prompt or equivalent, response parsing)

Step 1: Write the failing test

# backend/tests/test_life_prompt_integration.py
from app.models.agent import (
    AgentPersona, Personality, LifeState, FamilyMember,
    FormativeEvent, LifePressure,
)
from app.services.life_context import build_life_prompt_block

def test_life_block_in_prompt_format():
    ls = LifeState(
        childhood_summary="Grew up poor in the Mission district.",
        formative_events=[FormativeEvent(
            age_at_event=11, description="Found mother crying over bills",
            lasting_effect="Learned money problems mean love problems",
            trait_modifier={"honesty": -0.05},
        )],
        family=[FamilyMember(name="Linh", relation="spouse", age=32, dependency=0.2)],
        finances=0.25, career=0.4, health=0.7,
        pressures=[LifePressure(domain="finances", description="Rent went up again", severity=0.6, created_day=1)],
    )
    agent = AgentPersona(
        id=0, name="Marcus", role="Line cook", age=34,
        personality=Personality(), background="test", life_state=ls,
    )
    block = build_life_prompt_block(agent, current_day=10, context="new rule proposed by council")
    assert "YOUR LIFE RIGHT NOW" in block
    assert "struggling" in block.lower() or "desperate" in block.lower()
    assert "Linh" in block
    assert "Rent" in block
    assert "YOUR HISTORY" in block
    assert "Mission" in block or "poor" in block

Step 2: Run test to verify it passes (should already pass from Task 2)

Run: cd backend && python -m pytest tests/test_life_prompt_integration.py -v

Step 3: Modify engine.py

Modify AGENT_DECISION_SYSTEM prompt template:

  • Add {life_context} placeholder between WHO YOU ARE and CORE MEMORIES
  • Add LIFE and PAST steps to the reasoning chain
  • Add life_context and past_echo to the JSON response schema

Modify the method that builds agent prompts (find by searching for AGENT_DECISION_SYSTEM.format):

  • Import build_life_prompt_block, compute_action_bias from app.services.life_context
  • If agent has life_state, compute life block and action bias annotations
  • Inject life block into prompt
  • Append bias annotations to action descriptions

Modify response parsing:

  • Accept life_context and past_echo fields in AgentDecision (optional, for logging)

Step 4: Run full test suite

Run: cd backend && python -m pytest tests/ -v Expected: all PASS

Step 5: Commit

git add backend/app/services/engine.py backend/app/models/action.py backend/tests/test_life_prompt_integration.py
git commit -m "feat: integrate life context into agent decision prompt and reasoning chain"

Task 7: Integrate LifeEngine into Simulation Loop

Files:

  • Modify: backend/app/services/engine.py (SimulationEngine.init, run loop)
  • Modify: backend/app/api/simulate.py (pass LifeEngine to SimulationEngine)

Step 1: Modify SimulationEngine

In SimulationEngine.__init__:

  • Add life_engine: LifeEngine | None = None parameter
  • Store as self.life_engine

In SimulationEngine.run() — insert life engine evaluation at line ~356 (after _select_active_agents, before _batch_decisions):

# Life events for active agents
life_events_this_round = []
if self.life_engine:
    life_events_this_round = await self.life_engine.evaluate(active, world_state, round_num)
    for agent, event_desc in life_events_this_round:
        agent.working_memory.append(f"Day {world_state.day}: {event_desc}")
        if len(agent.working_memory) > 9:
            agent.working_memory = agent.working_memory[-9:]

After narration (line ~413), emit life events as SSE:

for agent, event_desc in life_events_this_round:
    await emit(SSEEvent(type="life_event", data={
        "agent_id": agent.id, "agent_name": agent.name,
        "event_description": event_desc,
        "day": world_state.day,
        "time_of_day": TIMES_OF_DAY[world_state.round_in_day % 3],
    }))

Step 2: Modify simulate.py

In the simulation startup route, create LifeEngine and pass to SimulationEngine:

from app.services.life_engine import LifeEngine
life_engine = LifeEngine(llm=request.app.state.llm)
engine = SimulationEngine(llm=..., store=..., tension=..., resolver=..., narrator=..., life_engine=life_engine, ...)

Step 3: Test manually

Run: cd backend && uvicorn app.main:app --reload Start a simulation and verify life events appear in the SSE stream.

Step 4: Commit

git add backend/app/services/engine.py backend/app/api/simulate.py
git commit -m "feat: integrate LifeEngine into simulation loop with SSE emission"

Task 8: City Demographics in API and Generation

Files:

  • Modify: backend/app/api/simulate.py (SimulateRequest, startup route)
  • Modify: backend/app/services/citizen_generator.py (demographic-constrained generation)
  • Modify: backend/app/main.py (initialize CensusService)

Step 1: Add city field to SimulateRequest

In backend/app/api/simulate.py:

class SimulateRequest(BaseModel):
    # ... existing fields ...
    city: str | None = None

Step 2: Modify startup route

In the simulation start handler:

  • If city is provided, call CensusService.get_profile(city)
  • Pass DemographicProfile to CitizenGenerator.generate()
  • Emit demographics_loaded SSE event

Step 3: Modify CitizenGenerator.generate()

Add demographics: DemographicProfile | None = None parameter.

  • If provided, use DEMOGRAPHIC_CAST_PROMPT instead of CAST_SYSTEM_PROMPT
  • After cast generation, validate demographic fit
  • During life history generation, call _calibrate_life_domains() to adjust finances/health/career based on income bracket and housing status

Step 4: Modify frontend client.js

Add city to simulate payload:

async simulate(rules, population, durationDays, proposedChange, segments, city) {
    const payload = { rules, population, duration_days: durationDays }
    if (city) payload.city = city
    // ... rest
}

Step 5: Commit

git add backend/app/api/simulate.py backend/app/services/citizen_generator.py backend/app/main.py frontend/src/api/client.js
git commit -m "feat: add city demographics parameter — real Census data seeds agent populations"

Task 9: Frontend — City Input on HomeView

Files:

  • Modify: frontend/src/views/HomeView.vue

Step 1: Add city input UI

Add a new progressive-disclosure option after "Add customer segments":

  • + Base on a real city toggle
  • When revealed: text input for city name
  • After typing, show a summary card with key stats (fetched from backend or just shown after simulation starts)

Add reactive state:

const city = ref('')
const showCity = ref(false)

Pass city to api.simulate() call.

Step 2: Test manually

Open http://localhost:5173, click "+ Base on a real city", type "San Francisco", start simulation.

Step 3: Commit

git add frontend/src/views/HomeView.vue
git commit -m "feat: add city demographics input to HomeView"

Task 10: Frontend — Life Tab on AgentDetailPanel

Files:

  • Modify: frontend/src/components/AgentDetailPanel.vue

Step 1: Add Life tab

Add { id: 'life', label: 'Life' } to tabs array.

Add Life tab content section showing:

  • Life domain bars (finances, career, health) with labels computed from value ranges
  • Family members list with status badges
  • Active pressures with severity bars and deadline countdowns
  • Childhood summary
  • Formative events timeline
  • Life event log

Use existing Tailwind patterns from the Profile tab for consistency.

Step 2: Test manually

Click an agent during simulation, switch to Life tab, verify all sections render.

Step 3: Commit

git add frontend/src/components/AgentDetailPanel.vue
git commit -m "feat: add Life tab to AgentDetailPanel showing backstory, family, pressures"

Task 11: Frontend — Life Events in Simulation Feed

Files:

  • Modify: frontend/src/views/SimulationView.vue

Step 1: Handle life_event SSE type

In the SSE event handler, add case for type === "life_event":

  • Push to event feed with a distinct visual style
  • Use dashed border, muted colors, hollow icon to differentiate from society actions
  • Show domain change tags (e.g., [finances ↓])

Step 2: Test manually

Run simulation, verify life events appear in the feed with distinct styling.

Step 3: Commit

git add frontend/src/views/SimulationView.vue
git commit -m "feat: render life events in simulation feed with distinct visual treatment"

Task 12: Interview Prompt Enhancement

Files:

  • Modify: backend/app/api/agents.py

Step 1: Add life context to interview prompt

In the interview endpoint, if agent has life_state, add life context to the system prompt:

  • Childhood summary
  • Current pressures
  • Family situation
  • Domain levels

This makes agent interviews life-aware without any frontend changes.

Step 2: Test manually

Interview an agent, verify response references their life situation.

Step 3: Commit

git add backend/app/api/agents.py
git commit -m "feat: add life context to agent interview prompts"

Task 13: Final Integration Test

Step 1: Run full backend test suite

Run: cd backend && python -m pytest tests/ -v Expected: all PASS

Step 2: Run end-to-end test

Start both servers: npm run dev

  1. Create simulation with city = "San Francisco" and rules = "Universal basic income of $1000/month"
  2. Verify agents have diverse demographics matching SF
  3. Click an agent → Life tab → verify backstory, family, pressures
  4. Watch simulation → verify life events appear in feed
  5. Interview an agent → verify life-aware response
  6. Run for 10+ days → verify life events fire and pressures evolve

Step 3: Commit everything

git add -A
git commit -m "feat: complete Life Domain System — full human lifecycles for agents"