Overview — two loops, one infrastructure¶
SocioVerse expresses a social simulation as one abstraction, B = f(P, E), and
runs it as two loops over one infrastructure:
- the longitudinal simulation loop moves a run forward one step at a time,
E_{t+1} = g(E_t, B_t); - the controllable research loop moves the study forward one edit at a
time,
σ_{k+1} = h(a_k, σ_k); - the social science agentic infrastructure carries both — the skills that author a study, the services that ground P and E in real data, and the records that make a finished study auditable and re-runnable.
This page gives the formulas and the map. Each loop then has its own page with the mechanism, the functions that implement it, and what it writes to disk.
flowchart LR
subgraph INFRA["Agentic infrastructure — workflow · services · governance"]
SIGMA["study state<br/>σ<sub>k</sub> = (P, E<sub>0:T</sub>, f, Θ)"]
end
SIGMA --> S1
subgraph LOOP["Longitudinal simulation loop — one step t"]
S1["① exogenous update<br/>advance_to(t)"] --> S2["② observe<br/>observe_batch"]
S2 --> S3["③ decide<br/>decide_batch = f"]
S3 --> S4["④ apply<br/>apply = g"]
S4 --> S5["⑤ record<br/>panel + metrics"]
end
S5 -.->|"t + 1"| S1
S5 --> T[("trajectory<br/>study.duckdb")]
T -->|"edit a<sub>k</sub> — branch or version"| SIGMA
1. B = f(P, E)¶
The abstraction is borrowed from Kurt Lewin's field theory: behaviour is a function of the person and their environment. Its three terms are independently replaceable, which is what lets one runtime host Schelling segregation and macro-economic nowcasting alike:
- P may be synthesized from census marginals, drawn from a real-platform user pool, or constructed by the study itself;
- E may be grounded in real event streams or designed as a stylized experiment;
- f may be a classical ABM rule, an LLM prompt chain, a trained RL policy, or a hybrid that assigns different implementations to different agent groups.
Longitudinal, not cross-sectional¶
Frameworks like AgentSociety or OneSim excel at cross-sectional studies: many single-round experiments, often over freshly sampled crowds. SocioVerse targets the complementary question — how do the same people change over time?
That inverts two design choices:
| cross-sectional | SocioVerse (longitudinal) | |
|---|---|---|
| population | resampled per experiment | fixed pool, persistent ids |
| environment | mostly static per round | dynamic — the thing that changes |
| output | outcome distributions | panel data: one row per agent per step |
The persistent agent id is the longitudinal key: it is what lets you ask
"show me household x-042 across all twelve steps" instead of only
"what did the aggregate do".
The two-axis environment¶
E is dynamic along two independent axes, giving each agent a four-quadrant view of the world at every step:
| macro | local | |
|---|---|---|
| physical | the world's global state | the agent's neighbourhood state |
| information | broadcasts everyone receives | feeds & messages scoped to this agent |
Two kinds of change drive E forward, and they are two separate calls in the
step: exogenous change is the schedule you declared in the environment
bundle (advance_to(t)), endogenous change is the feedback of the agents'
own actions (apply(actions)).
2. The longitudinal simulation loop¶
Inside one run, the environment carries the consequences of behaviour forward:
e_t^i = ω(E_t, p_i) # the agent's four-quadrant observation
b_{i,t} = f(p_i, e_t^i, m_{i,t}) # its typed action, given a bounded memory window
B_t = { b_{i,t} }_{i=1..N}
E_{t+1} = g(E_t, B_t)
Step 0 materializes P under the declared seed and builds E_0, records the
initial panel rows, and computes no decision. Every later step runs the same
five phases in a fixed order, implemented by
LongitudinalSimulator.run() in
socioverse/engine/loop.py:
| phase | call in run() |
what it does |
|---|---|---|
| ① exogenous update | env.advance_to(t) |
fires the events and broadcasts scheduled for t |
| ② observe | env.observe_batch(ids, t, r) |
projects E_t onto each agent (modality × scope) |
| ③ decide | decision.decide_batch(obs, memories) |
computes B_t = f(P, E_t, memory), one typed action per agent |
| ④ apply | env.apply(actions) |
folds the behaviours back in: E_{t+1} = g(E_t, B_t) |
| ⑤ record | store.record(...) + memory push |
one panel row per agent, one metrics row, the fired events |

Three properties of this ordering are deliberate:
- An intervention enters at phase ①, before anyone observes, so every agent of a round decides against the same state.
- Decisions are taken per step.
decide_batchreceives the whole step's observations at once and returns one action per agent, so every agent of a step decides against the sameE_t. How the LLM is called inside is the model's choice: from-scratch studies send one call per agent, concurrently; a legacy adapter keeps its engine's own pattern (the call pattern). - Every step is recorded as it is produced. Because
applyis a function of the recorded actions, a run can be re-executed from its stored rows with no decision model at all — which is what a branch inherits.
→ The Simulation Loop — the phases in detail, the typed
hand-offs (Persona[] → Observation[] → Action[] → metrics, all Pydantic
models in socioverse/schemas/), the memory window, and the DuckDB tables.
3. The controllable research loop¶
A longitudinal study is rarely a single run. The study itself is an object you edit. Let the study state be
An edit a_k = (c, u) names the components c ⊆ {P, E, f, Θ} it touches
and the update u applied to them. Either the researcher or the agent may
raise it, and h applies it to give the next version. Three cases exhaust h:
c |
outcome | what runs | in the code |
|---|---|---|---|
| ∅ | unchanged | nothing | the current version stays current |
{E} after a fork step t* |
branch (分支) | steps 0..t*-1 are replayed from the parent's stored actions (no LLM, no spend), only t*..T are computed |
create_branch(study_dir, note, fork_step=…) |
| anything else | version (版本) | a cold run from step 0 | create_version(study_dir, note, …) |
Both live in skills/sv_workspace.py, and both append an entry to the study's
versions.json manifest with id, parent, note, kind ("branch" or
"version"), fork_step, and the re-entry stage. Creating a version first
freezes the whole live workspace into versions/v<k>/, so every earlier
version stays inspectable and re-runnable.
A branch only earns its replay if nothing but the post-fork environment
differs. check_branch_invariant() verifies that before the run, and on a
violation it downgrades the branch to a version rather than failing: the
run goes cold from step 0, the results stay correct, and the reason is recorded
on the manifest entry.
fork means something else here
In this documentation fork is reserved for the study level: /sv-init
forks a matching reference study into a new study of your own (Path A).
Within a study you create branches and versions, never forks.
→ The Research Loop — the version gate, the manifest
fields, and how to compare versions.
→ Intervention & Branching — what a
ScheduledEvent or Broadcast declares, how replay reproduces the parent
exactly, and how to read Δ_t(δ) off two branches.
4. The social science agentic infrastructure¶
Both loops run on one infrastructure, organized in three layers.
| layer | what it does | where it lives |
|---|---|---|
| workflow | decomposes the research lifecycle into skills; each reads earlier artifacts, writes exactly one validated artifact, and pauses at a checkpoint | .claude/skills/ — sv-init, sv-build-model, sv-build-environment, sv-build-population, sv-run, sv-report (with sv-lit / sv-paper), sv-iterate |
| services | resolve P and E from real-world data at build time, as typed MCP tools rather than datasets copied into the study | the population service (sv-user-pool) and the event service (sv-event-tool) |
| governance | records what each stage did, which data it used, and what you approved | grounding/grounding.json, resources.json, versions.json, the events table in trajectory/study.duckdb |

Two consequences are worth stating plainly:
- The runtime fetches nothing. Services are called while a study is being built; their results are materialized into the environment and population bundles. A run therefore is self-contained and repeatable.
- Load-bearing numbers cite a source. Every study carries a grounding
sidecar (
grounding/grounding.json) holding facts with sources, modeling references, and declared assumptions. What cannot be sourced is declared rather than silently invented.
→ Data Services — the two MCP services and what they materialize. → Grounding & Provenance — the sidecar's fields and how the build stages extend it.
Where each concept lives¶
| in the formula | in the code |
|---|---|
P — the fixed population |
socioverse/abc/population.py (PopulationProvider.build(seed)); bundle population/population.json + population/roster.jsonl |
E — the dynamic environment |
socioverse/abc/environment.py, reusable layers in socioverse/env_layers/; bundle environment/environment.json |
f — the behaviour function |
socioverse/abc/decision.py (DecisionModel.decide_batch) |
g — the environment update |
EnvironmentProvider.apply(actions) |
ω — the observation operator |
EnvironmentProvider.observe_batch(ids, t, round) |
m — the memory window |
socioverse/engine/memory.py (AgentMemory, 8 steps by default) |
B_t — the step's behaviours |
Action in socioverse/schemas/runtime.py; the panel table's action_kind / action_payload |
Θ — the run settings |
simulation/simulation.json (seed, n_steps, interaction_rounds, the *_ref / *_args bindings) + resources.json |
| the simulation loop | socioverse/engine/loop.py (LongitudinalSimulator.run) |
| the trajectory | socioverse/io/duckdb_store.py → trajectory/study.duckdb (panel / metrics / events / messages) |
| σ and its versions | versions.json + skills/sv_workspace.py (create_version, create_branch, check_branch_invariant) |
| the typed contracts | socioverse/schemas/ — validated on write and on read at every stage boundary |
Next: Anatomy of a Study — what's inside studies/<id>/.