scripts/decide.py takes an OpenAI Decisions API request (input plus
questions of type predicate, choice or score) and returns its response
shape: a probability per option, the chosen value or score, and confidence.
Claude Haiku 5.5 answers. Its logits are not exposed, so the distribution is
estimated: each of k parallel samples states a probability per option with the
options in a different order, the samples are averaged, and the average is
temperature-scaled from a labelled eval. Disagreement between samples lowers
confidence and shows up in diagnostics.agreement.
Expect about 85% of what a purpose-built decision model gets: on the bundled
104-item eval, accuracy 0.846 against Jev's 0.894, with a calibration error
(ECE 0.041) that matches it. The whole accuracy gap was on eight-way intent
questions; yes/no and rubric questions tied. Disagreement between samples is
the strongest warning sign. references/method.md has the numbers.
When NOT to use this skill
| Situation | Use instead |
|---|---|
| The label set is too large to list in a prompt (a taxonomy, hundreds of tags) | hallucinating-labels |
| The answer needs reasoning, multi-step work or a written explanation | a normal Claude call; this skill forbids explanation |
| You need extracted fields or free-form JSON | structured outputs on the Messages API |
A real decision model is reachable (OpenAI /v1/decisions, Strands Decider, Jev) and latency matters |
that model: 0.1–0.3 s against ~0.7–4 s here |
| Picking which Claude tier runs a subagent | agent-routing |
| The evidence is an image | not supported; the transports send text only |
Abandon a run when diagnostics.agreement is below 1 on most of a batch: the
questions or option descriptions are ambiguous, and more samples will not fix
them. Rewrite the options (distinct, observable criteria) before sampling more.
Procedure
-
Write the request as JSON in the OpenAI shape. Give every question a unique
name. Options needdescriptions that say when each applies; add anotherchoice when the set is not exhaustive. Scorelevelsgo lowest first.{"input": "I was charged twice for my order.", "questions": [ {"type": "choice", "name": "department", "instructions": "Which department should handle this?", "choices": [{"value": "billing", "description": "Payments, invoices, refunds."}, {"value": "shipping", "description": "Delivery and tracking."}, {"value": "other", "description": "Anything else."}]}, {"type": "predicate", "name": "angry", "instructions": "Is the customer expressing anger?"}, {"type": "score", "name": "severity", "instructions": "How severe is this?", "levels": [{"label": "Cosmetic", "description": "No lost functionality."}, {"label": "Workaround", "description": "Fails, another way works."}, {"label": "Blocked", "description": "Fails with no workaround."}]}]} -
Pick the transport.
--transport auto(the default) takes the first that works:Transport Needs Wall time per request apipip install anthropicandANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKEN~0.7 s expected (the API time measured inside claude -p)clithe claudeCLI, logged in~4 s, measured bedrockpip install anthropic,AWS_REGION, AWS credentials; explicit onlyas apiWith none of them, use the subagent path in step 3b.
-
Run it.
a. With a transport:
S=/mnt/skills/user/deciding-with-confidence/scripts # or wherever the skill lives python3 $S/decide.py run request.json # k=3 by default echo '{...}' | python3 $S/decide.py run -From Python:
sys.path.insert(0, S); import decide; decide.decide(req)returns the response dict, or None when no sample was usable.b. In a Claude Code session, the plugin's
decidersubagent is the cheaper path: it is pinned toclaude-haiku-5-5(the model the calibration was fitted on) and carries its instructions inline, so it answers without a tool call. Install thedeciding-with-confidenceplugin, or copyagents/decider.mdto.claude/agents/; agent definitions load at session start. Then:python3 $S/decide.py emit request.json -k 3 --agent deciding-with-confidence:decider > batch.jsonUse the name the Agent tool lists:
deciding-with-confidence:deciderfrom the plugin,deciderfrom.claude/agents/. Send eachsamples[i].promptverbatim as an Agent call with thatsubagent_type, all in one message. Collect the reply texts into a JSON list inreplies.json, thenpython3 $S/decide.py aggregate batch.json replies.json. Pool before acting: a single reply carries no agreement signal and no calibration.Without the agent installed, omit
--agent: the system prompt then rides inside each prompt for a stock general-purpose subagent withmodel: haiku. That works and costs about 58K tokens a sample against 9K. -
Read the answer. Predicates give
probability(P true). Choices givechoice,probabilitiesandconfidence. Scores givescore(the probability-weighted level index, so 1.4 sits between levels 1 and 2),probabilitiesandconfidence. Every answer carriesdiagnostics:agreement,spread, the pre-calibrationrawpool andper_sample. -
Act on it with thresholds, never on the top answer alone.
agreement < 1(the samples' top answers differ): escalate. On the bundled eval, unanimous answers were 92% right and split ones 44%.- Top probability under 0.7: escalate. Together with the split rule that sent 23 of 104 items onward and left 6 wrong, assuming the escalation target got every escalated item right. The 0.7 was chosen on the same data, so treat it as a starting point.
- Set the final thresholds from labelled examples of your own traffic, weighing the cost of a false positive against a false negative.
-
Calibrate on your own labels when you have 50 or more per question type:
python3 $S/decide.py eval my_labelled.jsonl --out results.jsonl # {id, input, questions, labels} python3 $S/decide.py calibrate results.jsonl --out my-cal.json export DECIDER_CALIBRATION=$PWD/my-cal.jsonThe bundled
assets/calibration.jsonfits onlychoice(T = 0.62, fitted through theclitransport). Predicate and score keep T = 1 until a type has 50 labels, because smaller sets swung between folds.
Earned exceptions
| Rule | Banned when | Earned when |
|---|---|---|
| k=3 samples | latency or cost is the constraint | always the default: parallel samples cost no wall time, and k=1 loses the agreement signal |
| Bundled calibration | your transport or domain differs from the eval's (cli, banking and support text) |
you have not yet labelled 50 items of your own |
Escalate on agreement < 1 |
nothing stronger sits behind it | a stronger model or a human can take the split items |
Common failure modes
decide: no usable sample; first error: TypeError: "Could not resolve authentication method..."→ theapitransport found no key. SetANTHROPIC_API_KEY, or pass--transport cli.decide: no transport→ no key and noclaudeon PATH. Use step 3b.diagnostics.samplesbelow k → some replies did not parse. Usually a transport timeout under heavy concurrency; lowerDECIDER_WORKERS(default 8) or raiseDECIDER_TIMEOUT(default 60 s).- Every answer near uniform (confidence around 0.1–0.2 throughout) → options without descriptions, or descriptions that overlap. Rewrite them so each has distinct, observable criteria; more samples will not help.
- A subagent reply with prose around the JSON →
aggregatetakes the outermost{...}and copes. A reply with no JSON at all counts as a failed sample. - Transcript text showing up as evidence in the subagent path → the
[no-context]line was dropped from the prompt. Send the emitted prompt verbatim.
Verification
Run the tests, then one live request:
python3 -m pytest /mnt/skills/user/deciding-with-confidence/tests -q # 39 pass, no network
echo '{"input":"Nobody can log in since the 2pm deploy.","questions":[{"type":"predicate","name":"outage","instructions":"Is a service down for users?"}]}' \
| python3 $S/decide.py run -
The live answer should have probability above 0.9 and diagnostics.samples
equal to 3. A bad success is an answer with samples: 1 from a k=3 run:
it looks normal but has lost the agreement signal. Check samples when a
batch runs under load.
Diagnosed failures
- 2026-10-09, first eval: 14 of 520 samples were discarded because Haiku left the options it ruled out of 8-way choices. Missing keys now share the probability mass the sample left unassigned, and the re-run lost none. The prompt also asks for ruled-out options at 0.001.
- 2026-10-09:
claude -p --json-schemadoubled API time (2.2 s against 0.8 s) with a second turn and three times the output tokens. Theclitransport sends the JSON contract in the system prompt instead. - 2026-10-09: the claude-workspace PreToolUse hook that appends
parent-transcript chunks to Agent prompts would have put them in the
evidence.
emitappends[no-context], which that hook honours.