Don't ask the model to decide. Ask it questions. Decide in code.
A decision circuit is a set of typed questions a model answers with probabilities, and a set of gates that turn those probabilities into decisions: thresholds, AND/OR/NOT, votes, verification. The model never sees the gates. Every decision carries its probability, its outcome, and a trace. When the number sits too close to a threshold to call, the gate says so and a human gets the case.
$pip install decision-circuits# no dependencies
The family
One recipe, one head, every modality the base has an encoder for
Each circuit model is a LoRA on the language model plus the same pointer readout head, trained on code-labeled and CC-licensed data with soft targets so calibration is learned. What changes between them is the base and what the state can carry. All of them serve the same POST /v1/systemone contract.
Reads text, JSON, and chat threads. A yes/no, a pick from any number of options, or a score, in one pass and 175 ms on a laptop. Runs anywhere, no GPU needed.
The 8-ball. The strongest text model in the family: 151-way intents, long threads, real production questions. Probabilities steady enough to put a threshold on.
Reads receipts, charts, tables, forms, and photos, and video as frames. Ask what a document says, whether a claim matches it, how many rows are flagged. No cap on options.
Listens to calls and recordings. What kind of call is this, what amount was said, how many items were listed, is it speech or a sound. Hears the clip; no transcript needed.
Same head everywhere
A query from the decide token, a key from each option's closing delimiter, softmax. Order-invariant, no cap on options, and it doesn't know or care what encoder produced the hidden state.
Same data idea everywhere
A grid of judgment operations times state formats, labels computed at generation time. For images that's rendered receipts and charts; for audio it's synthesized calls with known content.
Same test everywhere
Hold out a whole operation or a whole format and measure transfer. So far: layouts transfer, new kinds of judgment have to be in the data, and nobody is calibrated on undecidable inputs yet.
API
Call the models without running anything
A hosted endpoint serves circuit-1.7b and circuit-8b at api.decisioncircuits.com, same request format as TypeSafe's POST /v1/systemone. Keys are free: 1,000 questions a day per key, no card, no account. The models scale to zero between calls, so the first call after a quiet spell takes about a minute while a GPU spins up; warm calls answer in under half a second.
curl https://api.decisioncircuits.com/v1/systemone \
-H "Authorization: Bearer dc-..." -H "Content-Type: application/json" \
-d '{"model": "circuit-8b", "state": "Card charged twice, my card ends in 4412",
"questions": {"pii": {"type": "noul", "instructions": "Does the message contain personal information?"}}}'
Or from Python: SystemOne("https://api.decisioncircuits.com/v1/systemone", api_key=KEY) as the backend of any circuit. Pass "model": "circuit-1.7b" for the faster, smaller one.
A support-triage circuit, live
drag the probabilities a model might return; gates re-evaluate
What the model answered
billingtechnicalother
What code decided
redact(pii & ~business) ≥ 0.6, band 0.1, on_uncertain: escalate
routeargmax(dept), min_confidence 0.35
human(angry | pii) ≥ 0.6
Nudge pii to 0.65 and watch redact stop answering. That's the point: inside the band, the circuit escalates instead of guessing.
Write one
The circuit above, in Python
# questions the model answers
c = Circuit()
c.noul("pii", "Does the message contain personal information about a private individual?")
c.noul("business", "Are all identifying details about a business rather than a person?")
c.noul("angry", "Is the customer angry?")
c.choice("dept", "Which team should handle this?", {"billing": "Money, refunds", "technical": "Bugs, outages", "other": None})
# gates code evaluates
c.gate("redact", (Q("pii") & ~Q("business")) >= 0.6, band=0.1, on_uncertain="escalate")
c.gate("route", argmax("dept", min_confidence=0.35))
c.gate("human", (Q("angry") | Q("pii")) >= 0.6)
out = c.run(SystemOne(api_key=KEY), "Card charged twice, refund NOW. My card ends in 4412.")
out["gates"]["redact"]
# {'value': True, 'p': 0.97, 'outcome': 'decided', 'trace': ['pii p=0.97', 'gate _redact_1 p=0.98', 'and under independence -> p=0.95']}
The arithmetic is deliberately simple and is written into the trace: AND is a product, OR is 1 − ∏(1 − p), NOT is 1 − p. AND and OR assume the inputs are independent, and every trace says so, because a reviewer should see that assumption next to the number.
form
meaning
Q("pii"), Q("dept")["billing"], Q("urgency")[3]
a question's probability, or one option's
~a, a & b, a | b, e >= tau
NOT, AND, OR, threshold with an uncertainty band
argmax("dept", min_confidence=…)
top option; abstain below the confidence floor
majority("q1", "q2", "q3")
vote across paraphrased questions
verify("dept", check=Q("supported"), tau=…)
a negative checker: escalate when the check doesn't support the pick
order("severity", [c1, c2, …])
bucket an ordered score
G("route")["billing"]
gates over gates
c.to_mermaid() renders any circuit as a schematic: inputs, logic, decisions, colored by outcome after a run.
Numbers
The 2025 article, re-run on a model built for this
The original write-up classified 100 water-utility customer calls into eleven types with two LLM parsers and a negative checker, combined into confidence tiers. Same calls, same circuit, one call to a System One model:
2025, Claude Sonnet 3.7, three LLM calls
2026, Jev, one call
single question, no circuit
91%
98%
circuit, overall
87%
98%
high-confidence calls
80 calls, 92.5% right
93 calls, 97.8% right
latency
three round trips
325 ms for all three questions
And a test that didn't exist before: nine kinds of judgment (extract, compare, count, apply a rule, check a claim against a record, …) times six ways of laying out the data, every label computed by code. Jev scores 95% on it. It also gets confident on inputs built to be undecidable, which is exactly what a threshold with a band is there to catch.
Scoreboard
Every System One model we can get our hands on, same items, same labels
Jev is TypeSafe's API. Bespoke-Nimble-9B and kev-0.5b are open reproductions released September 18. circuit-1.7b is ours. Cells are accuracy / expected calibration error; lower ECE is better calibrated. Latency is per question, measured the same way for every row in a block.
cold eval, 1,200 human-labeled items
MultiNLI
SMS spam
toxicity
CLINC, 151 intents
latency
Jev
88% / 0.04
96% / 0.05
82% / 0.06
90% / 0.05
164 ms, API
Bespoke-Nimble-9B
84% / 0.09
91% / 0.06
86% / 0.08
unsupported (26-option cap)
1.8 s, laptop
kev-0.5b
46% / 0.28
50% / 0.30
62% / 0.16
62% / 0.17
325 ms, laptop
circuit-1.7b
81% / 0.09
98% / 0.02
90% / 0.16
86% / 0.06
175 ms, laptop
circuit-8b
86% / 0.08
98% / 0.02
93% / 0.14
95% / 0.03
178 ms, A6000
Those four tasks' public train splits are in circuit-1.7b's training data and presumably not in Jev's. The two blocks below were trained on by nobody.
out of distribution
Jev
Nimble-9B
kev-0.5b
circuit-1.7b
circuit-8b
the article's 100 water-utility calls, 11 types
98% / 0.02
93% / 0.05
80% / 0.13
92% / 0.08
93% / 0.05
546 production questions from a real site (agreement with Jev)
* The grid generator is ours, so that row is held-out items, not held-out structure. Trained with one operation and one format withheld, the same recipe scores 91% on the unseen format and 57% on the unseen operation. Layouts transfer; new kinds of judgment have to be in the data.
The finding nobody else measures. About five percent of grid items are built to be undecidable: a vague claim, two equal bars under "which is taller", a date with no year. The right answer is a flat distribution. Every model above, Jev included, answers those with mean confidence between 0.5 and 0.97. That is exactly the failure an uncertainty band around a threshold exists to catch, and why circuits escalate instead of trusting a number near the line.
circuit-1.7b: LoRA plus a pointer readout head on Qwen3-1.7B-Base, trained in 61 minutes on one RTX 4090 on code-labeled and CC-licensed data only. Weights and card: huggingface.co/jbarney/circuit-1.7b. circuit-8b: the same recipe on Qwen3-8B-Base, 75 minutes on one RTX A6000; it beats Jev on three of the four cold-eval tasks and matches Nimble-9B on the production questions. Weights: huggingface.co/jbarney/circuit-8b. Training and evaluation code: github.com/Barneyjm/circuit.
Agents
The circuit sits where the agent shouldn't be trusted
LangChain
CircuitToolGuard judges each tool call: allow, block, or pause with the standard human-in-the-loop interrupt. CircuitRouter picks a model per run.
OpenAI Agents SDK
Input, output, and tool guardrails from a circuit. Tripwires carry the gate's probability and trace.
Claude Agent SDK
A PreToolUse hook whose allow / deny / ask maps straight onto the gate's decided / blocked / uncertain.
Compared with a single yes/no classifier at a fixed 0.5, a circuit combines several questions, makes the threshold and its band explicit, and sends the uncertain cases to a person instead of silently allowing or blocking them.
Models
Built for System One models
Circuits need calibrated probabilities. That's what a System One model produces: typed questions in, a distribution per question out, in one pass, in about 200 ms. The SystemOne backend talks to any server speaking that contract.
Jev, by TypeSafe
The model that made this practical. Drop in an API key.
Our own S1 models: pointer readout, code-labeled and CC-licensed data only. The small one runs at 175 ms per question on a laptop; the large one leads the scoreboard.
Yours
A backend is one method: answer(state, questions). Wrap a cache, inject code-owned facts, chain fallbacks.
No S1 model yet? Chat models can stand in through logprobs (OpenAI-compatible) or tool use (Claude), with the caveat that a chat model's stated confidence isn't calibrated the way an S1 model's output is.