Core concepts¶
A TriageSim run is a small, auditable simulation loop. This page explains the pieces and how control flows between them, which makes the rest of the documentation much easier to read.
The hidden ground truth¶
Every run starts from a structured clinical vignette:
ground_truth = {
"chiefcomplaint": "Syncope",
"vitals": {"temperature": 99.1, "heartrate": 112, "resprate": 26,
"o2sat": 91, "sbp": 98},
"acuity": 2,
"pain": 7,
}
Neither agent receives this dictionary. The patient agent is given only
chiefcomplaint and pain, and must express them in character. The nurse
agent is given nothing at all — it starts from an empty transcript.
Vitals are released by the environment, not spoken by the patient: when the
nurse takes a check_vital action, the environment reads the true value out of
ground_truth["vitals"] and appends it to the transcript as a system event.
acuity is never released; it exists purely so the run can be scored
afterwards.
This separation is what makes the data useful. The dialogue is generated, but the label it should have produced is known exactly.
The environment¶
TriageEnv (in triagesim.core.environment) is the single source of truth. It
owns the belief graph, applies nurse actions, releases vitals, tracks red flags,
and persists everything through a StateStore. Its three methods are
reset(ground_truth) -> run_id, observe(run_id), and
step(run_id, actor, action).
TriageRunner deliberately contains no clinical logic. It only decides
whose turn it is. Everything that could affect a triage outcome lives in the
environment, where it is schema-validated and recorded.
Structured actions, not free text¶
The nurse model does not act directly. It emits a NurseOutput — a Pydantic
model — which map_nurse_output_to_action converts into exactly one executable
action:
| Nurse action | Executable action | Effect |
|---|---|---|
utterance | NurseUtteranceAction | Appends the nurse's line to the transcript and hands control to the patient. |
check_vital | NurseCheckVitalAction | Releases one vital from the ground truth as a system event. |
log_red_flag | NurseLogRedFlagAction | Records clinical red flags. |
end | NurseEndAction | Terminates the run with a final triage level. |
Invalid combinations are rejected rather than guessed at: an utterance with
no text, or a log_red_flag with no flags, raises ActionMappingError. This
mapping layer is what prevents a model from taking an action the environment
never sanctioned.
Why an intermediate representation
NurseOutput is what the model is good at producing; the action classes
are what the environment can safely execute. Keeping them separate means a
malformed generation fails loudly at the boundary instead of corrupting the
run state.
Vitals¶
Only five vitals exist, defined as ALLOWED_VITALS:
temperature, heartrate, resprate, o2sat, sbp
Which one the nurse asked for is inferred from its utterance by keyword
matching — "pulse" and "heart rate" both resolve to heartrate, "bp" and
"blood pressure" to sbp, "oxygen" and "saturation" to o2sat, and so
on. If several are mentioned, the first match wins; if none match, the
vital resolves to None.
Setting ENABLE_LLM_DETECTORS = True in triagesim.config adds a model-based
fallback when the keyword rules find nothing. See
Configuration.
The belief graph¶
BeliefGraph is the environment's record of what the nurse has actually
learned. It is provenance-aware: each slot value carries the turn it was
acquired on, whether it came from the patient, the nurse or a system vital, and
whether it was explicit, inferred, or suspected.
Updates are merged deterministically even though extraction is probabilistic:
None values are dropped, exact (slot, value) duplicates are ignored, and
genuinely different values for the same slot are both kept so ambiguity
survives rather than being silently resolved.
Two things are tracked as special sets: vitals_known (which vitals have been
released) and red_flags_logged (flags the nurse explicitly logged).
to_observation() flattens the graph into the lossy, provenance-free view that
agents and metrics consume — this is what appears under the belief key of a
run artifact.
Turn structure¶
The loop is asymmetric, and this catches people out:
The nurse may act several times before the patient responds. Checking a
vital or logging a red flag is a micro-turn — it does not advance the turn
counter or hand over control. Only a nurse utterance does that.
turn 0 nurse: check_vital -> system releases heartrate = 112 (micro-turn)
turn 0 nurse: log_red_flag -> "tachycardia" recorded (micro-turn)
turn 0 nurse: utterance -> "Did you hit your head?" (yields)
turn 0 patient: utterance -> "No, my partner caught me." (turn -> 1)
The turn counter advances only on a patient action, so max_turns counts
patient responses, not model calls. After each patient utterance the nurse
infers belief updates from what was said, and the environment merges them.
A run ends when the turn counter reaches max_turns, or when the nurse takes
the end action. A hard safety cap of max_turns * 4 total steps guarantees
termination even if an agent misbehaves.
Red-flag logging is deduplicated: flags are normalised to lowercase, whitespace-collapsed strings, repeats within a turn and across earlier turns are discarded, and at most five new flags are accepted per turn.
Triage algorithms¶
The nurse agent reasons with one of two protocols, selected at construction:
The Emergency Severity Index, used widely in the United States. Level 1 is the most acute.
Both scales run from 1 to 5 and NurseOutput.triage is constrained to that
range, but the reasoning prompt differs substantially. Because lower means
more urgent on both, predicting a number below the true acuity is
over-triage — a convention that matters when reading
the metrics.
Persistence¶
All state lives behind the StateStore interface, 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
InMemoryStateStore keeps these in a dictionary; RedisStateStore keeps them
in Redis so runs outlive the Python process. Both implement the same five
operations, so switching is a one-line config change. See
Running a simulation.
Putting it together¶
flowchart TD
GT[Ground truth vignette] -->|hidden| ENV[TriageEnv]
ENV -->|transcript| NA[NurseAgent]
NA -->|NurseOutput| MAP[map_nurse_output_to_action]
MAP -->|executable action| ENV
ENV -->|vital released| ENV
ENV -->|transcript| PA[PatientAgent]
PA -->|PatientOutput| ENV
ENV --> BG[(BeliefGraph)]
ENV --> SS[(StateStore)]
SS --> ART[Run artifact]Next: Personas covers how the two agents are conditioned, and Run artifacts covers what comes out the other end.