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
260 changes: 260 additions & 0 deletions docs/capabilities.md

Large diffs are not rendered by default.

19 changes: 11 additions & 8 deletions docs/simulation-v2-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Decisions confirmed before implementation. These override any conflicting detail
| 5 | Timeline merge semantics | Timeline entry overrides base event for that timestep. |
| 6 | DB schema for new artifacts | Define conversations/posts/action_history tables before Phase D. |
| 7 | Name data | Local SSA baby names + Census surnames, bundled CSVs (~500KB), US-only. Non-US via country-specific CSVs later behind same interface: `generate_name(gender, ethnicity, birth_decade, country="US")`. |
| 8 | Conformity/threshold mechanics | Soft prompt signal only (inject local adoption ratio + conformity phrasing). No hard numeric gate. |
| 8 | Conformity/threshold mechanics | Soft prompt signal only (conformity self-awareness + peer opinions + mood rendering). No explicit ratios or hard numeric gates. |
| 9 | Backtesting ground-truth | Define one validation dataset schema before Phase G. |

### Phase-Specific Decisions
Expand Down Expand Up @@ -1034,12 +1034,12 @@ The 12 tenets below define what a high-fidelity simulation must satisfy. Tenets
| 3 | Social hierarchy & influence topology | **Partial** | Structural role edges, degree multipliers in network config, edge weight hierarchy | No explicit power-law degree enforcement; no hub/opinion-leader generation. Need scenario-dependent centrality targets |
| 4 | Behavioral heterogeneity | **Partial** | Big Five personality, risk tolerance, institutional trust, cognitive attributes vary per agent | Decision-policy heterogeneity relies entirely on LLM interpretation of persona. Need explicit behavioral parameters (conformity threshold, action inertia) as agent attributes |
| 5 | Temporal dynamics & decay | **Strong** | Conviction decay, temporal prompt awareness, emotional trajectory, memory history, scenario timeline with evolving events | Need intent→action accountability loop (surface prior action_intent, ask about follow-through) |
| 6 | Social contagion & network effects | **Partial** | Network propagation, share modifiers, conversation system, aggregate mood, peer opinions | No explicit threshold/complex contagion. Need per-agent conformity parameter + local adoption ratio injection |
| 6 | Social contagion & network effects | **Partial** | Network propagation, share modifiers, conversation system, aggregate mood, peer opinions | No explicit threshold/complex contagion. Need per-agent conformity parameter + conformity-aware prompt phrasing |
| 7 | Friction & transaction costs | **Weak** | option_friction on outcomes, bounded confidence mechanics | Biggest gap. Need explicit intent→behavior pipeline: surface what agent planned vs what they actually did. Friction emerges from agent constraints but isn't tracked or measured |
| 8 | Bounded rationality & heuristics | **Strong** | LLM is inherently a bounded rationality engine. Persona attributes (education, digital literacy, neuroticism) shape heuristic use. Agents satisfice, anchor, exhibit status quo bias naturally | Could strengthen with explicit bias nudges in prompts for specific attributes |
| 9 | Environmental & contextual sensitivity | **Partial** | Scenario timeline handles exogenous shocks. Channel templates adapt to agent demographics | Need ambient macro context in every prompt (economic conditions, cultural moment). Need previous-timestep macro summary injection |
| 10 | Identity & group membership | **Partial** | race_ethnicity, political_orientation, religious_affiliation in persona. Social role edges create in-group connections | Need identity-threat framing: when the scenario threatens a group identity, persona rendering should explicitly flag it as identity-relevant |
| 11 | Preference interdependence | **Partial** | Aggregate mood rendering ("most people I know are doing X"), peer opinions, social posts. Bandwagon/FOMO effects emerge from context | Need explicit local adoption ratio in prompt: "X% of people you know have already done Y." Makes interdependence concrete, not just vibes |
| 11 | Preference interdependence | **Partial** | Aggregate mood rendering ("most people I know are doing X"), peer opinions, social posts. Bandwagon/FOMO effects emerge from context | Named peer opinions + local mood + macro summary provide social pressure without omniscient ratio framing |
| 12 | Macro-micro feedback loops | **Partial** | Micro→macro works (agent decisions → aggregate stats). Timeline handles exogenous macro shifts | No endogenous macro: agent behavior doesn't produce emergent macro variables that feed back. Need at minimum: inject previous timestep aggregates as ambient context |

### Concrete Fixes to Close Gaps
Expand Down Expand Up @@ -1087,16 +1087,19 @@ These are the minimum changes needed to move every tenet to **Strong**. Listed i

**Soft conformity/threshold behavior:**
- Add `conformity` as a standard personality attribute (0-1 scale, correlated with agreeableness). Sampled at population creation time.
- At prompt build time, compute **local adoption ratio**: what fraction of this agent's network has already taken action (changed position, shared, etc.)
- Inject into prompt: "About 7 out of 10 people you know have already started making changes. You tend to [wait until most people around you have acted / act independently of what others are doing]." (phrasing depends on conformity level)
- This gives the LLM explicit threshold context without hardcoding a threshold formula. A high-conformity agent seeing 70% adoption will likely act. A low-conformity agent seeing the same might resist specifically because everyone else is doing it (contrarian behavior).
- Inject conformity self-awareness into prompt: "I tend to go along with what most people around me are doing" (high) or "I tend to form my own opinion regardless of what others think" (low). Mid-range agents get no explicit phrasing.
- Social pressure is conveyed through **existing mechanisms**, not explicit ratios:
- Named peer opinions: "My coworker Darnell thinks X"
- Local mood rendering: "Most people around me seem worried"
- Macro summary: "The general mood is shifting toward X"
- **Rationale:** People don't actually know "7 out of 10 contacts did X" — that's omniscient narrator framing. Real social pressure comes from specific conversations and vague impressions, which the peer opinion and mood systems already capture.

**Macro state feedback:**
- After each timestep, compute macro summary from TimestepSummary data:
- Position distribution rendered as "Most people are choosing X. A growing minority is doing Y."
- Sentiment trend: "The general mood is getting worse / stabilizing / improving."
- Exposure saturation: "Almost everyone has heard about this now."
- Action adoption rate: "About X% of people have already taken concrete action."
- Action momentum: "More and more people are taking action" / "Most people are still waiting"
- Inject this into every agent's next-timestep prompt as ambient context, rendered as what the agent would sense from media/social feeds, not raw numbers.
- This closes the macro→micro loop: agent decisions → aggregate stats → rendered as ambient context → influences next round of agent decisions.

Expand Down Expand Up @@ -1203,7 +1206,7 @@ Ship this alone. Every simulation immediately feels more human, and the accounta

- Scenario timeline: sequence of events at specified timesteps
- Timeline injection into agent prompts as "what's happened since last time"
- Local adoption ratio computed per agent ("7 out of 10 people you know have acted")
- Named peer opinions + local mood convey social pressure without explicit ratios
- Conformity-aware prompt rendering ("You tend to wait for others / act independently")
- Ambient scenario context field (`background_context` in ScenarioSpec)
- Macro state feedback: timestep aggregates rendered as ambient vibes in next prompt
Expand Down
2 changes: 2 additions & 0 deletions extropy/cli/commands/extend.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ def do_hydration():
attributes=bound_attrs,
sampling_order=sampling_order,
sources=sources,
household_config=household_config,
name_config=name_config,
)
merged_spec = base.merge(extension_spec)

Expand Down
31 changes: 31 additions & 0 deletions extropy/cli/commands/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def scenario_command(
"-o",
help="Output path (defaults to {population_stem}.scenario.yaml)",
),
timeline: str = typer.Option(
"auto",
"--timeline",
help="Timeline mode: auto (LLM decides), static (single event), evolving (multi-event)",
),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
):
"""
Expand Down Expand Up @@ -116,6 +121,8 @@ def on_progress(step: str, status: str):
def run_pipeline():
nonlocal result_spec, validation_result, pipeline_error
try:
# Convert timeline mode (auto -> None for LLM decision)
timeline_mode = None if timeline == "auto" else timeline
result_spec, validation_result = create_scenario(
description=scenario_desc,
population_spec_path=population,
Expand All @@ -124,6 +131,7 @@ def run_pipeline():
network_id=network_id,
output_path=None, # Don't save yet
on_progress=on_progress,
timeline_mode=timeline_mode,
)
except Exception as e:
pipeline_error = e
Expand Down Expand Up @@ -214,6 +222,29 @@ def run_pipeline():
)
console.print()

# Timeline info (Phase C)
if result_spec.timeline:
console.print(f"[bold]Timeline:[/bold] {len(result_spec.timeline)} events")
for te in result_spec.timeline[:3]:
desc = te.description or te.event.content[:40]
console.print(f" • t={te.timestep}: {desc}")
if len(result_spec.timeline) > 3:
console.print(f" [dim]... and {len(result_spec.timeline) - 3} more[/dim]")
console.print()
else:
console.print("[bold]Timeline:[/bold] static (single event)")
console.print()

# Background context (Phase C)
if result_spec.background_context:
ctx_preview = (
result_spec.background_context[:60] + "..."
if len(result_spec.background_context) > 60
else result_spec.background_context
)
console.print(f"[bold]Background:[/bold] {ctx_preview}")
console.print()

# Validation Results
if validation_result.errors:
console.print(
Expand Down
7 changes: 7 additions & 0 deletions extropy/cli/commands/simulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ def simulate_command(
"-p",
help="PersonaConfig YAML for embodied personas (auto-detected if not specified)",
),
merged_pass: bool = typer.Option(
False,
"--merged-pass",
help="Use single merged reasoning pass instead of two-pass (experimental)",
),
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress progress output"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
debug: bool = typer.Option(
Expand Down Expand Up @@ -316,6 +321,7 @@ def on_progress(timestep: int, max_timesteps: int, status: str):
writer_queue_size=writer_queue_size,
db_write_batch_size=db_write_batch_size,
resource_governor=governor,
merged_pass=merged_pass,
)
simulation_error = None
except Exception as e:
Expand Down Expand Up @@ -352,6 +358,7 @@ def do_simulation():
writer_queue_size=writer_queue_size,
db_write_batch_size=db_write_batch_size,
resource_governor=governor,
merged_pass=merged_pass,
)
except Exception as e:
simulation_error = e
Expand Down
4 changes: 4 additions & 0 deletions extropy/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
# Event
EventType,
Event,
# Timeline
TimelineEvent,
# Exposure
ExposureChannel,
ExposureRule,
Expand Down Expand Up @@ -160,6 +162,8 @@
# Scenario - Event
"EventType",
"Event",
# Scenario - Timeline
"TimelineEvent",
# Scenario - Exposure
"ExposureChannel",
"ExposureRule",
Expand Down
24 changes: 24 additions & 0 deletions extropy/core/models/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ class SeedExposure(BaseModel):
)


class TimelineEvent(BaseModel):
"""A development in the scenario timeline.

Timeline events represent how a scenario evolves over time. For evolving
scenarios (crises, campaigns), multiple events occur at different timesteps.
Static scenarios (policy announcements) have no timeline events.
"""

timestep: int = Field(ge=0, description="When this development occurs")
event: Event = Field(description="The event content at this timestep")
exposure_rules: list[ExposureRule] | None = Field(
default=None,
description="Custom exposure rules; if None, reuses seed_exposure.rules with updated content",
)
description: str | None = Field(
default=None,
description="Human-readable context for this development",
)


# =============================================================================
# Interaction Model
# =============================================================================
Expand Down Expand Up @@ -285,6 +305,10 @@ class ScenarioSpec(BaseModel):

meta: ScenarioMeta
event: Event
timeline: list[TimelineEvent] | None = Field(
default=None,
description="Subsequent developments; None or empty = static scenario",
)
seed_exposure: SeedExposure
interaction: InteractionConfig
spread: SpreadConfig
Expand Down
21 changes: 21 additions & 0 deletions extropy/core/models/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,23 @@ class ReasoningContext(BaseModel):
default_factory=dict,
description="Mapping of agent_id → first name for resolving peer references",
)
# Phase C additions
timeline_recap: list[str] | None = Field(
default=None,
description="Bullet list of what's happened so far in the scenario",
)
current_development: str | None = Field(
default=None,
description="This timestep's new development (if any)",
)
observable_peer_actions: int | None = Field(
default=None,
description="Count of neighbors who visibly acted (shared/posted)",
)
conformity: float | None = Field(
default=None,
description="Agent's conformity attribute (0-1)",
)


# =============================================================================
Expand Down Expand Up @@ -390,6 +407,10 @@ class SimulationRunConfig(BaseModel):
default=None,
description="Max concurrent async reasoning calls (None = auto from RPM)",
)
merged_pass: bool = Field(
default=False,
description="Use single merged pass instead of two-pass reasoning (experimental)",
)

# Backward compat aliases
@property
Expand Down
20 changes: 17 additions & 3 deletions extropy/core/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,27 @@
def _clean_schema_for_tool(schema: dict) -> dict:
"""Clean a JSON schema for use as a tool input_schema.

Removes fields that aren't valid in tool input schemas
(like 'additionalProperties' in nested objects that Claude
doesn't support in tool definitions).
Anthropic structured outputs support additionalProperties: false but NOT
schema-valued additionalProperties (e.g. {"type": "number"}).

This function:
- Keeps additionalProperties: false (valid and useful)
- Strips additionalProperties when it's a dict/schema (not supported)
- Logs a warning when stripping schema-valued additionalProperties
"""
cleaned = {}
for key, value in schema.items():
if key == "additionalProperties":
if value is False:
# Keep additionalProperties: false - it's valid
cleaned[key] = value
elif isinstance(value, dict):
# Schema-valued additionalProperties not supported - strip with warning
logger.warning(
"Stripping schema-valued additionalProperties from tool schema "
"(not supported by Anthropic structured outputs)"
)
# Skip other truthy values (True, etc.)
continue
if isinstance(value, dict):
cleaned[key] = _clean_schema_for_tool(value)
Expand Down
Loading
Loading