decision-circuits · v0.3

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

A support-triage circuit, live

drag the probabilities a model might return; gates re-evaluate

Inputs · what the model answered

0.91
0.10
0.35
billing
billing0.72 technical0.20 other0.08

Gates · 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.

formmeaning
Q("pii"), Q("dept")["billing"], Q("urgency")[3]a question's probability, or one option's
~a, a & b, a | b, e >= tauNOT, 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
Circuit schematic: inputs on the left, logic gates in the middle, decisions on the right, colored by outcome.
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 calls2026 · Jev, one call
single question, no circuit91%98%
circuit, overall87%98%
high-confidence calls80 calls · 92.5% right93 calls · 97.8% right
latencythree round trips325 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.

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.

guard = CircuitToolGuard(c, jev, gate="block", tools=[delete_file, send_email])
agent = create_agent(model, tools=[read_file, delete_file, send_email], middleware=[guard])

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 · TypeSafe

The model that made this practical. Drop in an API key.

circuit-1.7b · open weights

Our own S1 model, trained on code-labeled data with a pointer readout. Runs on a laptop.

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.