Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 70 additions & 27 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ Agents experience the crisis as it unfolds. Early timesteps have high uncertaint

**Automatic detection**: If you define a single event with no timeline, Extropy treats it as static. If you provide multiple events or explicit timeline entries, it switches to evolving mode. You can override with `timeline_mode: static` or `timeline_mode: evolving`.

**Background context**: Scenarios can include ambient framing that appears in every agent's prompt. "The US economy is in a mild recession. Unemployment was at 4.5% before the AI announcement. It's early spring." This shapes reasoning without being the focal event.

**Timestep units are configurable**: Days, weeks, months - whatever fits your scenario. A crisis might unfold over days. A policy change might play out over months. A generational shift might span years.

---
Expand Down Expand Up @@ -190,10 +192,14 @@ Agents aren't goldfish. They remember.

**Temporal labeling**: Prompts explicitly state the current timestep. "It's now Week 5 of this situation." Agents can reason about time - how long something has been going on, whether their views have been stable or shifting.

**Emotional trajectory**: The system detects sentiment trends. "You started skeptical but have been warming up" or "Your enthusiasm has been fading over the past few weeks." This shapes agent self-awareness.
**Emotional trajectory**: The system detects sentiment trends and renders them as narrative. "I've been feeling increasingly negative since this started" or "My mood has been fairly steady." This gives agents emotional continuity between timesteps instead of starting fresh every time.

**Conviction self-awareness**: Agents know how firm they've been. "I've been firm about this since Week 1" or "I started certain but my certainty has been slipping." This enables commitment bias (consistent agents resist change) and openness (wavering agents are more receptive).

**Intent accountability**: If an agent said they'd do something, they get reminded. "Last week you said you were going to look into alternatives. Has anything changed?" This prevents agents from making bold claims they never follow through on.

**Repetition detection**: When agents keep thinking the same thing for multiple timesteps, they get nudged: "You've been thinking the same things for a while now. Has anything actually changed? Are you starting to doubt yourself? Have you done anything about it, or just thought about it?" This prevents stale convergence where agents repeat identical reasoning.

**Conviction decay**: Strong opinions fade without reinforcement. A conviction score of 0.9 doesn't stay at 0.9 forever. Configurable decay rate means you can model how quickly certainty erodes.

**Flip resistance**: High-conviction agents are harder to move. If someone is absolutely certain, new information needs to be compelling to shift them. This prevents unrealistic opinion swings.
Expand All @@ -216,24 +222,6 @@ You don't program social behavior explicitly. It emerges from the mechanics.

---

## What You Get Out

After simulation runs, you have:

**Position distributions**: What fraction of the population supports, opposes, or remains neutral? Segmented by any attribute - how do young people differ from old? Urban from rural? High-income from low-income?

**Sentiment trajectories**: How did emotional response evolve over time? Did initial negativity soften? Did enthusiasm fade?

**Conviction patterns**: Where are the true believers vs. the persuadable middle? How does certainty correlate with position?

**Sharing behavior**: Who's talking about this? Which demographics amplify vs. stay silent?

**Reasoning traces**: The actual first-person reasoning each agent produced. Qualitative insight into why people think what they think.

**Network effects**: How did information flow? Which communities adopted early? Where did resistance cluster?

---

## Agent Conversations

Agents can talk to each other. When reasoning, an agent can choose to initiate a conversation with someone in their network.
Expand All @@ -248,10 +236,10 @@ Agents can talk to each other. When reasoning, an agent can choose to initiate a

**Conflict resolution**: When multiple agents want to talk to the same target, priority determines who wins. Higher relationship weight wins - a partner request beats a coworker request. Deferred requests can execute in later timesteps.

**Fidelity control**: The `--fidelity` flag controls conversation depth:
- `low`: No conversations at all - just reasoning
- `medium` (default): 2 turns (4 messages), top 1 conversation per agent
- `high`: 3 turns (6 messages), up to 2 conversations per agent
**Fidelity control**: The `--fidelity` flag controls conversation depth and prompt richness:
- `low`: No conversations, last 5 memory traces, basic prompts
- `medium` (default): 2 turns (4 messages), 1 conversation per agent, full memory traces
- `high`: 3 turns (6 messages), up to 2 conversations per agent, explicit THINK vs SAY separation, repetition detection

---

Expand Down Expand Up @@ -289,8 +277,63 @@ To make it concrete, here are scenarios that work right now with no additional d
- Social media dynamics where public discourse influences individuals
- Any population, any country, any event, any outcome structure

The constraints are:
- No runtime fidelity/cost tradeoffs beyond merged pass and fidelity levels (yet)
- No validation against historical ground truth (yet)
---

## Cognitive Depth at High Fidelity

At `--fidelity high`, agents get additional cognitive architecture features:

**THINK vs SAY separation**: Prompts explicitly distinguish between internal monologue (raw, honest thoughts) and public statement (socially filtered). Agents with high agreeableness might have a large gap between what they think and what they say - that's interesting data.

**Repetition detection**: If an agent's reasoning is too similar to their previous timestep (>70% trigram overlap), they get a prompt nudge forcing them to go deeper. This prevents the stale convergence where agents just repeat "save money, learn AI, find backup work" verbatim for 5 timesteps.

These features are always-on at high fidelity, adding cognitive realism without additional configuration.

---

## Scenarios You Can Run Today

To make it concrete, here are scenarios that work right now with no additional development:

- US households responding to a streaming service price increase
- Japanese employees adapting to return-to-office mandates
- Indian consumers in multiple cities evaluating a new fintech app
- Brazilian families weighing migration decisions
- UK residents responding to congestion pricing expansion
- German citizens reacting to energy policy changes
- Mixed urban/rural populations facing a natural disaster
- Multi-generational households navigating technology adoption
- Professional networks processing industry disruption news
- Religious communities responding to doctrinal changes
- Parent networks reacting to school policy updates
- Couples having conversations that shift their positions
- Workplace discussions that change minds
- Social media dynamics where public discourse influences individuals
- Crisis scenarios that evolve over days/weeks with new developments
- Any population, any country, any event, any outcome structure

The core simulation engine is complete through Phase F - households, networks, timelines, conversations, social posts, cognitive architecture, fidelity tiers, and results export.

---

## What You Get Out

After simulation runs, you have:

**Position distributions**: What fraction of the population supports, opposes, or remains neutral? Segmented by any attribute - how do young people differ from old? Urban from rural? High-income from low-income?

**Sentiment trajectories**: How did emotional response evolve over time? Did initial negativity soften? Did enthusiasm fade?

**Conviction patterns**: Where are the true believers vs. the persuadable middle? How does certainty correlate with position?

**Sharing behavior**: Who's talking about this? Which demographics amplify vs. stay silent?

**Reasoning traces**: The actual first-person reasoning each agent produced. Qualitative insight into why people think what they think.

**Network effects**: How did information flow? Which communities adopted early? Where did resistance cluster?

**Conversation impact**: Which conversations changed minds most? Ranked by sentiment and conviction delta. See exactly what was said that moved people.

**Elaborations export**: Flattened CSV with every agent's demographics, outcomes, and reasoning - ready for downstream analysis, clustering, or visualization.

Those are Phases E and F. What's here now is Phases A through D - the core simulation engine with households, networks, timelines, conversations, and social posts.
**Social posts timeline**: Every public statement made during the simulation, with agent name, position, and sentiment.
30 changes: 15 additions & 15 deletions docs/simulation-v2-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1167,27 +1167,27 @@ Ship this alone. Every simulation immediately feels more human, and the accounta
- Day phase templates (optional, adapts to timestep unit)
- Social posts + public discourse aggregation

### Phase E: Cognitive Architecture (~1.5 weeks)
### Phase E: Cognitive Architecture — COMPLETE ✓

**Files:** `reasoning.py`, `engine.py`, `persona.py`
**Files:** `reasoning.py`, `engine.py`, `text_utils.py`

- Emotional trajectory rendering (all tiers — deterministic string formatting)
- Conviction self-awareness (all tiers — deterministic)
- THINK vs SAY separation (high fidelity — schema change)
- Repetition detection + deepening nudge (high fidelity — string overlap check)
- Emotional trajectory rendering (all tiers — deterministic string formatting)
- Conviction self-awareness (all tiers — deterministic)
- THINK vs SAY separation (high fidelity — prompt-only, uses `reasoning` field)
- Repetition detection + deepening nudge (trigram Jaccard >70% threshold)
- ~~Episodic/semantic memory consolidation~~ — **Omitted**

### Phase F: Fidelity Tiers + Results (~1.5 weeks)
### Phase F: Fidelity Tiers + Results — COMPLETE ✓

**Files:** `engine.py`, `reasoning.py`, CLI, `results/` module
**Files:** `engine.py`, `reasoning.py`, `aggregation.py`, CLI

- `--fidelity low|medium|high` flag on `SimulationRunConfig` (runtime choice, not scenario-intrinsic)
- Fidelity-gated feature inclusion (conversations, cognitive features, memory depth)
- Exploratory outcome export (structured JSON/CSV for downstream DS — no built-in clustering)
- Conversation analysis (impact, themes, state changes)
- Enhanced segment breakdowns
- `--fidelity low|medium|high` flag on `SimulationRunConfig`
- Fidelity-gated feature inclusion (conversations, memory depth, peer limits 5/5/10)
- Exploratory outcome export (`elaborations.csv` with agent demographics + all outcomes)
- Conversation analysis (`compute_most_impactful_conversations` — ranks by sentiment+conviction delta)
- ✅ Social posts export (`social_posts.json`)

### Phase G: Backtesting Harness (~2 weeks)
### Phase G: Backtesting Harness — DEFERRED

**Files:** `tests/`, new `extropy/validation/` module

Expand All @@ -1199,4 +1199,4 @@ Ship this alone. Every simulation immediately feels more human, and the accounta

**Total estimated: ~13 weeks for full v2.**

Phase A alone (~1.5 weeks) is a massive improvement with zero risk — named agents, temporal awareness, full memory, intent accountability, and macro feedback. Each subsequent phase is independently shippable and testable.
**Status: Phases A-F COMPLETE.** Phase G (backtesting) is deferred — requires curating ground-truth datasets for historical scenarios.
155 changes: 155 additions & 0 deletions extropy/simulation/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,158 @@ def compute_conversation_stats(
"total_messages": total_messages,
"avg_turns": round(avg_turns, 2),
}


def compute_most_impactful_conversations(
study_db: StudyDB,
run_id: str,
max_timesteps: int,
top_n: int = 10,
) -> list[dict[str, Any]]:
"""Identify most impactful conversations by state change magnitude.

Impact is measured by the sum of absolute sentiment and conviction changes
for both participants.

Args:
study_db: Study database connection
run_id: Simulation run ID
max_timesteps: Maximum timestep for iteration
top_n: Number of top conversations to return

Returns:
List of top conversations with impact scores
"""
scored_conversations: list[tuple[float, dict[str, Any]]] = []

for timestep in range(max_timesteps):
convs = study_db.get_conversations_for_timestep(run_id, timestep)

for conv in convs:
impact = 0.0

# Initiator state change
init_change = conv.get("initiator_state_change")
if init_change:
if init_change.get("sentiment") is not None:
impact += abs(init_change["sentiment"])
if init_change.get("conviction") is not None:
impact += abs(init_change["conviction"])

# Target state change (if not NPC)
target_change = conv.get("target_state_change")
if target_change:
if target_change.get("sentiment") is not None:
impact += abs(target_change["sentiment"])
if target_change.get("conviction") is not None:
impact += abs(target_change["conviction"])

if impact > 0:
scored_conversations.append((impact, conv))

# Sort by impact descending and take top N
scored_conversations.sort(key=lambda x: x[0], reverse=True)

return [
{
"impact_score": round(score, 3),
"timestep": conv.get("timestep"),
"initiator_id": conv.get("initiator_id"),
"target_id": conv.get("target_id"),
"target_is_npc": conv.get("target_is_npc", False),
"initiator_state_change": conv.get("initiator_state_change"),
"target_state_change": conv.get("target_state_change"),
"message_count": len(conv.get("messages", [])),
}
for score, conv in scored_conversations[:top_n]
]


def export_elaborations_csv(
state_manager: StateManager,
agent_map: dict[str, dict[str, Any]],
output_path: str,
) -> int:
"""Export open-ended elaborations as flattened CSV for DS workflows.

Exports one row per agent with their demographics and outcome values.

Args:
state_manager: State manager with final states
agent_map: Mapping of agent_id to agent attributes
output_path: Path to write CSV file

Returns:
Number of rows exported
"""
import csv

final_states = state_manager.export_final_states()

if not final_states:
return 0

# Determine all outcome keys across agents
outcome_keys: set[str] = set()
for state in final_states:
if state.get("outcomes"):
outcome_keys.update(state["outcomes"].keys())

# Define columns: agent_id, demographics, state fields, outcomes
demographic_fields = [
"first_name",
"age",
"gender",
"race_ethnicity",
"state",
"education_level",
"occupation_sector",
"household_income",
]

state_fields = [
"position",
"sentiment",
"conviction",
"will_share",
"public_statement",
"raw_reasoning",
]

outcome_fields = sorted(outcome_keys)

# Build header
header = ["agent_id"] + demographic_fields + state_fields + outcome_fields

rows_written = 0
with open(output_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(header)

for state in final_states:
agent_id = state["agent_id"]
agent = agent_map.get(agent_id, {})

row = [agent_id]

# Demographics
for field in demographic_fields:
row.append(agent.get(field, ""))

# State fields
for field in state_fields:
value = state.get(field, "")
# Truncate long text for CSV
if isinstance(value, str) and len(value) > 500:
value = value[:500] + "..."
row.append(value)

# Outcomes
outcomes = state.get("outcomes", {})
for key in outcome_fields:
row.append(outcomes.get(key, ""))

writer.writerow(row)
rows_written += 1

return rows_written
30 changes: 29 additions & 1 deletion extropy/simulation/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
compute_outcome_distributions,
compute_timeline_aggregates,
compute_conversation_stats,
compute_most_impactful_conversations,
export_elaborations_csv,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -1475,6 +1477,8 @@ def _get_peer_opinions(self, agent_id: str) -> list[PeerOpinion]:
observability where agents can only perceive what peers have explicitly
shared or posted. Silent position changes are invisible.

Peer limit is fidelity-gated: low=5, medium=5, high=10.

Args:
agent_id: Agent ID

Expand All @@ -1484,7 +1488,10 @@ def _get_peer_opinions(self, agent_id: str) -> list[PeerOpinion]:
neighbors = self.adjacency.get(agent_id, [])
opinions = []

for neighbor_id, edge_data in neighbors[:5]: # Limit to 5 peers
# Fidelity-gated peer limit: low/medium=5, high=10
max_peers = 10 if self.config.fidelity == "high" else 5

for neighbor_id, edge_data in neighbors[:max_peers]:
neighbor_state = self.state_manager.get_agent_state(neighbor_id)

# Only include peer if they actively shared (observable behavior)
Expand Down Expand Up @@ -2013,6 +2020,27 @@ def _export_results(self) -> None:
with open(self.output_dir / "social_posts.json", "w") as f:
json.dump(all_posts, f, indent=2)

# Export most impactful conversations
if conv_stats["total_conversations"] > 0:
impactful = compute_most_impactful_conversations(
study_db=self.study_db,
run_id=self.run_id,
max_timesteps=self.scenario.simulation.max_timesteps,
top_n=10,
)
if impactful:
meta["most_impactful_conversations"] = impactful

# Export flattened elaborations CSV for DS workflows
csv_path = self.output_dir / "elaborations.csv"
rows_exported = export_elaborations_csv(
state_manager=self.state_manager,
agent_map=self.agent_map,
output_path=str(csv_path),
)
if rows_exported > 0:
meta["elaborations_csv_rows"] = rows_exported

with open(self.output_dir / "meta.json", "w") as f:
json.dump(meta, f, indent=2)

Expand Down
Loading