Random decisions, LLM selection, and deterministic replay
The scenario
Section titled “The scenario”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.
What this example shows
Section titled “What this example shows”- A random decision policy
(
DecisionPolicyKind::Random) with decision option weights (DecisionOptionWeight). - An operation-keyed random draw
(
random_sample_for_operation) from a declared random stream (RandomStreamKey). - A boundary system that resolves the ticket
with
BoundaryDirective::ResolveDecisionRandomly. - An LLM controller driven through
QueuedLlmPolicy,ExternalDecisionRequest, andExternalDecisionResponse, with no network call. - The same decision ticket contract serving both runs.
Run it
Section titled “Run it”cargo run -p canwu-api --example uncertainty_resolutionOutput (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.
Walkthrough
Section titled “Walkthrough”All code is in
uncertainty_resolution.rs.
1. Declare the system that owns the draw
Section titled “1. Declare the system that owns the draw”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.
What to notice
Section titled “What to notice”- Chance enters only through a declared boundary system. The draw is recorded
as draw evidence and cross-referenced by the
trace’s
randomfield. 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.
Try changing
Section titled “Try changing”- Weights. Use
fail30 andpass70. The draw does not depend on the weights, so it still returns 29, which now falls infail’s range. - Seed. Change
Canwu::demo(202)inrandom_branchto 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_decisionreturns an error (policy selected unknown option maybe) and nothing is queued. Aproviderthat differs from the model identity is also rejected.
Beyond the example
Section titled “Beyond the example”Random tie-break for a utility policy
Section titled “Random tie-break for a utility policy”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
passandfailonly when the model deliberately treats unresolved bargaining as chance.canwu-lawkeeps 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.
Source
Section titled “Source”Open the runnable example
Read the guarded utility policy tests
Continue with Randomness, the warlord aid decision, or Save, replay, and fork.