LLM Clients & Dry Runs¶
In SocioVerse the LLM sits in exactly one place: the decision model.
decide_batch(observations, memories) is where B = f(P, E) gets computed,
and whether that computation is a rule, a statistical model, or LLM calls is
the study's choice. Everything else — environment, population, store,
reports — runs identically either way.
Configuration¶
LLM-driven decision models read their settings from the environment or from
the gitignored .env (see
Installation
for the lookup order). Real-LLM runs need the llm extra
(pip install -e ".[llm]"), which brings the OpenAI-compatible client.
| variable | meaning |
|---|---|
SV_LLM_API_KEY |
the key for the simulated agents' calls (OPENAI_API_KEY honored as fallback) |
SV_LLM_BASE_URL |
optional endpoint override — any OpenAI-compatible gateway (unset: the official OpenAI API) |
SV_LLM_MODEL |
optional; read by scripts/demo_offline.py, the Chicago adapter and studies whose simulation.json names no model. The shipped studies pin decision_args.model |
These variables configure the simulation's LLM only. The coding agent that
runs the /sv-* skills (Claude Code, or the experimental Codex CLI path) uses
its own login — see
what powers the workflow skills.
The call pattern¶
The engine hands decide_batch the whole step — every agent's
observation — and expects one Action per agent back. "Batch" describes that
interface, not how the LLM is called. A well-behaved LLM decision model:
- renders each observation's 4-quadrant view into the prompt
(
Observation.renderedis the conventional slot for the prepared text); - conditions on the agent's
AgentMemory(a rolling window of its own past actions — "I already moved twice, I'll stay"); - in a from-scratch study, calls the LLM once per agent, sending the
step's calls concurrently on a bounded thread pool (
max_workersindecision_args), and parses — or falls back to the rule — per agent (seestudies/opinion_diffusion/model.py). It never asks for one JSON reply covering the whole population: long structured replies get truncated, and one bad reply fails the step; - in a legacy adapter, keeps the wrapped engine's own call pattern (for example the Chicago model's archetype-grouped calls), so its validated dynamics are unchanged;
- returns typed
Actions thatapply()can fold back into E.
Deterministic dry runs¶
You should be able to prove the plumbing — loop, artifacts, store, metrics — without spending a token.
The general path: llm_kind: "scripted". Every from-scratch template
registers one decision model with two modes, switched in
simulation/simulation.json without a code change:
"scripted" runs the study's deterministic rule (free, exactly reproducible
from the seed); "openai" role-plays each agent through the SV_LLM_*
channel and falls back, for that agent only, when a reply does not parse (to
the rule in opinion_diffusion; campus_dining_choice keeps the agent's
previous choice).
scripts/demo_offline.py runs opinion_diffusion this way, and /sv-run
offers the scripted mode as the free dry run. A new study built by
/sv-build-model follows the same pattern, so the rule always stays
available as the fallback path.
Rule-based decision models need nothing at all: the decision is a pure function of the observation.
Wrapped legacy engines: a stand-in client. A study that wraps a legacy
simulator keeps that engine's LLM client, so it dry-runs by injecting a
deterministic stand-in instead. The shipped instance is
chicago_schelling's DeterministicLLMClient
(studies/chicago_schelling/adapter/fake_llm.py), passed as
llm_client=DeterministicLLMClient() to build_chicago_simulator. It
exists only inside that adapter; it is not a general client for other
studies. Parity tests use it: the wrapped study asserts it reproduces the
original trajectory on the same seed, with the stand-in removing the only
source of nondeterminism.
Reproducibility, honestly stated¶
Live LLM runs are not reproducible
Real LLM calls sample at nonzero temperature: two live runs of the same study will differ. The reproducible objects are (a) dry runs — seeded and deterministic end to end, and (b) any recorded run — its panel store is permanent, and branch replay replays stored actions exactly rather than re-asking the LLM.
Cost control¶
/sv-runshows the run configuration and asks for confirmation before spending — the spend gate.- Prefer dry runs while you iterate on artifacts; go live once per meaningful configuration.
- Branch replay inherits the parent version's steps before the fork step (no LLM), so a counterfactual pays only for the steps after it.