Skip to content

Random decisions, LLM selection, and deterministic replay

A legislature must decide whether a proposed law passes; the ticket context records 72 supporting and 28 opposing seats. The example answers that pass-or-fail question two ways. In the first run, a Random controller draws the outcome from authored weights: 25 for fail, 75 for pass. In the second, an LLM controller receives the question as a structured request and answers with one option ID.

The question the example answers: how can a run leave a choice to chance or to a language model and still replay exactly?

The 25/75 weights are illustrative values, not historical estimates.

Terminal window
cargo run -p canwu-api --example uncertainty_resolution

Output (two long JSON lines):

random_trace={"id":1,"ticket_id":1,"ticket_version":1,"controller_id":"law-random-controller","policy":{"kind":"random","id":"weighted-random","version":"1"},"decided_at":1440,"outcome":{"type":"selected","option_id":"pass"},"summary":"random policy selected pass","random":{"draw_id":1,"value":29,"upper_exclusive":100,"option_weights":[{"option_id":"fail","weight":25},{"option_id":"pass","weight":75}]}}
llm_request={"ticket_id":2,"ticket_version":1,"definition":"example.law-passage","summary":"Will the proposed law pass?","context":{"schema":"example.law-passage.v1","payload":{"opposing_seats":28,"public_pressure":"high","supporting_seats":72}},"options":[{"id":"fail","label":"Reject the law","description":"","metadata":null},{"id":"pass","label":"Pass the law","description":"","metadata":null}]}

In random_trace, the draw returned 29 out of 0..100. Weights are laid out in option-ID order, so fail covers 0–24 and pass covers 25–99, and 29 selects pass. decided_at is 1440 minutes, one day after the start. The random field names the draw record (draw_id: 1) that produced the value.

llm_request is everything the model sees: the ticket, its version, the context, and the option IDs and labels.

All code is in uncertainty_resolution.rs.

UncertaintyPlugin registers one boundary system that may read decisions and draw from one stream:

let mut contract = BoundarySystemContract::new(
"resolve-random-decision",
BoundaryPhase::StrategicAggregation,
SystemCadence::Daily,
);
contract.reads = vec![StateKey::core_decisions()];
contract.random_streams = vec![decision_stream()];
registrar.register_boundary_system(contract, random_resolution_system)

decision_stream() returns RandomStreamKey::new("example-uncertainty", "decision-selection", 1). A system can draw only from streams its contract declares.

2. Open the ticket with a Random controller

Section titled “2. Open the ticket with a Random controller”

random_branch calls enqueue_ticket, which registers the controller law-random-controller with DecisionPolicyIdentity::new(DecisionPolicyKind::Random, "weighted-random", "1"), opens ticket 1 with the options pass and fail, and settles one boundary.

3. Draw and resolve inside the boundary system

Section titled “3. Draw and resolve inside the boundary system”

random_resolution_system reads the ticket and draws a value keyed to this operation:

let sample = view.random_sample_for_operation(
&decision_stream(),
EvidenceRef::Ingress(calendar_ingress),
"decision_selection",
"law-proposal-42",
RandomOperationTarget::DecisionTicket {
ticket_id: ticket.id,
ticket_version: ticket.version,
},
0,
100,
"select whether the law passes from configured weights",
)?;

The arguments are the stream, the cause (the calendar ingress that triggered the boundary), an operation kind and ID, the target ticket and version, a draw slot (0), the exclusive upper bound (100), and a purpose. Because the draw is addressed by these values, a retry or a replay gets the same sample.

The system returns the sample and the weights in a BoundaryDirective::ResolveDecisionRandomly with a RandomDecisionResolution. The simulation core checks the stream, the target, that the weights sum to the bound and cover every available option, and that the controller is current. It then generates ordinary decision ingress for the next boundary.

4. Settle the source boundary and the resolution boundary

Section titled “4. Settle the source boundary and the resolution boundary”
let selection_at = SimTime::EPOCH + SimDuration::days(1);
canwu.schedule_calendar_boundary(selection_at, vec![SystemCadence::Daily])?;
canwu.step_canonical()?.expect("random source boundary");
canwu
.step_canonical()?
.expect("generated decision resolution boundary");

The first step runs the Daily system, which draws and generates the resolution. The second admits the resolution and writes the trace. If the source boundary fails, the draw and the generated ingress roll back together. One boundary may generate at most one random resolution, so independent random decisions go in separate source boundaries.

5. Ask an LLM through the same ticket contract

Section titled “5. Ask an LLM through the same ticket contract”

llm_interface_branch opens ticket 2 for a controller whose policy kind is DecisionPolicyKind::Llm, then builds the policy:

let mut policy = QueuedLlmPolicy::new(
"strict-law-selector",
"1",
LlmModelIdentity {
provider: "not-connected".to_owned(),
model: "host-selected-model".to_owned(),
prompt_contract: "return one existing option_id and no new action".to_owned(),
},
);
let ticket = canwu.decision_ticket(ticket_id).expect("LLM ticket");
let request = policy.external_request(ticket);

A real host sends request to its model and parses a structured answer. The example skips the network call and submits the answer directly:

policy.submit(
ticket_id,
ExternalDecisionResponse {
ticket_version: request.ticket_version,
option_id: "pass".to_owned(),
provider: "not-connected".to_owned(),
request_id: "example-response-1".to_owned(),
metadata: BTreeMap::new(),
},
)?;

drive_decision then checks the answer against the ticket and queues it as decision ingress, and step_canonical() records it. The trace stores the provider, model, prompt contract, and request ID, and exact replay reads them back without calling the model.

  • Chance enters only through a declared boundary system. The draw is recorded as draw evidence and cross-referenced by the trace’s random field. Decision ingress written by the host with a draw attached is rejected, both live and on load.
  • The model sees only the request shown above and answers with one option ID for that ticket version.
  • Both runs use the same ticket definition and options. Only the controller differs. Register the controller before opening the ticket. A controller ID can be registered once, so its policy stays fixed for the run.
  • Weights. Use fail 30 and pass 70. The draw does not depend on the weights, so it still returns 29, which now falls in fail’s range.
  • Seed. Change Canwu::demo(202) in random_branch to another seed and watch the drawn value and the outcome change. Each seed still replays to the same result.
  • An invalid LLM answer. Submit option_id: "maybe". drive_decision returns an error (policy selected unknown option maybe) and nothing is queued. A provider that differs from the model identity is also rejected.

A deterministic utility policy can hand only a genuine tie to chance. GuardedUtilityPolicy runs ordered guard rules, scores the remaining options, and, when random_tie_break is set and two or more options score within its near-equivalence margin of the best, returns DecisionOutcome::PendingRandom naming only those candidates, each with weight 1. A boundary system draws for the current ticket version as in step 3 and passes the pending decision as the tie_break of RandomDecisionResolution; the weights must equal the candidates. The controller opts in with with_random_tie_break, and the trace records the Random stage. Without the flag, the best score wins and equal scores go to the lowest option ID.

A random resolution fails its boundary before drawing if the ticket’s decision maker, or its controller’s authority person, was already unavailable when the boundary began. Systems that resolve tickets should skip those tickets.

Choose a mechanism for each kind of uncertainty

Section titled “Choose a mechanism for each kind of uncertainty”
Situation Mechanism
A fact is missing, disputed, or unknown to the actor Keep it as Unknown, Indeterminate, or contested evidence
A person or institution picks among allowed options A DecisionTicket with a Utility, Rule, Random, Human, External, or LLM policy
A discrete world incident has a real probability An operation-keyed draw in the boundary system that owns the mechanic
A large population changes at an expected rate Deterministic integer transfers with a persisted remainder

Applied to common domains:

  • Law. Keep quorum, authority, vote counting, and thresholds deterministic. Weight pass and fail only when the model deliberately treats unresolved bargaining as chance. canwu-law keeps a compatible Random, External, or LLM controller that the host registered in advance under the seat’s stable controller ID.
  • People. Put personality, knowledge, doctrine, obligations, and current options in the ticket context. Utility and Rule policies express deterministic preference, Random expresses bounded variation, and Human, External, and LLM policies choose from the same options.
  • Belief. Use integer rates and remainders for population diffusion. A single schism, revival, or suppression is a boundary draw if it is an incident, or a ticket if a leader decides it.
  • Crowds. Settle routine movement and diffusion deterministically. A panic or riot can be a discrete incident; an organizer’s response is a ticket.

The repository’s docs/uncertainty-resolution.md has the full design and a checklist.

Open the runnable example

Read the guarded utility policy tests

Continue with Randomness, the warlord aid decision, or Save, replay, and fork.