Skip to content

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 a batched LLM call 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 (via the gitignored .env):

variable meaning
SV_LLM_API_KEY primary API key (OPENAI_API_KEY honored as fallback)
SV_LLM_BASE_URL optional endpoint override — any OpenAI-compatible gateway
SV_LLM_MODEL optional model selection
cp .env.example .env   # fill in your key; .env is gitignored

Batching is the contract

decide_batch receives all observations for the step (or for the cohort — Persona.group_key defines batching cohorts). A well-behaved LLM decision model:

  • renders each observation's 4-quadrant view into the prompt (Observation.rendered is 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");
  • issues batched calls, never one request per agent per step;
  • returns typed Actions that apply() can fold back into E.

Deterministic dry runs

You should be able to prove the plumbing — loop, artifacts, store, metrics — without spending a token. Two equivalent patterns:

A rule-based decision model (the shipped from-scratch template works this way): the decision is a pure function of the observation, so runs are exactly reproducible from the seed.

A deterministic stand-in client: keep your LLM decision model, but inject a client whose complete() is a fixed function of its input. A minimal stub:

class DeterministicLLMClient:
    """Stands in for the real client: same input → same output, no network."""

    def complete(self, prompt: str, **kwargs) -> str:
        # pick a stable, boring policy — e.g. always the first listed option
        return "stay"

Inject it wherever your decision model accepts its client (decision_args in simulation/simulation.json is the conventional place for such wiring). Bundled studies that wrap legacy engines ship their own deterministic client for exactly this purpose — check the study's README.

Dry runs are how the repository's parity tests work: wrapped legacy studies assert they reproduce the original trajectory on the same seed, with the deterministic client 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 warm-started iterations replay stored actions exactly rather than re-asking the LLM.

Cost control

  • /sv-run shows 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.
  • Warm starts inherit a parent version's steps 0..K by replay (no LLM), spending only on the new steps.