Decision
Go · package github.com/muthuishere/toolnexus/golang · SPEC §8B · golang/classifier.go
type Decision struct { Model string Answers map[string]DecisionAnswer Usage ClassifierUsage Calibrated bool}
func (d Decision) Noul(key string) (NoulAnswer, error)func (d Decision) Choice(key string) (ChoiceAnswer, error)func (d Decision) Score(key string) (ScoreAnswer, error)
func NearUniform(probabilities map[string]float64) boolOne answer per question under the caller’s own keys, read through typed accessors that fail loudly
rather than hand back a zero. Model echoes what actually answered, which may be more specific
than the model you asked for (jev-latest → typesafe/jev-1.13-20260917).
DecisionAnswer is a sealed interface with three implementations, and AnswerType() is the wire
discriminator ("noul" | "choice" | "score"). It is named DecisionAnswer rather than Answer
because §10 already owns Answer — same idea, different seam.
The three answer shapes
Section titled “The three answer shapes”type NoulAnswer struct { Noul float64 // 0..1. NO confidence: the number IS the answer.}
type ChoiceAnswer struct { Choice string Probabilities map[string]float64 // one entry per OFFERED option Confidence float64 NearUniform bool // DERIVED on decode, never read from the wire}
type ScoreAnswer struct { Score float64 // MAY fall between levels; always within the rubric Legend map[string]string // the rubric echoed back, keyed by level index Probabilities map[string]float64 // one entry per level index Confidence float64}
func (a ScoreAnswer) Levels() []string // the legend in level order, which the map losesConfidence reports on the question, not on the answer. The worst working encoding measured
carried the highest median confidence (0.82) — see The encoding
obligation.
When to use it
Section titled “When to use it”Read a Decision through the typed accessors, not by type-asserting Answers[key] by hand. The
accessor is how a wrong-type read becomes a legible error instead of a silent zero value.
Why this and not the alternative
Section titled “Why this and not the alternative”Usage, and why cost may be absent
Section titled “Usage, and why cost may be absent”type ClassifierUsage struct { InputTokens int OutputTokens int Cost *float64 // nil = this backend does not report one}Cost is a pointer because absent is not zero. TypeSafe’s own API never reports a cost; a
gateway such as OpenRouter does. Printing $0.00 for a backend that simply does not say would be
a lie about money — print “not reported” instead.
Calibrated and NearUniform
Section titled “Calibrated and NearUniform”Calibrated travels with every decision. StyleSystemOne reports true; StyleLLM reports
false unless it derived its probabilities from provider token probabilities. On the wire, an
absent calibrated field decodes as true — the System One wire reports calibration by being
itself, and a backend that is not calibrated says so explicitly.
NearUniform is derived from the response on decode and never read from the wire — no wire change,
no request change, no fixture change. With n the number of entries in the probability map and
p_i their values as returned:
NearUniform ⇔ max over i of |p_i − 1/n| ≤ 0.05- the tolerance is 0.05 absolute and the comparison is inclusive — a maximum deviation of
exactly 0.05 is near-uniform (
NearUniformTolerance, computed infloat64); - the probabilities are never sorted, renormalised or rounded before the comparison, and an
offered option absent from the map counts as
0by not being an entry; n == 1⇒ true (a single option is trivially uniform); an empty map ⇒ false (there is no distribution at all).
Shared fixtures pin the boundary from both sides and never place a deviation within 1e-9 of the
tolerance, so the rule is decidable in float64 without a port-specific epsilon — but note that a
literal 0.55 is 0.05000000000000004 away from 0.5, so do not build your own boundary test
out of decimals that do not represent exactly.
It is absolute rather than relative because a relative band collapses below the noise floor: at 255
options 1/n is 0.0039, finer than the two-decimal rounding the wire already applies. 0.05 is
roughly 3σ of measured backend non-determinism, and it is the separation that matters — an
undescribed-options encoding returned a median top probability of 0.29 on a four-option choice
(deviation 0.04, inside), the described one 0.80 (deviation 0.55, far outside).
Examples
Section titled “Examples”1. The smallest useful call — typed accessors, and how they fail
Section titled “1. The smallest useful call — typed accessors, and how they fail”package main
import ( "context" "fmt" "log" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { state := "rm -rf ./build" questions := map[string]toolnexus.Question{ "risk": toolnexus.ScoreQuestion{ Instructions: "How destructive is this shell command?", Criteria: []string{"harmless", "reversible", "destructive"}, }, } recorded := `{"model":"typesafe/jev-1.13-20260917","calibrated":true, "answers":{"risk":{"type":"score","score":1.21, "legend":{"0":"harmless","1":"reversible","2":"destructive"}, "probabilities":{"0":0.10,"1":0.59,"2":0.31},"confidence":0.71}}, "usage":{"input_tokens":210,"output_tokens":24}}`
judge, err := toolnexus.CreateClassifier(toolnexus.ClassifierOptions{ Style: toolnexus.StyleStatic, Model: "typesafe/jev-1.13", Decisions: []toolnexus.RecordedDecision{{State: state, Questions: questions, Response: []byte(recorded)}}, }) if err != nil { log.Fatal(err) }
d, err := judge.Evaluate(context.Background(), state, questions) if err != nil { log.Fatal(err) }
risk, err := d.Score("risk") if err != nil { log.Fatal(err) }
// Wrong type: named, not a silent zero. if _, err := d.Noul("risk"); err == nil || !strings.Contains(err.Error(), "is a score answer, not noul") { log.Fatalf("expected a typed read failure, got %v", err) } // Absent key: a different message, on purpose. if _, err := d.Score("urgency"); err == nil || !strings.Contains(err.Error(), "no answer") { log.Fatalf("expected an absent-key failure, got %v", err) }
// The AUTHORITY stays here, in code — the decision only informs it. if risk.Score >= 1.5 { fmt.Println("would require approval") }
fmt.Printf("ok: score=%v confidence=%v levels=%v\n", risk.Score, risk.Confidence, risk.Levels())}2. The realistic case — NearUniform at the boundary
Section titled “2. The realistic case — NearUniform at the boundary”package main
import ( "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { cases := []struct { name string p map[string]float64 want bool }{ // Deviation of exactly 0.05 from 1/n = 0.25: the comparison is INCLUSIVE, // so this IS near-uniform. {"at the tolerance", map[string]float64{"a": 0.30, "b": 0.25, "c": 0.25, "d": 0.20}, true}, // Past it. {"just outside", map[string]float64{"a": 0.56, "b": 0.44}, false}, // A real answer: the model had something to rank on. {"decisive", map[string]float64{"a": 0.97, "b": 0.03}, false}, // Never renormalised: these sum to 0.5 and are still flat around 1/n. {"n == 1 is trivially uniform", map[string]float64{"only": 0.02}, true}, // No distribution at all. {"empty", map[string]float64{}, false}, }
for _, c := range cases { if got := toolnexus.NearUniform(c.p); got != c.want { log.Fatalf("%s: NearUniform = %v, want %v", c.name, got, c.want) } }
// The same rule is applied on decode, so ChoiceAnswer.NearUniform is already set. fmt.Println("ok: tolerance", toolnexus.NearUniformTolerance, "inclusive, five cases agree")}3. The full surface — a flat decision, Calibrated, and an unreported cost
Section titled “3. The full surface — a flat decision, Calibrated, and an unreported cost”A custom backend lets the whole Decision be written by hand, which is the clearest way to show
what the two flags mean when they are not the happy values.
package main
import ( "context" "fmt" "log" "strconv"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { judge, err := toolnexus.CreateClassifier(toolnexus.ClassifierOptions{ Style: toolnexus.StyleCustom, Evaluate: func(_ context.Context, _ any, _ map[string]toolnexus.Question) (toolnexus.Decision, error) { return toolnexus.Decision{ Model: "gpt-4o-mini", Answers: map[string]toolnexus.DecisionAnswer{ "desk": toolnexus.ChoiceAnswer{ Choice: "billing", Probabilities: map[string]float64{"billing": 0.34, "shipping": 0.33, "technical": 0.33}, Confidence: 0.95, // round self-reported numbers are an llm-style tell NearUniform: toolnexus.NearUniform(map[string]float64{"billing": 0.34, "shipping": 0.33, "technical": 0.33}), }, }, // An llm-style backend self-reports; the numbers are not token // probabilities, so no threshold tuned elsewhere transfers here. Calibrated: false, Usage: toolnexus.ClassifierUsage{InputTokens: 516, OutputTokens: 72}, // Cost nil }, nil }, }) if err != nil { log.Fatal(err) }
d, err := judge.Evaluate(context.Background(), "charged twice", map[string]toolnexus.Question{ "desk": toolnexus.ChoiceOver("Which desk owns this?", map[string]string{ "billing": "own it here when money moved: a duplicate charge, a refund owed", "shipping": "own it here when a physical parcel is late or damaged", "technical": "own it here when the product itself errors", }), }) if err != nil { log.Fatal(err) }
desk, err := d.Choice("desk") if err != nil { log.Fatal(err) } if !desk.NearUniform { log.Fatal("this distribution is flat within 0.05 of 1/3") } if d.Calibrated { log.Fatal("an llm-style decision reports Calibrated: false") }
// Absent is NOT zero: printing $0.00 here would be a lie about money. cost := "not reported by this backend" if d.Usage.Cost != nil { cost = "$" + strconv.FormatFloat(*d.Usage.Cost, 'f', -1, 64) }
fmt.Printf("ok: choice=%s nearUniform=%v calibrated=%v cost=%s\n", desk.Choice, desk.NearUniform, d.Calibrated, cost)}A flat distribution with a high self-reported confidence is the signature this page exists to make legible: the model had nothing to rank on, and said so with numbers that look decisive.
See also
Section titled “See also”CreateClassifier— A sibling of the client: pre-declared typed questions in, calibrated answers out — no messages, no tool calling, no loop.NoulQuestion— The three question types, the criteria each one needs, and the limits enforced client-side before the request.- The encoding obligation — why a flat distribution is usually your criteria, not the model.
- Typed decisions · Backends · Cookbook