Train decision models that answer with probability distributions instead of generated text. You give a state and a question with explicit options; the model returns a distribution over those options in one forward pass.
Exu is a toolkit, not a method. The training method it implements is RLCD, Reinforcement Learning for Calibrated Decisions, the name TypeSafe AI gives to the recipe behind Jev, its System One model. The reward is a strictly proper scoring rule: its expected score is uniquely maximized by the true distribution, and the direct baseline optimizes it as is. RLCD optimizes the same reward under logit perturbations, and propriety alone does not guarantee that its unperturbed predictions are calibrated, so compare the two on held-out data. Calibration is not coaxed out of the model with a prompt, it is what the objective asks for.
The name honours Exu, the Orixá of the crossroads, of movement and of communication, who in Afro-Brazilian religions opens the paths and governs the choice made at every crossing. A model that answers with a distribution over explicit options is a crossroads by construction.
This is the agnostic base. It is not tied to a language, a domain, or an encoder. Swap the encoder, write your questions, and train.
Pre-alpha. The API, the checkpoint format and the results can change.
A reflex decision (route a ticket, score urgency, flag fraud) may need probabilities, but it does not need text generation. Generating a token takes hundreds of milliseconds, needs a parser, and the "confidence" a chat model writes is just more text. Here the output is already a distribution, so there is nothing to parse and nothing to hallucinate.
Three primitives cover most decisions, and all three are the same mechanism:
| Primitive | Question | Output |
|---|---|---|
choice |
pick one of N explicit options | distribution over options |
score |
place the state on an ordinal rubric | distribution over levels |
noul |
boolean question | distribution over No and Yes |
The options are written at request time and read by the model as text, so a new task is a new question, not a new output layer. No retraining to add a label.
uv add exu-base # or: pip install exu-baseTo work on this repository itself, install from the source tree:
uv sync --extra devfrom exu import DecisionQuestion, Option
route = DecisionQuestion.choice(
"Where should this ticket go?",
[
Option("billing", "payment, invoice or refund"),
Option("support", "access or outage"),
],
)
risk = DecisionQuestion.score(
"How risky is this request?",
["no risk", "review", "block"],
)
fraud = DecisionQuestion.noul("Is this message attempting fraud?")exu-train \
--mode baseline \
--train data/train.jsonl --train-split train \
--validation data/train.jsonl --validation-split validation \
--test data/train.jsonl --test-split test \
--calibration data/train.jsonl --calibration-split calibration \
--output artifacts/my-model \
--encoder google-bert/bert-base-multilingual-cased \
--epochs 4 --batch-size 8 --option-shuffle --calibrateUse --mode baseline. It optimizes the strictly proper score directly, with no
sampling, and it is what we recommend for a model you mean to use. --mode rlcd
adds the perturbed-logit policy gradient and stays in the package as the method
under evaluation: it has to beat the baseline on held-out NLL, and so far it has
not. In every comparison measured on real data (fourteen, across three backbones
and three datasets, see BENCHMARKS.md) the direct baseline was as good or better,
and RLCD made confident misses more often. That includes the setting its authors
report gains in, soft targets from a teacher model, measured on one dataset. What
remains untested is a reward per sampled answer with no target distribution, such
as a judge.
The step-by-step version, from writing the records to reading the report and the failure modes, is docs/usage-guide.md.
exu-evaluate \
--checkpoint artifacts/my-model \
--data data/train.jsonl --split test \
--order-permutations 4 --latencyPrints NLL, Brier, accuracy, ECE, ordinal scores, per-kind and per-family breakdowns, selective coverage, the three trivial baselines, order robustness and latency.
from exu import DecisionRuntime
runtime = DecisionRuntime.load("artifacts/my-model")
decision = runtime.decide("I was charged twice for the same invoice.", route)
print(decision.label, decision.confidence) # billing 0.87
decisions = runtime.decide_all(ticket, [route, urgency, refund]) # one state, many questionsA state longer than the sequence is cut from the right, and every decision says how much of it the model read:
usage = decision.input_usage
if usage.state_truncated:
print(f"read {usage.state_tokens_used} of {usage.state_tokens_original} state tokens")
strict = DecisionRuntime.load("artifacts/my-model", truncation="error") # refuse insteadOr from a shell, one question with flags or a JSONL batch with --input, where a
record can carry one question or a list of questions about its state:
exu-decide --checkpoint artifacts/my-model \
--state "I was charged twice for the same invoice." \
--instruction "Where should this ticket go?" \
--option "billing=payment, invoice or refund" --option "support=access or outage"One JSONL record per decision. The target is a distribution, not a label, so soft
targets from several annotators or a teacher model are first-class. An optional
context (a policy, a rubric) is read before the state with a budget of its own;
section 15 of BENCHMARKS.md measures whether a model follows a policy given there.
{
"id": "t-1",
"state": {"text": "I was charged twice for the same invoice."},
"question": {
"kind": "choice",
"instruction": "Where should this ticket go?",
"options": [
{"name": "billing", "description": "payment, invoice or refund"},
{"name": "support", "description": "access or outage"}
]
},
"target": [1.0, 0.0],
"split": "train",
"family": "routing",
"language": "en"
}See docs/dataset-format.md for the full field list and the rules that keep held-out honest.
state + typed question -> one sequence per question -> bidirectional encoder -> question-type embedding and two extra transformer layers -> read the hidden state at each option's marker -> softmax over options. Training samples Gaussian
perturbations of the logits and rewards each candidate distribution with a
strictly proper scoring rule. Then a temperature map, fitted on held-out data,
brings confidence in line with accuracy.
That is the default, a bidirectional encoder. --backbone decoder is an
experimental second architecture for causal models such as Qwen3: one sequence
per option (state, question, candidate), read at its last token, with the same
reward, calibration and evaluation after the logits. The state runs once for all
the questions about it and each option adds a short tail, at inference and, on
Qwen3 and Qwen3.5, in training too. Measured with Qwen3-0.6B, it has the best NLL on
GoEmotions and on ChaosNLI, and it learned balanced NLI on every seed, where the
encoder needs --scorer marker-cls. It is not immune to the uniform-guess
plateau: on balanced BoolQ two seeds in three stayed at log 2. The numbers are
in BENCHMARKS.md. The checkpoint records which architecture it is, so
evaluation and inference take no flag for it.
The full walkthrough is in docs/exu-guide.md, and docs/algorithm.md has the reward and policy math.
make setup # install dependencies
make test # pytest
make lint # ruff
make check # lint + test + build, exactly what CI runs
make site # build the static explainer into _site/Run make for the full list.
Working: typed-decision contract, token-budgeted sequence builder with marker injection defense, direct and RLCD training, temperature calibration, calibration-aware evaluation, portable checkpoint, offline inference runtime.
Not done: a human-annotated calibration set, learned act-or-escalate training (the cost-based baseline is in place), and a multi-turn prefix objective.
Small synthetic fixtures are for plumbing only. They are not evidence of quality.
Every run on real data so far, with datasets, encoders, seeds and the results that
did not go RLCD's way, is in
BENCHMARKS.md.
Two models from those runs are published as examples, each with a card that says
what it is and is not.
exu-qwen3.5-2b-typed-decisions
is the decoder backbone on Qwen3.5-2B with rank-16 adapters, trained on the
synthetic agent traces of typed-decisions (section 14), and loads with
DecisionRuntime.load.
exu-qwen3-0.6b-goemotions
is a Qwen3-0.6B body with a decision head trained on GoEmotions; it comes with its
own one-file runtime, and this package does not load that format.
Two projects are the inspiration for this one, and neither contributed code.
TypeSafe's Jev came first and is the bigger influence:
the typed-decision contract of choice, score and noul, answers that carry a
distribution plus a separate confidence, the idea of gating action on that
confidence, and the RLCD name for the training method. The "System One" framing
is theirs too.
Laya (Apache-2.0, by Nandakishor) is the open build recipe this repository follows: one sequence per question with a mask marker per option, an explicit header and option token budget, the log + spherical + ranked-probability reward, and one temperature per question type and option-count bucket. The benchmark and latency numbers quoted in the docs come from Laya's measurements, not from runs made here.
This is an independent implementation. No source code was copied from either project.
NanoJev (MIT) published the layout of
the experimental decoder backbone first, on 2026-09-17: one sequence per option,
the state and the question before a Candidate: and a Decision: line, and the
last token's hidden state read through LayerNorm and Linear(hidden, 1). The
probe behind --backbone decoder was written four days later without knowing
NanoJev existed, and we only found it afterwards. It got there first, so the credit
for that layout is its. No code was copied from it either.
Two more open reconstructions of Jev, found at the same time: Kev (Apache-2.0, by Jared Palmer), which is wire-compatible with TypeSafe's API, and jev-forge (MIT).
One more reference, in a different setting. Nandakishor M, Confidence-Aware Routing for Large Language Model Reliability Enhancement (arXiv:2510.01237, 2025) is by the same author as Laya. It estimates confidence before generation and routes a query across four pathways: local generation, retrieval, a larger model, or human review. That is the routing idea the act-or-escalate gate applies to a single encoder here. It does not describe this design, and nothing in this repository implements it.
Code under MIT. Datasets and checkpoints carry their own licenses. Before publishing an artifact, confirm the encoder, tokenizer, data and resulting weights are compatible.