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.
414 lines
19 KiB
414 lines
19 KiB
# MiroSociety Life Domain System — Design Document
|
|
|
|
> Agents don't just have personalities. They have lives — childhoods that haunt them, rent that's overdue, sick parents, kids who need shoes, careers that are stagnating. Every decision is made through the weight of a whole life.
|
|
|
|
## Overview
|
|
|
|
MiroSociety agents currently have personality traits, memories, beliefs, relationships, and emotional states. They reason through FEEL → WANT → FEAR → DECIDE. This is good for producing interesting *social* behavior, but it misses the invisible forces that shape every real human decision: financial pressure, family obligation, health, career trajectory, and the formative experiences that made a person who they are.
|
|
|
|
This design adds a **Life Domain System** — a structured layer of life context that gives every agent a full backstory, ongoing life pressures, and a life events engine that changes their circumstances during the simulation. Combined with **real city demographics** from the US Census Bureau, this turns MiroSociety from "imagine a society" into "simulate how the people of San Francisco would actually react."
|
|
|
|
### What Changes
|
|
|
|
| Before | After |
|
|
|--------|-------|
|
|
| Agents have a 2-3 sentence background | Agents have a full life history: childhood, formative events, family, financial situation |
|
|
| Background is narrative-only | Formative events carry `trait_modifier` that mechanically shift personality values |
|
|
| No family model | Agents have named family members as contextual pressures (not separate agents) |
|
|
| Life is static during simulation | Life Events Engine injects personal changes: illness, job loss, promotions, family crises |
|
|
| Fictional demographics | Real census data seeds agent populations based on actual US cities |
|
|
| Decision prompt has no life context | New LIFE and HISTORY sections in the prompt; action bias annotations based on life state |
|
|
| FEEL → WANT → FEAR → DECIDE | FEEL → LIFE → PAST → WANT → FEAR → DECIDE |
|
|
|
|
### What Stays the Same
|
|
|
|
- Core personality model (honesty, ambition, empathy, confrontational, conformity)
|
|
- Three-tier memory (core + working + reflective)
|
|
- TensionEngine (world-level disruption)
|
|
- ActionResolver and all existing actions
|
|
- Reactive micro-rounds producing dialogue
|
|
- Report pipeline and fork system
|
|
|
|
---
|
|
|
|
## 1. The LifeState Model
|
|
|
|
### Data Structures
|
|
|
|
```python
|
|
class FamilyMember(BaseModel):
|
|
name: str
|
|
relation: str # "spouse", "child", "parent", "sibling"
|
|
age: int
|
|
status: str # "healthy", "ill", "estranged", "deceased"
|
|
dependency: float # 0.0 = independent, 1.0 = fully dependent
|
|
bond_strength: float = 0.7 # how close they are emotionally
|
|
|
|
class FormativeEvent(BaseModel):
|
|
age_at_event: int
|
|
description: str # scene, not summary
|
|
lasting_effect: str # specific behavioral pattern
|
|
trait_modifier: dict[str, float] # {"conformity": -0.1, "empathy": +0.15}
|
|
|
|
class LifePressure(BaseModel):
|
|
domain: str # "finances", "health", "career", "family"
|
|
description: str # specific, personal, 2 sentences
|
|
severity: float # 0.0 = mild annoyance, 1.0 = crisis
|
|
deadline_day: int | None = None # sim day by which this must resolve
|
|
created_day: int = 0
|
|
|
|
class LifeState(BaseModel):
|
|
childhood_summary: str # 4-5 sentences, explains WHY they are who they are
|
|
formative_events: list[FormativeEvent] # 4-6 key life moments with trait modifiers
|
|
family: list[FamilyMember] # named family members as contextual pressures
|
|
|
|
finances: float = 0.5 # 0.0 = crisis, 1.0 = thriving
|
|
career: float = 0.5
|
|
health: float = 0.5
|
|
|
|
pressures: list[LifePressure] = [] # max ~3-4 active at a time
|
|
life_log: list[str] = [] # last 5 significant life events
|
|
```
|
|
|
|
### Design Rationale
|
|
|
|
**Three life domains, not five.** `finances`, `career`, `health` are the three axes that can't be derived from existing agent state. Social standing is already captured by `relationships` + `social_connections` + `faction`. Family wellbeing is derived at prompt-build time from `family[].status` and `family[].dependency`.
|
|
|
|
**No Maslow needs hierarchy stored.** Need-priority is computed at decision time from life domains and injected as a single framing sentence. This avoids double-bookkeeping and is always consistent.
|
|
|
|
**FormativeEvent carries trait_modifier.** Each backstory event mechanically shifts personality values during generation. "Grew up in poverty" → `{"price_sensitivity": +0.15, "ambition": +0.1}`. The backstory literally shapes who the agent is numerically, not just narratively.
|
|
|
|
**LifePressure has a lifecycle.** Pressures appear, escalate (deadline missed → severity increases), resolve (domain improved → pressure removed), and expire (old low-severity pressures decay). Capped at ~3-4 to keep prompts tight.
|
|
|
|
**deadline_day creates urgency.** A pressure at severity 0.6 with a deadline in 2 days produces fundamentally different behavior than severity 0.6 with no deadline.
|
|
|
|
---
|
|
|
|
## 2. The Life Events Engine
|
|
|
|
A new service that runs alongside TensionEngine. While TensionEngine disrupts the *world*, LifeEngine disrupts *individual lives*.
|
|
|
|
### Principles
|
|
|
|
1. **Probabilistic, not random.** A financially struggling agent is more likely to face eviction. A high-ambition agent is more likely to get a promotion. Events emerge from current state.
|
|
2. **Events cascade.** Job loss → financial pressure → can't afford medicine for sick parent. Problems compound like real life.
|
|
3. **Most rounds are uneventful.** Only 1-2 agents per evaluation cycle get a life event. Quiet stability is realistic.
|
|
4. **Good things happen too.** ~35% of events are positive. Promotions, windfalls, recoveries, family milestones.
|
|
|
|
### Event Catalog
|
|
|
|
A weighted catalog of event templates organized by domain. Each template specifies:
|
|
- `domain_delta`: how much the life domain changes
|
|
- `pressure`: what new pressure is created (if any)
|
|
- `weight_factors`: what makes this event more/less likely for a given agent
|
|
|
|
Weight factor types:
|
|
- `"inverse"`: probability increases as domain value decreases (poor health → more likely to get sick)
|
|
- `"direct"`: probability increases as trait/domain value increases (high ambition → more likely to get promoted)
|
|
- `"required"`: event can only fire if condition is true (family events require having family)
|
|
|
|
**Domains covered:** finances (unexpected expenses, windfalls, debt), career (job threats, promotions, public failures), health (illness, injury, recovery), family (family illness, conflict, milestones, dependent needs).
|
|
|
|
### Engine Cycle
|
|
|
|
```
|
|
EVAL_INTERVAL = 6 rounds (every 2 sim-days)
|
|
MAX_EVENTS_PER_CYCLE = 2
|
|
MAX_PRESSURES_PER_AGENT = 4
|
|
POSITIVE_RATIO = 0.35
|
|
```
|
|
|
|
Each cycle:
|
|
1. **Tick pressures** — escalate overdue, resolve improved, decay old
|
|
2. **Select candidates** — agents most "due" for a life event (weighted by time since last event, domain extremes, personality)
|
|
3. **Fire events** — select from catalog based on weight factors, personalize via LLM
|
|
4. **Apply cascades** — check cascade rules for follow-on effects
|
|
|
|
### Pressure Lifecycle
|
|
|
|
- **Deadline passed:** severity increases by 0.2, deadline cleared (now chronic)
|
|
- **Domain improved above 0.7 + severity below 0.4:** pressure resolved, logged
|
|
- **Age > 20 days + severity below 0.3:** pressure decays naturally
|
|
|
|
### Cascade Rules
|
|
|
|
Event chains defined as condition → effect mappings:
|
|
- `job_threat` + `finances < 0.4` → financial pressure (savings running out)
|
|
- `family_illness` + high-dependency family → financial pressure (medical costs)
|
|
- `family_illness` + `empathy > 0.6` → health delta -0.1 (stress of caregiving)
|
|
- `unexpected_expense` + `finances < 0.3` → career pressure (desperate for income)
|
|
|
|
### Integration Point
|
|
|
|
LifeEngine evaluates **between agent selection and decision** (so life events influence the same round's decision) and **after tension check** at cycle intervals for inactive agents.
|
|
|
|
### Event Personalization
|
|
|
|
One LLM call (~100 tokens output) per event, turning template into specific narrative:
|
|
- "A major unexpected expense hits" → "The roof of Marcus's house collapsed during the storm — with winter coming and two children under 10, he needs 40 units of goods to repair it before the first frost."
|
|
|
|
---
|
|
|
|
## 3. Decision Prompt Integration
|
|
|
|
### New Prompt Sections
|
|
|
|
Two new sections added to the agent decision prompt:
|
|
|
|
**YOUR LIFE RIGHT NOW** — computed need-priority + active pressures + family summary + domain levels as human-readable text (not raw floats).
|
|
|
|
**YOUR HISTORY** — childhood summary + contextually relevant formative event echoes.
|
|
|
|
### Domain Level Translation
|
|
|
|
Raw floats are never shown to the LLM. They're translated to narrative:
|
|
|
|
| Range | Level | Example (finances) |
|
|
|-------|-------|-------------------|
|
|
| 0.0-0.2 | desperate | "you can barely feed yourself, let alone your family" |
|
|
| 0.2-0.35 | struggling | "every trade matters — you're one bad week from crisis" |
|
|
| 0.35-0.5 | tight but managing | "you get by, but there's no safety margin" |
|
|
| 0.5-0.65 | stable | "you're not rich, but money isn't keeping you up at night" |
|
|
| 0.65-0.8 | comfortable | "you can afford to take a risk here and there" |
|
|
| 0.8-1.0 | thriving | "money is the last thing on your mind" |
|
|
|
|
Similar flavor tables for career and health.
|
|
|
|
### Modified Reasoning Chain
|
|
|
|
```
|
|
1. FEEL: What is your emotional reaction to the current situation?
|
|
2. LIFE: How does your current life situation (family, money, health, pressures) affect how you see this?
|
|
3. PAST: Does anything in your history make you react differently than most people would?
|
|
4. WANT: Given ALL of the above, what do you want right now?
|
|
5. FEAR: What could go wrong — for you AND for the people who depend on you?
|
|
6. DECIDE: What action do you take?
|
|
```
|
|
|
|
Two new JSON response fields: `life_context` and `past_echo`. These force the LLM to reason through life context before choosing an action, making backstory causal rather than decorative.
|
|
|
|
### Mechanical Action Biasing
|
|
|
|
Actions get contextual annotations based on life state. The action space stays full (no actions removed) but annotations shift emotional weight:
|
|
|
|
- **Financial desperation:** TRADE gets "(you NEED resources — this is urgent)"; DEFECT gets "(risky — if caught, your family suffers)" when dependents exist
|
|
- **Health crisis:** BUILD gets "(you don't have the physical energy)"; OBSERVE gets "(rest and watching is all you can manage)"
|
|
- **High dependency:** DEFECT and PROTEST get "(think about your family)"; SPEAK_PRIVATE gets "(ask for help — your family is counting on you)"
|
|
- **Career pressure:** COMPLY gets "(you need to be seen as reliable right now)"; BUILD gets "(proving yourself could save your position)"
|
|
- **Imminent deadline:** DO_NOTHING gets "(you can't afford to wait — {pressure_description})"
|
|
- **Thriving:** PROPOSE_RULE gets "(you have the standing to shape the rules)"; BUILD gets "(you can afford to invest)"
|
|
|
|
Annotations are nudges, not restrictions. An agent *can* still PROTEST with kids at home — and those are the most interesting moments.
|
|
|
|
### Contextual Echo Matching
|
|
|
|
Formative events aren't listed every round. They're pattern-matched against the current situation and surfaced only when relevant:
|
|
- Authority situations (rules, leaders) → echo authority-related formative events
|
|
- Scarcity situations → echo poverty/loss experiences
|
|
- Trust situations → echo betrayal/abandonment experiences
|
|
- Belonging situations → echo isolation/rejection experiences
|
|
|
|
Max 2 echoes per round to keep prompts tight.
|
|
|
|
### Token Budget
|
|
|
|
| Section | Tokens |
|
|
|---------|--------|
|
|
| Childhood summary | ~90 |
|
|
| Formative events (4-6 × 30) | ~150 |
|
|
| Family summary | ~25 |
|
|
| Need priority | ~20 |
|
|
| Active pressures (2-3 × 40) | ~100 |
|
|
| Relevant echoes (0-2) | ~50 |
|
|
| Domain levels with flavor | ~40 |
|
|
| Action bias annotations | ~15 |
|
|
| **Total input addition** | **~490** |
|
|
| `life_context` + `past_echo` output | ~60 |
|
|
|
|
At 25 agents, 3 rounds/day: ~37K extra input tokens/day, ~4.5K output tokens/day. At gpt-4o-mini pricing: ~$0.008/day.
|
|
|
|
---
|
|
|
|
## 4. Backstory Generator with Real City Demographics
|
|
|
|
### Census Data Integration
|
|
|
|
Two free public APIs provide real demographic data for any US city:
|
|
|
|
**Census Bureau ACS (American Community Survey)**
|
|
- Endpoint: `https://api.census.gov/data/2023/acs/acs5/profile`
|
|
- Free API key (optional for <500 queries/day)
|
|
- Four profile tables: DP02 (social), DP03 (economic), DP04 (housing), DP05 (demographic)
|
|
- Available at city/place level via FIPS codes
|
|
|
|
**Bureau of Labor Statistics OEWS**
|
|
- Endpoint: `https://api.bls.gov/publicAPI/v2/timeseries/data/`
|
|
- No key required for basic access
|
|
- Occupation breakdown by metropolitan area (800+ SOC categories)
|
|
|
|
### DemographicProfile Model
|
|
|
|
```python
|
|
class DemographicProfile(BaseModel):
|
|
city_name: str
|
|
state: str
|
|
population: int
|
|
age: list[AgeDistribution] # brackets: 18-24, 25-34, 35-44, 45-54, 55-64, 65+
|
|
income: list[IncomeDistribution] # brackets: <25K, 25-50K, 50-75K, 75-100K, 100-150K, 150K+
|
|
occupations: list[OccupationDistribution]
|
|
ethnicity: list[EthnicityDistribution]
|
|
median_household_income: int
|
|
poverty_rate: float
|
|
unemployment_rate: float
|
|
homeownership_rate: float
|
|
median_rent: int
|
|
rent_burden_rate: float # % of renters spending >30% income on rent
|
|
college_education_rate: float
|
|
median_age: float
|
|
city_character: str # LLM-generated qualitative summary
|
|
```
|
|
|
|
### CensusService
|
|
|
|
- Pre-populated FIPS lookup table for top ~100 US cities
|
|
- Parallel fetch of DP02, DP03, DP05 tables
|
|
- Results cached to SQLite with 30-day TTL
|
|
- **Graceful fallback:** if city not in FIPS table or API fails, LLM estimates demographics from world knowledge
|
|
|
|
### Demographic-Constrained Cast Generation
|
|
|
|
When a city is selected, the cast generation prompt receives real demographic data and strict instructions:
|
|
- Age distribution must match census proportions
|
|
- Income distribution must reflect real inequality
|
|
- Occupations proportional to city's industry breakdown
|
|
- Names and cultural backgrounds reflect ethnic composition
|
|
- Housing situations calibrated to homeownership rate and rent burden
|
|
- Poverty rate determines minimum number of financially struggling agents
|
|
|
|
Post-generation validation checks cast against demographic targets with 15% tolerance and logs deviations.
|
|
|
|
### Life Domain Calibration
|
|
|
|
Income bracket + housing status → base `finances` value, adjusted by rent burden. City-specific pressures auto-generated (e.g., rent-burdened agent in SF's Mission district gets an initial financial pressure).
|
|
|
|
### Non-US Cities
|
|
|
|
Falls back to LLM demographic estimation. The LLM is asked to estimate age distribution, income levels, ethnic composition, occupation categories, poverty rate, housing affordability. Not Census-accurate, but sufficient for representative cast generation.
|
|
|
|
---
|
|
|
|
## 5. Frontend Changes
|
|
|
|
### HomeView — City Input
|
|
|
|
New progressive-disclosure option: "+ Base on a real city". Reveals a city input with live search. When a city is resolved, shows a summary card:
|
|
|
|
```
|
|
San Francisco, CA
|
|
Pop: 870K · Median income: $120K · Poverty: 11% · Rent-burdened: 46%
|
|
✓ Real census data (ACS 2023)
|
|
```
|
|
|
|
City name sent as new `city` field in `/api/simulate` payload.
|
|
|
|
### AgentDetailPanel — Life Tab
|
|
|
|
Fourth tab added: **Life**. Shows:
|
|
- Life domain bars (finances, career, health) with human-readable labels
|
|
- Family members with status and dependency
|
|
- Active pressures with severity bars and deadlines
|
|
- Childhood summary and formative events
|
|
- Life event log
|
|
|
|
### Simulation Feed — Life Events
|
|
|
|
Life events appear in the timeline with distinct visual treatment (dashed borders, hollow icon) and small domain-change tags: `[finances ↓ 0.1]`.
|
|
|
|
### Generation Phase
|
|
|
|
Extra progress line when city demographics are loaded: "Demographics loaded: SF census data".
|
|
|
|
### New SSE Events
|
|
|
|
- `life_event`: agent_id, description, domain_changes, new_pressures, day, time_of_day
|
|
- `demographics_loaded`: city, state, source (census_acs_2023 | llm_estimate), key stats, summary
|
|
|
|
---
|
|
|
|
## 6. Generation Pipeline Changes
|
|
|
|
### Updated Flow
|
|
|
|
```
|
|
1. Generate cast (existing, enhanced with demographic constraints)
|
|
2. Generate personas (existing)
|
|
3. Generate life histories (NEW — one LLM call per agent, batched in 5s, max_tokens=1200)
|
|
4. Apply trait modifiers from formative events (NEW — pure computation, no LLM)
|
|
5. Enforce life diversity (NEW — audit distributions, correct LLM drama bias)
|
|
6. Generate relationships (existing, now informed by family/neighborhood context)
|
|
7. Assign knowledge levels (existing, market only)
|
|
```
|
|
|
|
### Trait Modifier Application
|
|
|
|
Formative event trait modifiers stack with damping: 1st event = 100% effect, 2nd = 67%, 3rd = 50%. Prevents runaway personality values from multiple events hitting the same trait.
|
|
|
|
### Life Diversity Enforcement
|
|
|
|
Post-generation audit corrects LLM bias toward dramatic backstories:
|
|
- If average finances < 0.35, push top quartile toward stability (0.6-0.85)
|
|
- If average health < 0.4, push top third toward healthy (0.7-0.95)
|
|
- Ensure at least 20% of agents have zero active pressures
|
|
- Ensure at least ~15% of agents have minimal family (some people are alone)
|
|
|
|
---
|
|
|
|
## 7. API Changes
|
|
|
|
### POST /api/simulate — New Fields
|
|
|
|
```json
|
|
{
|
|
"rules": "...",
|
|
"population": 25,
|
|
"duration_days": 30,
|
|
"proposed_change": null,
|
|
"segments": null,
|
|
"city": "San Francisco, CA"
|
|
}
|
|
```
|
|
|
|
`city` is optional. When provided, triggers demographic fetch and constrained generation.
|
|
|
|
### GET /api/simulation/{id}/agent/{aid} — Extended Response
|
|
|
|
Agent response now includes `life_state` object with all life domains, family, pressures, backstory, and life log.
|
|
|
|
### GET /api/simulation/{id}/demographics — New Endpoint
|
|
|
|
Returns the DemographicProfile used to seed the simulation (if city-based).
|
|
|
|
---
|
|
|
|
## 8. Cost Analysis
|
|
|
|
### Generation (One-Time)
|
|
|
|
| Step | Calls | Input tokens/call | Output tokens/call | Total |
|
|
|------|-------|-------------------|-------------------|-------|
|
|
| Life history generation | 25 (batched 5×5) | ~600 | ~1000 | ~40K tokens |
|
|
| Event personalization | 0 at generation | — | — | 0 |
|
|
| **Total generation overhead** | | | | **~40K tokens (~$0.006 at gpt-4o-mini)** |
|
|
|
|
### Per Simulation Day (25 agents, 3 rounds)
|
|
|
|
| Component | Extra tokens/day | Cost/day (gpt-4o-mini) |
|
|
|-----------|-----------------|----------------------|
|
|
| Life context in decision prompts | ~37K input | ~$0.006 |
|
|
| life_context + past_echo output | ~4.5K output | ~$0.002 |
|
|
| Life event personalization (~0.3 events/day avg) | ~200 | negligible |
|
|
| **Total daily overhead** | **~42K tokens** | **~$0.008** |
|
|
|
|
### Full Simulation (100 days)
|
|
|
|
~$0.80 additional at gpt-4o-mini. ~$12 at gpt-4o. Negligible relative to existing simulation cost.
|
|
|