Extropy has three phases, each mapping to a package under extropy/.
extropy spec → extropy scenario → extropy persona → extropy sample → extropy network → extropy simulate → extropy results
All commands operate within a study folder — a directory containing study.db and scenario subdirectories. Data is keyed by scenario_id rather than population_id.
These are the canonical study patterns consolidated from prior showcase and study docs.
| Study Type | Population | Scenario Mode | Horizon | Typical Scale |
|---|---|---|---|---|
| ASI announcement + societal transition | National population | Evolving | Monthly, 6 steps | 5,000 agents |
| Geopolitical shock (for example Iran strikes) | National population | Evolving | Weekly, 12 steps | 5,000 agents |
| Asset/market mania shock (for example BTC extreme rally) | National population | Evolving | Weekly, 8-12 steps | 5,000 agents |
| Election projection (house control / state race) | National or state electorate | Static or lightly evolving | Decision-focused | 1,000-5,000 agents |
Before moving forward to the next stage, the architecture assumes these checks:
specgate: distribution realism and dependency coherence.scenariogate: timeline/outcome consistency and no contradictory rules.personagate: natural language quality and no contradictory rendering.samplegate: no impossible household or demographic combinations.networkgate: topology pass with realistic structural edge mix.simulategate: expected timestep dynamics, checkpoint integrity, and tracked outputs.
LLM validates the description has enough context (who, how many, where).
LLM discovers 25-40 attributes across 4 categories:
universal— age, gender, incomepopulation_specific— specialty, seniority, commute methodcontext_specific— scenario-relevant attitudes and behaviorspersonality— Big Five traits
Each attribute gets a type (int/float/categorical/boolean) and sampling strategy (independent/derived/conditional).
Four sub-steps, each using different LLM tiers:
- Independent (
hydrators/independent.py) —agentic_research()with web search finds real-world distributions with source URLs - Derived (
hydrators/derived.py) —reasoning_call()specifies deterministic formulas (e.g.,years_experience = age - 26) - Conditional base (
hydrators/conditional.py) —agentic_research()finds base distributions for dependent attributes - Conditional modifiers (
hydrators/conditional.py) —reasoning_call()specifies how values shift based on other attributes
Topological sort (Kahn's algorithm) resolves attribute dependencies into a valid sampling order.
Iterates through sampling_order, routing each attribute by strategy. Supports 6 distribution types: normal, lognormal, uniform, beta, categorical, boolean.
When household_mode: true:
- Sample primary adults first
- Generate correlated partners (shared attributes: location, income correlation)
- Generate NPC dependents (children, elderly) based on household type
- Household types: singles, couples, single parents, couples with kids, multi-generational
agent_focus_modecontrols who reasons:primary_only,couples,all
Hybrid algorithm: similarity-based edge probability with degree correction, calibrated via simulated annealing to hit target metrics, then Watts-Strogatz rewiring for small-world properties.
Edge types (structural, from attributes):
partner— frompartner_idfield (weight: 1.0)household— samehousehold_id(weight: 0.9)coworker— sameoccupation_category+ region (weight: 0.6)neighbor— same region + similar age (weight: 0.4)congregation— same religious affiliation + high religiosity (weight: 0.4)school_parent— both have school-age kids + same region (weight: 0.35)
Remaining degree filled with acquaintance or online_contact based on attribute similarity.
The scenario command is a mini spec builder — it discovers and researches scenario-specific attributes that extend the base population, then configures simulation parameters.
- Attribute Selection — LLM discovers attributes relevant to this scenario but not in base population (e.g.,
vaccine_hesitancyfor a vaccine scenario,job_automation_exposurefor an AI scenario) - Distribution Research — Hydrates new attributes with distributions (same hydrators as spec command)
- Household Config — Determines if/how household dynamics matter for this scenario
- Event Configuration — Defines the triggering event (type, content, source, credibility)
- Exposure Rules — Configures how agents encounter the event (channels, targeting, timing)
- Outcome Definition — What to measure (categorical, boolean, open-ended)
- Simulation Parameters — Timesteps, timeline mode, stopping conditions
extended_attributes— Scenario-specific attributes with full distribution specshousehold_config— Household composition parameters (age brackets, type weights, partner correlation)event— Event definitiontimeline— For evolving scenarios: subsequent events at different timestepsseed_exposure— Channels and rules for initial exposureinteraction— How agents interact about the eventspread— Network propagation configoutcomes— What to measuresimulation— Timestep config and stopping conditions
Compiler (compiler.py) orchestrates event parsing, exposure rules, interaction model, outcomes, and spec assembly.
| Type | Examples |
|---|---|
announcement |
Company policy, product feature, organizational change |
news |
Breaking news, industry report, research findings |
policy_change |
Government regulation, tax change, zoning decision |
product_launch |
New product, service update, feature rollout |
rumor |
Unconfirmed reports, speculation, leaked information |
emergency |
Crisis, natural disaster, security incident |
observation |
Behavioral change noticed in environment |
| Channel | Reach | Trust |
|---|---|---|
broadcast |
High, many agents | Varies |
targeted |
Filtered by attributes | Higher |
organic |
Network-dependent | High |
| Type | Schema | Use |
|---|---|---|
categorical |
Enum options | Known decision space |
boolean |
Yes/No | Binary decisions |
float |
Range [-1,1] or [0,1] | Intensity measures |
open_ended |
Free text | Unknown space, discover post-hoc |
- Static — Single event, opinions evolve over time
- Evolving — Multiple events injected at specified timesteps
Per-timestep loop with 5 phases:
Apply seed exposures, then timeline exposures (if a timeline event fires), then network propagation via conviction-gated sharing.
Timeline exposures stamp provenance metadata on each exposure:
info_epoch(the originating timeline timestep)force_rereason(whether committed agents should re-reason for that epoch)
Network exposures inherit epoch provenance from the source agent, so downstream re-reasoning can be driven by provenance rather than content keyword matching.
Select agents to reason, split into chunks, run two-pass async LLM reasoning.
Selection gates:
- First-time aware agents always reason.
- Non-committed agents reason on multi-touch threshold.
- Committed agents can re-reason when exposed to a newer forced
info_epoch. - A de-dup guard ensures at most one re-reason per agent per epoch.
Pass 1 (strong model): Agent role-plays in first person with no categorical enums. Produces:
reasoning— internal monologuepublic_statement— what they'd say publiclysentiment— emotional valenceconviction— 0-100 confidence scorewill_share— whether they'll discuss with others
Pass 2 (fast model): Classify free-text reasoning into scenario-defined outcomes.
- Bounded confidence opinion update
- Conviction-based flip resistance
- Private opinion tracking (separate from public)
- State persistence to DB
Non-reasoning agents experience gradual conviction decay, preventing stale states.
Compound conditions: explicit stop conditions, timeline-aware convergence/quiescence, max timesteps.
max_timestepsand explicitstop_conditionsare always evaluated.- Auto
convergedand autosimulation_quiescentare suppressed when future timeline events exist (unless overridden bysimulation.allow_early_convergenceor CLI--early-convergence).
| Tier | Conversations | Memory | Cognitive Features | Cost/Agent |
|---|---|---|---|---|
low |
None | Last 5 traces | Basic | ~$0.03 |
medium |
Top 1 edge (partner/closest) | All traces | Standard | ~$0.04 |
high |
Top 2-3 edges | All + beliefs | THINK vs SAY, repetition detection | ~$0.05 |
- Agents request
talk_toactions during reasoning - Multi-turn exchanges (2 turns at medium, 3 at high)
- Both participants update state independently
- Supports agent-NPC conversations (kids, elderly parents)
Explicit separation between internal monologue (raw, honest) and public statement (socially filtered).
If reasoning is >70% similar to previous timestep, agent gets nudged to consider what's actually changed.
Single-pass reasoning caused 83% of agents to pick safe middle options (central tendency bias). Splitting role-play from classification fixes this.
Agents output a 0-100 integer score. Bucketed immediately:
| Score | Float | Level | Meaning |
|---|---|---|---|
| 0-15 | 0.1 | very_uncertain |
Barely formed |
| 16-35 | 0.3 | leaning |
Tentative |
| 36-60 | 0.5 | moderate |
Reasonably confident |
| 61-85 | 0.7 | firm |
Strong position |
| 86-100 | 0.9 | absolute |
Unwavering |
Agents see neighbors' public_statement + sentiment tone, NOT position labels. Influence is semantic — agents are swayed by arguments, not categorical stances.
Each agent maintains a sliding window memory trace (configurable by fidelity). Entries include timestep, summary of what they processed, and how it affected their thinking.
The extropy persona command generates a PersonaConfig via 5-step LLM pipeline. At simulation time, agents are rendered computationally — no per-agent LLM calls.
- Relative attributes positioned via z-scores ("I'm much more price-sensitive than most")
- Concrete attributes use format specs for proper number/time rendering
- Trait salience groups decision-relevant attributes first
Each phase commits separately. On crash:
- Detect crashed-mid-timestep or last-completed-timestep
- Skip already-processed agents
- Resume from checkpoint
All LLM calls go through this file. Two-zone routing:
| Function | Use |
|---|---|
simple_call() |
Sufficiency checks, simple extractions |
reasoning_call() |
Attribute selection, hydration, scenario compilation |
agentic_research() |
Distribution hydration with web search |
| Pass | Model | Use |
|---|---|---|
| Pass 1 | strong model | Agent role-play, freeform reaction |
| Pass 2 | fast model | Outcome extraction from narrative |
LLMProvider base class with OpenAIProvider, ClaudeProvider, and Azure OpenAI support. All calls use structured output (response_format: json_schema).
All Pydantic v2:
population.py:PopulationSpec,AttributeSpec,SamplingConfig, distributions, modifiersscenario.py:ScenarioSpec,Event,SeedExposure,OutcomeConfig,ScenarioSimConfigsimulation.py:AgentState(public/private position/sentiment/conviction + info epochs),ExposureRecord(channel/source + provenance),ReasoningContext,ReasoningResponsenetwork.py:Edge,NetworkConfig,NetworkMetrics
Canonical store: study.db (SQLite) in the study folder root.
my-study/
├── study.db # Canonical data store
├── population.v1.yaml # Base population spec
├── scenario/
│ └── my-scenario/
│ ├── scenario.v1.yaml # Scenario spec (references base_population)
│ ├── persona.v1.yaml # Persona rendering config
│ └── network-config.yaml # Optional custom network config
└── results/
└── my-scenario/ # Simulation outputs
| Table | Contents | Key |
|---|---|---|
agents |
Sampled agent attributes (JSON) | scenario_id |
network_edges |
Social graph edges with weights and types | scenario_id |
agent_states |
Current simulation state per agent | run_id |
exposures |
Exposure records with source/channel plus epoch provenance (info_epoch, force_rereason) |
run_id |
memory_traces |
Agent memory entries | run_id |
timeline |
Simulation events (JSONL-style) | run_id |
timestep_summaries |
Per-timestep aggregates | run_id |
simulation_runs |
Run metadata and status | run_id |
simulation_metadata |
Checkpoint state | run_id |
chat_sessions |
Post-sim agent chat sessions | session_id |
Agents and network edges are keyed by scenario_id, not population_id. This allows:
- Multiple scenarios to share a base population spec
- Each scenario to have its own extended attributes merged at sample time
- Clear association between agents/network and their scenario context
Resolution order: programmatic > env vars > config file > defaults
| Field | Default | Description |
|---|---|---|
mode |
human |
human = interactive prompts, rich output. agent = JSON output, exit codes, no prompts |
| Field | Default | Description |
|---|---|---|
fast |
provider default | Fast model for pipeline |
strong |
provider default | Strong model for pipeline |
| Field | Default | Description |
|---|---|---|
fast |
= models.fast |
Fast model for Pass 2 |
strong |
= models.strong |
Strong model for Pass 1 |
max_concurrent |
null (auto from RPM) |
Max concurrent LLM calls |
rate_tier |
null |
Provider rate limit tier |
Built-in: openai, anthropic, azure, openrouter, deepseek
Custom providers via providers.<name>.base_url and providers.<name>.api_key_env.
| Format | Files |
|---|---|
| YAML | Population specs, scenario specs, persona configs, network configs |
| SQLite | study.db — canonical simulation state |
| JSON | Result exports, legacy artifacts |
| JSONL | Timeline events, data exports |
pytest + pytest-asyncio. Key coverage:
test_engine.py— engine integration with mocked LLMtest_conviction.py— conviction bucketing and level mappingtest_propagation.py— exposure propagation and sharingtest_stopping.py— stopping conditions and convergencetest_memory_traces.py— memory window and multi-touch triggerstest_household_sampling.py— household sampling and partner correlationtest_conversations.py— multi-turn conversation dynamics
CI: .github/workflows/test.yml — lint (ruff) + test (Python 3.11/3.12/3.13)