Architecture¶
Three layers of infrastructure¶
Both loops — the longitudinal simulation loop inside a run, the controllable research loop across versions — sit on one infrastructure with three layers. Each layer owns different files, and knowing which layer a question belongs to is usually the fastest way to find the code.
| layer | what it does | where it lives |
|---|---|---|
| workflow | the sv-* skills. Each reads the artifacts of earlier stages, writes exactly one validated artifact of its own, and stops at a checkpoint |
.claude/skills/sv-*/SKILL.md; shared helpers in skills/sv_workspace.py (study dirs, versions, branches) and skills/sv_grounding.py |
| service | resolves P and the exogenous part of E from real-world data, as typed MCP tools called at build time — the population service (sv-user-pool) and the event service (sv-event-tool) |
deployed server-side, reached over MCP; results are materialized into population/ and environment/ and the runtime never calls out. See Data Services |
| governance | records what each stage did, which data it used, and what the researcher approved | grounding/grounding.json, resources.json, versions.json, and the events table in trajectory/study.duckdb |
This page is about what those layers stand on: the runtime.
Core runtime and study layer¶
The runtime 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[] — the whole step in, 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¶
LongitudinalSimulator.run() (socioverse/engine/loop.py) builds P once, resets
E, and then runs the same five phases every step, in this order:
build() → Persona[] # P: built once, persistent ids, fixed thereafter
reset() # E_0 — recorded as the step-0 panel rows
loop t = 1..N:
① exogenous update advance_to(t) # scheduled events + broadcasts mutate E
② observe observe_batch() → Observation[] # per-agent 4-quadrant view
③ decide decide_batch() → Action[] # = B_t = f(P, E_t, memory)
④ apply apply(actions) # endogenous feedback: E_{t+1} = g(E_t, B_t)
⑤ record collect() + store.record() # panel rows + metrics + fired events
| phase | the call | why it sits there |
|---|---|---|
| ① exogenous update | env.advance_to(t) → fired events |
an intervention enters before anyone observes, so every agent in the round decides against one and the same state |
| ② observe | env.observe_batch(ids, t, round) |
the only channel through which E reaches f — macro/local × physical/information |
| ③ decide | decision.decide_batch(obs, memories) |
the whole step in, typed out: one Action per agent, LLM or rule |
| ④ apply | env.apply(actions) |
on a from-scratch study this is a pure function of the actions passed in, which is what makes branch replay exact |
| ⑤ record | collector.collect(), store.record(...) |
memory window first, then one panel row per agent, one metrics row, and the events fired in ① |
Two variations on the same five phases:
interaction_rounds = K > 1repeats ②–④ K times inside the step; only the last round's actions reach the memory window and the panel row. See Multi-Round Interaction.- A branch replays ①, ④ and ⑤ from its parent version's stored actions
(
_replay_inherited) and starts calling the decision model only at its fork step. See Intervention & Branching.
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 information —
Broadcast/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
MessageBusplusinteraction_rounds(not O(N²) chatter): posts land on the bus, neighbours read them through the study'sInteractionStructure(engine/messaging.py,env_layers/neighbor_feed.py). - Bring-your-own data — a
ResourceManifestplus genericFilePopulationProvider/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:
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.