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,
)
| Field | Type | Default | Meaning |
|---|---|---|---|
max_turns | int | 6 | Maximum patient turns before the run is forced to end. |
enable_llm | bool | False | Allows the belief updater to call a model in addition to its deterministic rules. |
store_backend | str | "memory" | "memory" or "redis". Anything else raises ValueError. |
redis_db | int | 0 | Redis database index, used only when store_backend="redis". |
seed | int \| None | None | Seeds 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.
| Parameter | Type | Meaning |
|---|---|---|
nurse_agent | NurseAgent | The clinician. |
patient_agent | PatientAgent | The patient. |
ground_truth | dict | The hidden vignette. |
config | RunnerConfig | Run 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:
env.reset(ground_truth)mints arun_idand clears prior state.- Nurse phase. The nurse acts repeatedly until it yields:
check_vital→ the environment releases the value, and the nurse acts againlog_red_flag→ flags recorded, and the nurse acts againutterance→ control passes to the patientend→ the run terminates
- Patient phase. Exactly one patient utterance. The turn counter advances, and the nurse infers belief updates from what was said.
- Repeat until the turn counter reaches
max_turns, the nurse ends, or the safety cap trips. _finalize_runsnapshots 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:
This requires the extra and a running 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.