Skip to content

Architecture

SocioVerse is split into a Core runtime you reuse and a study layer you write. Core defines the contract and drives the loop; a study supplies the implementations. You never modify the engine to add a study.

socioverse/                CORE RUNTIME — reused, you don't touch it
├── schemas/               typed contracts (StudySpec, EnvironmentBundle, PopulationBundle,
│                          SimulationConfig, Observation, Action, TrajectoryRecord, …)
├── abc/                   the interfaces a study implements (EnvironmentProvider,
│                          PopulationProvider, DecisionModel, MetricCollector)
│                          + Simulator / TrajectoryStore / Reporter / MessageBus
├── engine/                LongitudinalSimulator (the loop), AgentMemory, registry,
│                          InMemoryMessageBus, build_simulator (assembly)
├── env_layers/            information-axis helpers (broadcast delivery, neighbour feed)
├── io/duckdb_store.py     the queryable panel / metrics / events / messages store
├── providers.py           generic File / MCP population providers
└── validation.py          validate_handoff — the strict stage-boundary guard

studies/<id>/              STUDY LAYER — what you write (or generate)
└── model.py + artifacts   four interface implementations + study.yaml / env / pop / sim

The four interfaces

A study is complete when it implements four abstract base classes from socioverse/abc/:

interface responsibility key methods
EnvironmentProvider own the world state E reset, advance_to(t) (exogenous), observe_batch() (4-quadrant views), apply(actions) (endogenous)
PopulationProvider build the fixed pool P build()Persona[] with deterministic persistent ids + interaction structure
DecisionModel compute B = f(P, E) decide_batch(observations)Action[] — batched, typed
MetricCollector measure each step collect() → the step's aggregate metrics

Core provides the rest: the LongitudinalSimulator loop, the trajectory store, the reporter, the message bus, agent memory.

The runtime path

build()  → Persona[]                 # P: built once, persistent ids, fixed thereafter
reset()                              # E_0
loop t = 1..N:
    advance_to(t)                    # exogenous: scheduled events + broadcasts mutate E
    observe_batch() → Observation[]  # per-agent 4-quadrant view (macro/local × physical/info)
    decide_batch()  → Action[]       # = B, batched decision (LLM or rule)
    apply(actions)                   # endogenous feedback: E_t → E_{t+1}
    collect() → metrics              # measure
    record()  → DuckDB               # persist panel rows + metrics + events

The hand-offs between these calls are typed (Persona[] → Observation[] → Action[] → metrics), with every type defined in socioverse/schemas/. Schemas are a contract layer everything references — not a stage that data "flows through".

Scenario hooks built into Core

Three recurring needs are already solved in Core, so studies declare rather than implement them:

  • Audience-scoped informationBroadcast / InformationProgram (env_layers/information.py): audience: "all" reaches every agent's macro-information quadrant; a selector dict targets matching agents' local-information quadrant.
  • Large-scale agent interaction — a mediated MessageBus plus interaction_rounds (not O(N²) chatter): posts land on the bus, neighbours read them through the study's InteractionStructure (engine/messaging.py, env_layers/neighbor_feed.py).
  • Bring-your-own data — a ResourceManifest plus generic FilePopulationProvider / McpPopulationProvider (socioverse/providers.py) for populations sourced from files or external services.

One name clash worth internalizing

  • The Core Engine is the reused LongitudinalSimulator — the loop.
  • An engine-seam is study-internal glue that wraps a legacy simulator (see Wrap a Legacy Simulator). Core never calls it; only the study's own providers reference it.

Registration & assembly

Studies register their implementations with the @register(kind, key) decorator (socioverse.engine.registry), and the study's artifacts point at them by key (provider_ref, decision_ref, collector_ref, …). Assembly is then generic:

build_simulator(env_bundle=, pop_bundle=, sim_config=, store_path=)

resolves every *_ref against the registry, instantiates the providers, wires the DuckDB store, and returns a ready LongitudinalSimulator. That is the whole integration surface — no engine edits, no plugin system to learn.