Running a simulation

TriageRunner drives one triage episode from start to finish and returns a complete, replayable artifact.

RunnerConfig

from triagesim import RunnerConfig

config = RunnerConfig(
    max_turns=20,
    enable_llm=False,
    store_backend="memory",
    redis_db=0,
    seed=42,
)
FieldTypeDefaultMeaning
max_turnsint6Maximum patient turns before the run is forced to end.
enable_llmboolFalseAllows the belief updater to call a model in addition to its deterministic rules.
store_backendstr"memory""memory" or "redis". Anything else raises ValueError.
redis_dbint0Redis database index, used only when store_backend="redis".
seedint \| NoneNoneSeeds Python's global RNG for the run.

max_turns defaults to 6

Six patient turns is short for a realistic triage encounter — enough to smoke-test a setup, rarely enough to reach a confident decision. Most real experiments want 15–25.

TriageRunner

from triagesim import TriageRunner

runner = TriageRunner(
    nurse_agent=nurse,
    patient_agent=patient,
    ground_truth=ground_truth,
    config=config,
)

artifact = runner.run()

All four arguments are required.

ParameterTypeMeaning
nurse_agentNurseAgentThe clinician.
patient_agentPatientAgentThe patient.
ground_truthdictThe hidden vignette.
configRunnerConfigRun settings.

Constructing the runner creates the state store, instantiates a TriageEnv, applies the enable_llm flag to the belief updater, and seeds the RNG. Nothing runs until you call run().

The ground-truth dict

ground_truth = {
    "chiefcomplaint": "Syncope",   # read by the patient agent
    "pain": 7,                      # read by the patient agent
    "vitals": {                     # released by the environment on request
        "temperature": 99.1,
        "heartrate": 112,
        "resprate": 26,
        "o2sat": 91,
        "sbp": 98,
    },
    "acuity": 2,                    # reference label, never revealed
}

chiefcomplaint and pain are read directly by the runner on every patient turn, so both keys must be present or the run raises KeyError. The five vitals keys are the only ones the nurse can check, and acuity is used only by the metrics.

The run loop

run() alternates nurse and patient phases:

  1. env.reset(ground_truth) mints a run_id and clears prior state.
  2. Nurse phase. The nurse acts repeatedly until it yields:
    • check_vital → the environment releases the value, and the nurse acts again
    • log_red_flag → flags recorded, and the nurse acts again
    • utterance → control passes to the patient
    • end → the run terminates
  3. Patient phase. Exactly one patient utterance. The turn counter advances, and the nurse infers belief updates from what was said.
  4. Repeat until the turn counter reaches max_turns, the nurse ends, or the safety cap trips.
  5. _finalize_run snapshots everything and returns it.

The safety cap

Beyond max_turns, the loop is bounded by max_turns * 4 total nurse-plus-patient steps. This guarantees termination if an agent gets stuck — for example a nurse that requests vitals indefinitely without ever speaking. With the default max_turns=6 that is 24 steps.

If your runs consistently stop early with few patient turns, the cap is the likely cause: a nurse that checks several vitals per turn burns steps quickly.

Cost

Each patient turn costs at least three model calls: one or more nurse act calls, one patient act call, and one nurse infer_belief_updates call. A nurse that checks three vitals before speaking adds three more. Budget roughly max_turns × 4 calls as a working estimate, and more if enable_llm=True.

Tip

Start at max_turns=6 while wiring things up, then raise it once the dialogue looks sensible.

Redis state store

By default, state lives in a plain dictionary and disappears when the process exits. Switching to Redis persists it:

config = RunnerConfig(
    max_turns=20,
    store_backend="redis",
    redis_db=0,
    seed=42,
)

This requires the extra and a running server:

pip install "triagesim[redis]"
redis-server

RedisStateStore connects to localhost:6379 at the configured db index. RunnerConfig exposes only redis_db, so a non-default host or port means constructing the store yourself:

from triagesim.core.state_store import RedisStateStore

store = RedisStateStore(host="10.0.0.5", port=6380, db=2)

State is written under keys namespaced by run:

triage:{run_id}:state
triage:{run_id}:belief_graph
triage:{run_id}:history
triage:{run_id}:trace
triage:{run_id}:red_flags

Keeping the run_id lets you inspect a completed run later without re-running it — useful when a batch of simulations takes hours and you want to analyse results incrementally.

Note

Importing RedisStateStore without the redis package installed raises ImportError when it is constructed, not at import time.

Reproducibility

seed calls random.seed() for the run, which fixes any internal random choices. Combined with seeded persona sampling, that makes the setup deterministic.

It does not make the dialogue deterministic. Sampling happens on the provider's side and is outside TriageSim's control, so two runs with identical seeds will differ in wording and may differ in outcome. For statistical claims, run each condition many times rather than relying on a single seeded run.

A complete example

from triagesim import TriageRunner, RunnerConfig
from triagesim.agents import OpenRouterLLM, NurseAgent, PatientAgent
from triagesim.core import NurseOutput, PatientOutput
from triagesim.personas import (
    load_patient_personas,
    load_nurse_personas,
    sample_patient_personas,
    sample_nurse_personas,
)
from triagesim.utils import compute_all_metrics

MODEL = "anthropic/claude-sonnet-4-5"

patients = load_patient_personas("patient.yaml")
nurses = load_nurse_personas("nurse.yaml")

ground_truth = {
    "chiefcomplaint": "Syncope",
    "pain": 7,
    "vitals": {"temperature": 99.1, "heartrate": 112, "resprate": 26,
               "o2sat": 91, "sbp": 98},
    "acuity": 2,
}

results = []

for seed in range(5):
    nurse = NurseAgent(
        llm=OpenRouterLLM(model_name=MODEL, output_type=NurseOutput),
        persona=sample_nurse_personas(nurses, k=1, seed=seed)[0],
        algorithm="esi",
    )
    patient = PatientAgent(
        llm=OpenRouterLLM(model_name=MODEL, output_type=PatientOutput),
        persona=sample_patient_personas(patients, k=1, seed=seed)[0],
    )

    artifact = TriageRunner(
        nurse_agent=nurse,
        patient_agent=patient,
        ground_truth=ground_truth,
        config=RunnerConfig(max_turns=20, seed=seed),
    ).run()

    results.append(
        compute_all_metrics(
            trace=artifact["trace"],
            belief=artifact["belief"],
            ground_truth=ground_truth,
        )
    )

correct = sum(bool(r["triage"]["correct"]) for r in results)
print(f"{correct}/{len(results)} runs triaged correctly")

Next: Run artifacts describes what run() gives you back.