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_leveluses the DOMAIN_LEVELS and DOMAIN_FLAVOR lookup tablescompute_action_biasreturnsdict[str, str]mapping action names to annotation stringsfind_relevant_echoespattern-matches formative eventlasting_effectagainst current context keywordsbuild_life_prompt_blockassembles 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:
CATALOGdict with event templates by domain (finances, career, health, family) × polarity (positive, negative)CASCADE_RULESdict mapping event templates to conditional follow-on effectsevaluate()— 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_PROMPTconstant (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 modelsDemographicProfile— full city profile with all distributions and key stats
Create backend/app/services/census.py:
CITY_FIPSdict — 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 tableget_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_promptor 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_contextandpast_echoto 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_biasfromapp.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_contextandpast_echofields 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 = Noneparameter - 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
cityis provided, callCensusService.get_profile(city) - Pass
DemographicProfiletoCitizenGenerator.generate() - Emit
demographics_loadedSSE event
Step 3: Modify CitizenGenerator.generate()
Add demographics: DemographicProfile | None = None parameter.
- If provided, use
DEMOGRAPHIC_CAST_PROMPTinstead ofCAST_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 citytoggle- 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
- Create simulation with city = "San Francisco" and rules = "Universal basic income of $1000/month"
- Verify agents have diverse demographics matching SF
- Click an agent → Life tab → verify backstory, family, pressures
- Watch simulation → verify life events appear in feed
- Interview an agent → verify life-aware response
- 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"