Confidence Gate

September 20, 2026 · View on GitHub

Confidence Gate

Can a model’s confidence tell you when to trust it?

A support-triage benchmark comparing TypeSafe Jev and OpenAI on the same LangGraph workflow.

Python 3.11+ LangGraph 24 labeled tickets

Quick start · Results · Architecture · HTML report · Project layout


At a glance

One graph, one policy, two judgment engines. This experiment tests TypeSafe’s published claims about Jev in a support-triage workflow, with a confidence gate that sends uncertain cases to a human.

Observed in this runJevOpenAI (gpt-4o-mini)
Time to routing decision163 ms1,164 ms
Department accuracy87.5%75.0%
Cost per 1,000 tickets$0.06$0.37
Routing errors caught by the gate0 of 30 of 6

Important

These results come from 24 invented tickets. Jev was faster and cheaper in this run, but the sample is too small to establish calibration. Its confidence gate caught none of its three routing errors. Read the caveats.

To view the HTML report locally, run uv run python -m http.server 8000 from the repository root and open localhost:8000/docs/.

Why this experiment

TypeSafe launched Jev as a "System One Model": a frontier-intelligence function call, unstructured state in and typed probabilistic decisions out, claiming two orders of magnitude more speed and efficiency than an LLM on decision-shaped work, and — the claim this repo cares about most — decisions that arrive with calibrated probabilities, where higher confidence really does mean higher accuracy.

That last claim is the interesting one, because it is the one that would change how you build. If a model reliably knows when it does not know, you can hand its uncertain cases to a human and automate the rest. If it does not, you cannot, however accurate it is on average.

So this is a multi-agent support-triage system on LangGraph subgraphs with the judgment layer behind a swappable interface, and a gate node whose only job is to act on a confidence number. The graph, the policy, the thresholds and the money rules are identical across engines; the only thing that changes between runs is where the probabilities came from. Then we measure which of TypeSafe's claims survive contact with it.

Short version: the speed and cost claims hold. The intelligence-parity claim holds. The calibration claim — the one that matters most — is not demonstrated here, and this workload does not have the statistical power to test it properly. Jump to the claims table.

Quick start

1. Install dependencies

Requires Python 3.11+ and uv.

uv sync --extra dev

2. Configure API keys

Copy .env.example to .env, then set both keys:

cp .env.example .env
TYPESAFE_API_KEY='apikey_...'
OPENAI_API_KEY='sk-...'

A bare TypeSafe key on its own line (no NAME=) is also accepted, since that is how the console hands it out.

Optional overrides: TYPESAFE_DEFAULT_MODEL (default jev-latest), OPENAI_MODEL (default gpt-4o-mini).

3. Run the benchmark

# compare both engines over the labeled set
uv run agentbench bench --out results/run.json

# one ticket, with the full decision trace
uv run agentbench run T-013 --engine jev

# no keys needed: a keyword-heuristic engine that exercises the graph
uv run agentbench run T-013 --engine fake

# print the topology, subgraphs expanded
uv run agentbench graph

uv run pytest

The graph

flowchart TD
    start([START]) --> triage["Triage · five parallel judgments"]
    triage --> gate{"Confidence gate"}
    gate --> billing["Billing · assess → decide"]
    gate --> technical["Technical · assess → decide"]
    gate --> escalation["Escalation · assess → decide"]
    billing --> finalize["Finalize · SLA and usage"]
    technical --> finalize
    escalation --> finalize
    finalize --> finish([END])

    style triage fill:#e0f2ee,stroke:#0e8c7e,color:#14201e
    style gate fill:#fff0dc,stroke:#bc5e22,color:#14201e
    style finalize fill:#e0f2ee,stroke:#0e8c7e,color:#14201e

The confidence gate is ordinary code. Each department is a compiled subgraph with two steps, assess → decide: assess asks the engine for judgments, decide turns them into an action with plain code.

Where the model stops and code starts

This is the design the project is actually testing. The model is never asked what to do — only to describe the situation:

Judgment (model)Decision (code)
department + its probabilitieswhich subgraph runs, and whether confidence is high enough to trust it at all
priority as a 0–3 expected scorethe SLA clock
needs_human as a probabilitythe escalation threshold
action: full_refund + policy_exceptionthe refund amount, and whether a supervisor must approve it
severity, data_loss_riskwhether to page on-call
tier, legal_exposurethe legal override, which no weighted score can average away

Two consequences worth noticing. Changing the refund approval limit does not re-run inference — the judgments are unchanged, only the policy reading them. And an uncertain department choice is not resolved by guessing; it routes to a human. That is the gate node, and it only works if the confidence number means something, which is what the benchmark measures.

Speculative questions

refund_eligible is asked during triage even though it only matters on the billing branch. It rides along in the same parallel request, which costs one extra question instead of a second round trip. Code consumes it only where it applies and ignores it everywhere else.

What the benchmark reports

Accuracy is the headline and the least interesting column:

  • department accuracy — did it pick the right team
  • priority MAE — error on the 0–3 urgency score
  • needs_human accuracy — at the policy threshold, not the model's opinion
  • calibration error — bin by stated confidence, compare against how often that bin was actually right; 0 means the numbers mean what they say
  • confidence separation — mean confidence when right minus when wrong. Near zero means confidence carries no signal and the gate cannot work, whatever the accuracy column says
  • abstention rate / errors caught by gate — how many of its own mistakes the system handed to a human instead of acting on

Timing is captured at three points, because "latency" alone hides where the time goes:

  • time to decision — wall clock from the ticket entering the graph to the gate node choosing a branch. This is the number an SLA starts on: the point where the ticket has somewhere to go.
  • end-to-end (mean, p50, p95) — wall clock around the whole invocation, including the department subgraph's own engine call.
  • of which engine / of which graph — end-to-end split into time spent waiting on the judgment layer versus time spent in LangGraph itself. The second number should stay small; if it does not, the comparison is measuring the harness rather than the engines.

Cost is reported as input/output tokens, the dollar cost of the run, and dollars per 1,000 tickets. Prices are hand-entered list prices in pricing.py and go stale — check them before quoting a figure.

Then: how often the engines agree, and a table of every ticket where they differ with who was right.

A routing layer that is wrong 10% of the time and knows which 10% is worth more than one that is wrong 5% of the time with uniform swagger. The first can route its uncertain cases to a person; the second cannot.

What this experiment tests

Every row below is a claim TypeSafe published. The verdict column is what this repo's 24-ticket workload can actually say about it — which for two of the rows is "less than you'd like."

TypeSafe's claimTested here?Verdict
End-to-end response 70–500 msDirectlyHolds. 163 ms to a routing decision, 316 ms mean end to end, 430 ms p95 — inside the claimed band, over the public internet from a laptop.
40–200× faster than LLMsPartiallyNot at that magnitude — but against the hardest possible comparator. 7.1× vs gpt-4o-mini. Their figure is against frontier reasoning models; a small non-reasoning model is the least flattering baseline you could pick.
$0.042/MTok in, output freeDirectlyHolds as priced. 6.1× cheaper per ticket. See the wrinkle below.
Type-safe, cannot hallucinateWeaklyNo counter-example in 48 calls. But OpenAI's strict JSON mode also produced zero type errors, so this workload cannot separate guaranteed from reliable in practice.
Similar intelligence on System One tasksDirectlyHolds. 0.875 vs 0.750 on departments — same ballpark, Jev ahead, margin of 3 tickets.
Consistent: similar answers for similar inputsIncidentallySupported. Across two full runs Jev picked the same department on all 24 tickets, with confidences moving only in the second decimal.
Calibrated: higher confidence → higher accuracyThis was the pointNot demonstrated. Confidence separation −0.010; the gate caught 0 of 3 errors. With 3 errors to work from, this is untested at adequate power, not refuted.

The run

24 tickets, jev-latest vs gpt-4o-mini, two requests per ticket each, same questions, same graph, same policy.

jevopenai
department accuracy ↑0.8750.750
priority MAE ↓0.5490.646
needs_human accuracy ↑0.5830.750
calibration error ↓0.1990.150
confidence separation ↑−0.010−0.000
abstention rate0.0830.000
errors caught by gate ↑0.0000.000
time to decision ↓163 ms1164 ms
end-to-end p95 ↓430 ms2996 ms
of which graph3.7 ms3.6 ms
input tokens ↓34,22640,750
output tokens4,5734,487
cost / 1k tickets ↓$0.06$0.37

The ~4 ms of graph overhead on both sides is what makes the rest of the table worth reading: the harness is not what is being measured.

The speed claim holds, for the reason they say

163 ms to a decision, 7.1× faster than a small non-reasoning LLM asked exactly the same questions with exactly the same rubrics. Both engines make two requests per ticket. The difference is what comes back: a System One model emits its answer in one parallel pass, where prompt-and-parse has to write a JSON object out token by token.

The cost claim holds, but not how you'd guess

6.1× cheaper per ticket — and worth understanding, because the naive reading is wrong. Jev's output token count was not smaller: 4,573 against 4,487, essentially identical. The saving comes from the price sheet, not the volume — output billed at zero, and input at $0.042/MTok against $0.15. If you were expecting parallel sampling to show up as fewer output tokens, it doesn't. It shows up on the invoice.

The calibration claim is the one that didn't land

This is the result most worth not overselling, and the reason the repo is named after the gate rather than the model.

Put two rows beside each other. Jev got 3 tickets wrong. It abstained on 2. They were different tickets — errors caught by gate: 0.000. It gated answers it had actually gotten right, and its real errors went through with high confidence. Confidence separation of −0.010 says it plainly: on this set the number was very slightly anti-correlated with being right.

Two honest qualifications, in both directions:

  • This does not refute the claim. Three errors is not a sample. A calibration measurement driven by 3 data points has essentially no power, and both ECE and confidence separation need hundreds of cases to be stable. The correct reading is "this workload could not test it," not "the claim is false."
  • It also isn't nothing. The gate is the thing you would actually build on a calibration guarantee, and on this workload it did not work. That is worth knowing before designing around it.

Do not credit the calibration error column to either engine, either. It nominally favours gpt-4o-mini (0.150 vs 0.199), but a predictor that always answers 0.9 scores well on ECE whenever 0.9 sits near its overall accuracy, which is roughly what happened. Confidence separation catches that, and at −0.000 it says the number carried no information about correctness at all.

What is true, narrowly: Jev's confidence varies — 0.32, 0.42, 0.55 on genuinely ambiguous tickets — where gpt-4o-mini reported 0.9 on nearly everything. A varying signal is a precondition for a gate to do anything; a pinned one makes it dead code at any threshold. Varying is not the same as useful.

Where they disagreed

On all three tickets where the engines diverged, Jev was right, and each time reported low confidence while the other reported 0.9.

ticketlabelledjevopenai
T-014 "your app lost my work"technicaltechnical @ 0.42billing @ 0.90
T-017 "third time asking"escalationescalation @ 0.55billing @ 0.90
T-024 "account compromised?"escalationescalation @ 0.32technical @ 0.90

What would settle the open question

The calibration claim needs a workload where the error count is large enough to measure against — a few hundred real tickets, not 24 invented ones. Until then the speed and cost claims are the ones this repo can vouch for.

Caveats

The 24 tickets are invented and the labels are one reviewer's judgment, about a third of them on deliberately ambiguous cases. That is enough to smoke-test the harness and see where two engines diverge; it is not enough to conclude anything about either model, and the run above says so in more detail. Swap in your own traffic before reading anything into a number here.

Every accuracy-style metric on 24 cases moves in steps of 4 percentage points, so a one-ticket difference looks like a margin. The confidence metrics are worse than that: they are driven by however many tickets the engine got wrong, which here was three. Running the suite twice bears this out — Jev's department choices were identical across both runs, but needs_human accuracy moved 0.667 → 0.583 and the abstention rate 0.125 → 0.083, purely from probabilities drifting across a fixed threshold.

The comparator matters as much as the sample. gpt-4o-mini is a small, non-reasoning model, which makes it a demanding baseline for a speed claim and a generous one for an accuracy claim. TypeSafe's own 40–200× figures are measured against frontier reasoning models; do not read 7.1× here as contradicting them.

The thresholds in graph/policy.py are starting points, not validated defaults. Tune them against your own data and your own consequences.

Layout

docs/
  index.html            visual benchmark report
src/agentbench/
  types.py              domain types, normalized judgment shapes
  config.py             .env loading
  bench.py              sweep + metrics
  cli.py                run / bench / graph
  engines/
    questions.py        the questions, defined once, engine-neutral
    base.py             the DecisionEngine protocol
    jev.py              TypeSafe System One
    openai_engine.py    strict JSON schema
    fake.py             offline heuristic, no key needed
  graph/
    state.py            shared state schema
    policy.py           thresholds, SLAs, money rules — no model calls
    supervisor.py       parent graph
    subgraphs/          billing, technical, escalation
  data/tickets.py       24 labeled tickets
tests/

Adding a third engine means implementing one method, ask(state, questions), and registering it in engines/__init__.py.