Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix A — Why Jev or Kev Instead of a Regular LLM, and How Decision Engines Work Inside

Read this when someone on your team asks “why not just call GPT/Claude with a JSON schema?”, when you need to choose between hosted Jev, self-hosted Kev and the open-source OpenDecision engine, or when you want to know why the rules in Chapters 2–5 and 9–12 exist (fan-out, no chained questions, permutation tests, thresholds per backend).

You get: a decision matrix for LLM vs decision engine, the architecture of three engines (each labelled by how much of it is actually public), where the engine sits in the SmartPlant system, an evaluation plan, and triage.probes, a tool that tests the architectural claims on your backend.

Jev is a proprietary cloud hosted service. Kev and OpenDecision are separate open-source projects. Their internals are not equally documented.


A.1 The problem with asking an LLM for a decision

Most production “AI decisions” today look like this:

resp = llm.generate(prompt + ticket, response_format=TriageSchema)   # 2–8 s
route = resp.parsed.department                                       # hope it parsed
confidence = resp.parsed.confidence                                  # "0.9", says the model
if confidence > 0.85:
    auto_route(route)

Four things are wrong with this, and none of them is about intelligence:

  1. The confidence is text. "0.9" is two or three tokens sampled like any other tokens. Nothing trained them to be right 90 % of the time. Fraud, moderation and routing systems that gate on it are gating on prose.
  2. Generation is wasted work for a decision. To return {"department": "billing"} the model decodes every token in sequence: braces, key, quotes, value. The decision was settled long before the closing brace.
  3. Schemas fail at runtime. Constrained decoding helps, but generation can still stop early or fail validation. The AI SDK reports these as structured-output errors, so your code needs a retry path for an output format problem.
  4. Questions run in series or pay twice. Ask five questions and you either pay five prompts’ worth of input tokens, or ask them in one prompt where answer 1 becomes context for answer 2.

A decision engine (hosted Jev, self-hosted Kev, or an NLI engine such as OpenDecision) changes the computation, not just the API:

LLM + structured outputJev (hosted)Kev (self-hosted)OpenDecision (self-hosted)
What the model producesTokens, then parsedA probability distribution per questionSame as JevAn entailment score per candidate label (NLI)
ConfidenceGenerated text, uncalibratedRead from the model; trained against outcomes (RLCD)Read from the model; fitted temperature (KEV_TEMPERATURE unset)NLI scores; the README says not calibrated
Invalid label possible?Yes (must validate)No: output is a distribution over your optionsNoNo
Many questionsSerial, or entangled in one promptParallel, isolated, shared state paid onceSame (and /v1/systemone/separate to compare)Supported; each label is scored against the state
Typical latency2–8 s (frontier LLMs: 3–329 s on hard items, per TypeSafe)~70–500 ms~18 ms model time (Kev-4B, H100) to ~150 ms (Kev-0.8B, M5)Measure it: a large encoder, grows with option count
Price$0.15–$10 / M input + output$0.042 / M input, output freeHardware onlyHardware only
Writes text, explains, plansYesNoNoNo

Toy illustration: what “reading a distribution” means

This isn’t Jev’s code. It’s the arithmetic that sits at the end of a readout, and it runs:

# toy_readout.py: illustrative only
import math
from typing import Sequence


def softmax(z: Sequence[float]) -> list[float]:
    m = max(z)
    e = [math.exp(v - m) for v in z]
    s = sum(e)
    return [v / s for v in e]


def sigmoid(z: float) -> float:
    return 1 / (1 + math.exp(-z))


# One forward pass over (shared state + this question) ends at a decision position.
# A readout maps that hidden state to one logit per allowed option. No tokens are generated.
p = softmax([2.1, 0.4, -1.0])                         # Choice: technical, billing, other
noul = sigmoid(1.7)                                   # Noul: P(yes)
score_p = softmax([-2.0, 1.2, 0.6])                   # Score: ordered levels 0..2
score = sum(i * q for i, q in enumerate(score_p))     # probability-weighted mean level
confidence = (max(p) - 1 / len(p)) / (1 - 1 / len(p)) # distance of the winner above uniform

print([round(x, 3) for x in p], round(noul, 3), round(score, 2), round(confidence, 3))
# [0.815, 0.149, 0.037] 0.846 1.32 0.722

A.2 When an LLM is still the right tool

System One doesn’t replace generative models. The book’s rule still decides: code computes, LLMs create, Jev/Kev decide, fresh state proves.

The honest trade-off. In the Every comparison (Chapter 7), Jev caught 6 of 7 planted defects and Fable caught 7 of 7, at about 1/25 the latency and 1/580 the cost. A slightly weaker check you can afford to run on every item usually beats a stronger one you can only afford to sample. Where it doesn’t, cascade.

A.3 Three engines, three levels of evidence

OpenDecisionJevKev
Who runs itYou (local / self-hosted)TypeSafe (hosted API)You (local / self-hosted)
What’s publicCode, model choice, APIAPI contract, two design facts (parallel readout, RLCD)Code, weights, training recipe
What’s inferredLittleBackbone, MoE, option handling, scheduling (§A.5)Little
Speaks the System One APIYes, /v1/systemoneYes (it is the API)Yes, plus /permute, /separate
Book clientKevDecisionClient(base_url=…)JevDecisionClientKevDecisionClient

Because all three serve the same System One request shape, one DecisionClient contract covers all of them. That’s what makes the comparison in §A.10 a configuration change rather than a rewrite. (OpenAI’s Decisions API answers the same three primitives with a different wire format; Chapter 7 adapts it.)

A.4 OpenDecision: an open NLI decision engine (published implementation)

OpenDecision is the most inspectable of the three. It doesn’t generate text, and it isn’t a decoder at all. It’s a zero-shot natural-language-inference (NLI) classifier. The default backend is MoritzLaurer/ModernBERT-large-zeroshot-v2.0, a compact encoder. It offers Choice, Noul and Score, and serves a /v1/systemone endpoint compatible with the TypeSafe SDK’s request/response flow. (The project’s material also describes a DeBERTa-v3-large backend and a Relation/document-evidence path; check the repository for which of these your version ships.)

What happens inside:

  1. Input preparation. The state is serialised, and each question’s instructions and candidate labels become NLI inputs: the state is the premise, each candidate answer a hypothesis.
  2. Model scoring. The encoder scores how strongly the premise entails each hypothesis. No answer is generated token by token.
  3. Decision formatting. Scores become a typed result (choice, probabilities, confidence), the same shape Jev returns.

Run it next to the book’s workspace, in its own folder and venv, like Kev (Python 3.13 recommended; the model downloads on first use):

git clone https://github.com/deepanwadhwa/OpenDecision.git && cd OpenDecision
uv sync --python 3.13 --all-groups
uv run uvicorn opendecision.api.app:app --app-dir src --host 127.0.0.1 --port 8000
curl http://127.0.0.1:8000/health            # interactive docs: http://127.0.0.1:8000/docs

It speaks the System One API, so the book’s Kev client talks to it with a different base_url. No new client code:

from triage.jev_client import KevDecisionClient

opendecision = KevDecisionClient(base_url="http://127.0.0.1:8000")   # same contract, NLI backend
# the Appendix A probes and the Part II eval suite, pointed at OpenDecision
KEV_BASE_URL=http://127.0.0.1:8000 DECISION_BACKEND=kev uv run python -m triage.probes
KEV_BASE_URL=http://127.0.0.1:8000 uv run python mini-projects/11-smartplant-eval-suite/run_eval.py --kev

The eval suite labels the row kev because it uses the Kev client; rename it in your report. If the server rejects the model name, set KEV_MODEL to the id its docs list.

Caution. v0.1.0 is a developer preview. Its README says the probabilities are NLI scores that “should not be assumed calibrated”, and that confidence isn’t a probability of being right. Exact dates, symbolic reasoning and subtle policies are hard for the current backend. Use it as an inspectable, CPU-friendly baseline and tune every threshold on your own labels.

A.5 Jev: the public contract, and the architecture unmasked

The contract you can build on (public API contract)

TypeSafe describes Jev as a model that takes shared state plus a map of typed questions and returns one typed answer per question, keyed the same way. That boundary is all your code should depend on:

  1. Shared state. Common context, natural-language text or JSON, seen by every question.
  2. Typed questions. Choice picks among your options, Score rates an ordered scale, Noul estimates whether a statement is true.
  3. Independent evaluation. Many questions run against the same state in one request, and one question’s answer never becomes another’s input.
  4. Structured output. Results keyed to your question ids, with distributions and confidence where the type supports them.

A concentrated distribution isn’t a guarantee of correctness. Validate thresholds against representative labelled examples (Chapter 4).

What’s behind it (inferred)

TypeSafe hasn’t published weights, layer counts or the network design. The rest of this section follows Archer Hume’s “Jev’s Architecture Unmasked” (17 September 2026), which reverse-engineers Jev 1.13.0 from about 10,000 API calls. TypeSafe has published only two parts: parallel outputs instead of token-by-token generation, and RLCD training. Everything else is inference from black-box behaviour.

Hume’s best reconstruction is a causal transformer with a shared-state prefix, isolated question suffixes, listwise option processing, typed numerical readouts, and training aimed at predictive distributions, probably on a sparse (MoE) backbone.

Evidence map

#ComponentStatusKey evidence (Jev 1.13.0, one account, one region)
1Readout, not decodingPublished + observedTypeSafe: “outputs all probabilities in parallel instead of autoregressively generating by token”. A 200-option question returned as fast as a 2-option one. output_tokens behaves like a post-hoc billing count (it even counts the question ID, which the docs say never reaches the model)
2Shared state, isolated questionsObservedInput tokens are additive (1 Noul = 268, 2 = 276). A secret in a sibling question gave P(correct) = 0.00; the same secret in the state gave 0.90–0.92. Limits: ~32,768 tokens per branch, ~65,536 per request (state counted once)
3Causal backboneInferredFrontier-scale knowledge (84.6 % MMLU-Pro), RLCD described as post-training a pretrained LLM, prefix-cache economics. The experiments can’t distinguish causal from bidirectional attention
4Options interact (listwise)ObservedAdding an irrelevant 5th option moved log-odds between two others by a mean of −0.28 (95 % CI ≈ −0.36 to −0.19, 10 blocks). Reversing option order moved a support classification from ~0.84–0.89 to ~0.93–0.96. Fake options injected via text never displaced real ones
5Trained distributions (RLCD)Published (name) + inferred (recipe)“Reinforcement Learning for Calibrated Decisions.” Likely proper scoring rules (log loss / Brier). MMLU sample ECE 0.0313 (1,200 items, mostly near-certain). Fresh maths showed gaps (modular exponents: 56 % accuracy at 35 % mean probability)
6Sparse MoE backboneInferred (least certain)~30k tokens in ~160 ms (a 29,835-token state with one question had a median of 218 ms). A prefill-only model is compute-bound, which MoE helps
7Batch schedulingInferredLatency nearly flat to ~100 questions, then rising. 1,500 questions on a short state took a median of 610 ms. Question text costs about 2× state per token

Other observations worth knowing:

  • Tokenizer: matches none of 192 public tokenizers. It splits digits individually, and Qwen is the closest (348 of 415 probes). The base model is unknown.
  • 255-option cap = 2⁸ − 1, enforced by request validation rather than by the model.
  • Non-determinism: identical requests (even duplicates within one request) return slightly different probabilities.
  • confidence is a shape statistic. For Choice, the official adapter computes c = (p_max − 1/K) / (1 − 1/K): three options with p_max = 0.8 give 0.7. It measures how far the winner is above uniform, not a learned probability of being right. Score uses a different, distance-from-mode formula.

A.6 Kev: the same idea, open (published implementation)

Kev’s README describes small decision models that follow the architecture laid out for Jev, served through a System One-compatible API. Unlike Jev, the mechanism is published:

ComponentWhat the README saysWhy it matters to you
Qwen base + LoRAEach checkpoint is a rank-16 LoRA adapter plus a small pointer head on a Qwen base (Qwen3 / 3.5 / 3.8). Kev-27B fine-tunes every weight insteadCheap to retrain on your own decisions (Chapter 13)
Token layoutThe state, then one line per question: <q> instructions, <opt>-wrapped options, a closing <decide>Long instructions cost tokens on every question line
Question isolationOn attention-only bases, the mask lets a token see the state and its own question only, and position ids restart after the state. On Gated DeltaNet bases (Qwen3.5/3.8), which ignore masks, each question runs as its own row with the state computed once and its cache reusedIsolation is exact, so a question can’t read a sibling’s text. Dependencies still need a second request
Pointer headCompares each option’s </opt> hidden state with the question’s <decide> hidden state; a softmax turns the scores into probabilities. Trained with cross-entropy on the correct answer<decide> sees the whole option list, so options are scored listwise: order and set changes can move probabilities, exactly as Hume observed for Jev
Temperature calibrationEach checkpoint stores a temperature applied by the pointer head. It “never changes which answer wins”Calibration moves confidence, never the argmax. Thresholds need re-tuning after a checkpoint change even when accuracy is identical

Being open helps in three more ways:

  • You can test the claims locally. /v1/systemone/separate runs each question in its own forward pass (so you can compare against shared-prefix fan-out), and /v1/systemone/permute re-asks one Choice under up to 64 option orders, which is the order-sensitivity test from §A.5 as an endpoint.
  • Calibration is explicit. Kev applies a fitted temperature by default. KEV_TEMPERATURE=1.0 returns raw probabilities, which shows how much the calibration step is doing (Kev-9B’s calibration error on new sources drops from 0.106 to 0.042 with it). For Jev, the API can’t tell training-time calibration from post-hoc scaling.
  • You can retrain the readout on your decisions. Chapter 13’s training_export + kev.train loop is not possible with a hosted model.

The trade: Kev’s published calibration is a little weaker than Jev’s (at a 5 % error budget it automates 0.45–0.57 of decisions against Jev’s 0.70), and you operate the GPUs.

A.7 Side by side

CapabilityOpenDecisionJevKev
DeploymentLocal / self-hostedHosted serviceLocal / self-hosted
Core approachEncoder NLI classification (ModernBERT-large)Proprietary decision model; internals not disclosed (inferred: causal backbone + readout)Qwen backbone + LoRA + pointer head
Typed decisionsChoice, Noul, ScoreChoice, Score, NoulChoice, Score, Noul
Multiple questionsSupportedOne request, isolated branchesOne request, exact isolation (mask or rows)
Options interact?Each label scored against the stateYes (observed, listwise)Yes (by design, listwise)
CalibrationNot calibrated: validateTrained (RLCD); validate on your dataCheckpoint temperature; validate on your data
CustomisationSwap the backend, change the codeNone beyond the contractTraining code and weights available
StatusDeveloper preview (v0.1.0)Early accessOpen source, fast-moving
Main reason to pick itInspectable, CPU-friendly baselineManaged, calibrated, no GPUs to runData stays on-site; you can retrain

A.8 What each trait means for your code

The architecture is useful because it explains rules this book already asked you to follow, and adds a few.

Architectural traitEngineering consequenceWhere in this book
Shared prefix, encoded oncePut the big context in state, ask many questions: speculative fan-out is nearly freeCh3 (13 questions: 10× faster, 12.2× cheaper)
Branches isolatedA question can’t use another’s answer. Dependencies need a second requestCh3 “don’t chain inside one call”, Ch6 two-stage re-ranker
Question text costs ~2× state per tokenKeep instructions tight; move shared detail into stateCh2 rules, Ch10 compact states
Latency flat to ~100 questionsBatch per decision, not per question; per-item Nouls for counting are fineCh5 counting pattern
Listwise options, order-sensitivePermutation-test every Choice whose threshold matters; never place a reference card first; keep the exit option stableCh10 (/permute on Kev), §A.11 probes
Irrelevant options shift oddsAdding/removing an option is a contract change: re-run evals; prune ineligible options before the callCh2 versioning, Ch3/Ch10 eligibility
Fake options can’t be injectedTyped output is a real defence against label forgery, but not against steering probabilitiesCh3 untrusted text, Ch5 defence in depth
confidence = distance from uniformGate on it, but calibrate thresholds per backend and model; log the full distribution and the marginCh4 sweep, Ch9 “never reuse Jev thresholds for Kev”
Calibration is aggregate, uneven by domainMeasure reliability on your labels; don’t trust a headline ECECh4, Ch7 (ECE/Brier), Ch12
Readout ≠ reasoningMath, dates, counting stay in code; multi-hop goes to a stage or an LLMCh5 jaggedness map
Non-deterministic servingDon’t assert exact floats in tests; compare bands and decisionsCh5 eval suite
output_tokens is billing, not computeBudget on input tokens and request countCh1 economics
32k per branch / 65k per requestSize states for the branch limit; Kev ≤ 9B is validated to 8,192Ch3, Ch9
Calibration via temperature (Kev)A checkpoint change can move every confidence without changing a single decision: re-tune bands, not just accuracyCh4, Ch13
NLI scores aren’t probabilities (OpenDecision)Use rank and margin, tune thresholds empirically, never reuse Jev/Kev bandsCh4, Ch12
NLI cost grows with option countPrune ineligible options before the call; keep Choices shortCh3, Ch10 eligibility

A.9 Where the engine sits: SmartPlant (proposed application architecture)

The decision engine is one component. Telemetry processing, equipment state, production context, deterministic policy, approvals, action execution and audit logging belong to the application, which is exactly how Part II builds SmartPlant:

Example: a motor with rising vibration during a critical batch

This is SmartPlant scenario S05 (Chapter 12): vibration in the alert zone and rising fast, bearing temperature rising, heavy load, a critical production order running, and no backup motor. The same SMARTPLANT_QUESTIONS contract (Chapter 10) goes to whichever engine is configured:

PrimitiveQuestion idAsks
Choiceoperational_actionMonitor, inspect, schedule maintenance, transfer load, or request shutdown?
Scoreoperational_severityHow severe is the condition, on a five-level rubric?
Noulstandard_workflowCan the standard work-order workflow handle it, without a reliability specialist?
Choiceintervention_urgencyRoutine, priority, urgent, or critical?
uv run python mini-projects/09-smartplant-decision-service/run_decision_service.py --offline S05
S05 Degradation during critical production order
  state : M-104: vib C_alert/rising_fast, temp normal/rising_fast, load heavy, data valid, order CRITICAL, backup none
  result: URGENT / INSPECT / severity 4 / standard_cmms  [fake, 0.0 ms]
  why   : INSPECT at URGENT urgency because vibration C_alert (rising_fast); bearing temperature normal (rising_fast); load heavy; a critical-priority order is running (considered for planning, never overriding equipment protection); backup coverage none.

(--offline uses the scripted stand-in, so this shows the plumbing, not a model’s judgment. Drop the flag with a Kev or OpenDecision server running to see a real engine’s answer.)

The engine only proposes. The policy layer from Chapter 11 checks equipment limits, maintenance rules, production constraints and approval requirements before any work order or control request is issued.

Safety boundary. Safety interlocks, emergency stops and safety instrumented systems stay independent of the decision model. No model confidence ever overrides a protective trip (Chapter 11’s trip mirror latches for this reason).

A.10 Evaluation plan: compare engines on your scenarios

Architecture descriptions tell you what could work. Only a fixed scenario set tells you what does:

  1. Fix a labelled scenario set covering normal operation, rising vibration, overheating, sensor faults, maintenance conflicts, urgent production demand and ambiguous cases. SmartPlant’s ten scenarios (Chapter 12) are the starting point, and Chapter 5’s labelled cases play the same role for triage.
  2. Run identical state and questions through every engine. All three speak System One, so this is a base_url change: Jev, Kev and OpenDecision behind the same DecisionClient.
  3. Measure more than accuracy: false negatives on critical-risk scenarios, calibration (ECE, Brier), latency, throughput, operating cost, and behaviour when the engine is unavailable (scenario S10). Chapter 7’s harness and Chapter 12’s suite report these.
  4. Include a deterministic rules baseline (smartplant.fallback_rules) so the comparison shows whether a model adds measurable value at all.
  5. Keep safety-critical decisions outside the model’s authority and test fail-safe behaviour explicitly (Chapter 11).

A.11 Probe your backend: triage.probes

Hume’s caveat applies to every number above: one model version, one account, one region. Before you set a threshold that moves money or stops a motor, measure the traits on your backend. triage.probes turns four claims into tests that run against anything behind DecisionClient: Jev, Kev, OpenDecision or the adapter.

# capstone-project/src/triage/probes.py
"""Black-box probes for any System One backend (Jev, Kev, adapter) behind triage.DecisionClient.

Each probe turns an architectural claim from Appendix A into a test you can run on YOUR
backend, model version and region before you trust a threshold:

  order_sensitivity  - does reversing the option list move the winner's probability?
  sibling_isolation  - can one question see text placed in another question? (it shouldn't)
  irrelevant_option  - does adding an unrelated option change the odds between two others?
  fanout_latency     - is latency roughly flat as you add questions to one request?

DECISION_BACKEND=kev uv run python -m triage.probes
"""
from __future__ import annotations

import math
import statistics
import time
from dataclasses import dataclass
from typing import Any, Mapping, Sequence

from triage.jev_client import DecisionClient

TICKET = {"ticket": "The export button crashes the settings page in Safari. Works in Chrome."}
OPTIONS: dict[str, str] = {
    "technical": "Bugs, crashes, integration problems",
    "billing": "Charges, invoices, refunds",
    "sales": "Pricing, upgrades, new accounts",
    "other": "None of the above",
}


@dataclass(frozen=True, slots=True)
class ProbeResult:
    name: str
    value: float
    verdict: str
    detail: str


def choice_confidence(probs: Sequence[float]) -> float:
    """Confidence as reported for the official adapter: (p_max - 1/K) / (1 - 1/K).

    It measures how far the winner sits above uniform. It is NOT a learned estimate of correctness.
    """
    k = len(probs)
    if k <= 1:
        return 1.0
    return (max(probs) - 1 / k) / (1 - 1 / k)


def _choice(instr: str, criteria: Mapping[str, str | None]) -> dict[str, Any]:
    return {"type": "choice", "instructions": instr, "criteria": dict(criteria)}


def order_sensitivity(client: DecisionClient, repeats: int = 3, threshold: float = 0.9) -> ProbeResult:
    """Same evidence, options reversed. A shift across your threshold means the action can flip."""
    fwd, rev = [], []
    reversed_opts = dict(reversed(list(OPTIONS.items())))
    for _ in range(repeats):
        fwd.append(client.decide(TICKET, {"q": _choice("Which team should handle `ticket`?", OPTIONS)})
                   .choice("q").probabilities["technical"])
        rev.append(client.decide(TICKET, {"q": _choice("Which team should handle `ticket`?", reversed_opts)})
                   .choice("q").probabilities["technical"])
    a, b = statistics.fmean(fwd), statistics.fmean(rev)
    crosses = (a >= threshold) != (b >= threshold)
    return ProbeResult("order_sensitivity", round(abs(a - b), 3),
                       "RISK: crosses threshold" if crosses else "ok",
                       f"p(technical) forward={a:.3f} reversed={b:.3f} threshold={threshold}")


def sibling_isolation(client: DecisionClient) -> ProbeResult:
    """Put a secret in question A; ask question B about it. Isolated branches can't see it."""
    secret_q = {"type": "noul", "instructions": "The access code is PELICAN-42. Is the sky blue?"}
    ask = _choice("What is the access code mentioned anywhere in this request?",
                  {"PELICAN-42": None, "HERON-17": None, "not_stated": None})
    with_sibling = client.decide({"note": "routine check"}, {"a": secret_q, "b": ask}).choice("b")
    in_state = client.decide({"note": "The access code is PELICAN-42."}, {"b": ask}).choice("b")
    leak = with_sibling.probabilities.get("PELICAN-42", 0.0)
    return ProbeResult("sibling_isolation", round(leak, 3),
                       "ok: isolated" if leak < 0.5 else "UNEXPECTED: sibling visible",
                       f"p(code | sibling)={leak:.2f}  p(code | state)={in_state.probabilities.get('PELICAN-42', 0):.2f}")


def irrelevant_option(client: DecisionClient) -> ProbeResult:
    """Independent per-option scores would keep log-odds(technical:billing) fixed when a 5th option appears."""
    def log_odds(opts: Mapping[str, str]) -> float:
        p = client.decide(TICKET, {"q": _choice("Which team should handle `ticket`?", opts)}).choice("q").probabilities
        return math.log(max(p["technical"], 1e-6) / max(p["billing"], 1e-6))
    base = log_odds(OPTIONS)
    extra = log_odds({**OPTIONS, "facilities": "Office buildings, parking, cleaning"})
    return ProbeResult("irrelevant_option", round(extra - base, 3),
                       "options interact" if abs(extra - base) > 0.05 else "independent",
                       f"log-odds technical:billing {base:+.2f} -> {extra:+.2f}")


def fanout_latency(client: DecisionClient, sizes: Sequence[int] = (1, 10, 50, 100), repeats: int = 3) -> ProbeResult:
    """Questions are evaluated in parallel over a shared state: latency should stay nearly flat."""
    med: dict[int, float] = {}
    for n in sizes:
        qs = {f"q{i}": {"type": "noul", "instructions": f"Does `ticket` mention item number {i}?"} for i in range(n)}
        runs = []
        for _ in range(repeats):
            t0 = time.perf_counter()
            client.decide(TICKET, qs)
            runs.append((time.perf_counter() - t0) * 1000)
        med[n] = statistics.median(runs)
    ratio = med[sizes[-1]] / max(med[sizes[0]], 1.0)       # 1 ms floor: sub-ms fakes are noise
    return ProbeResult("fanout_latency", round(ratio, 2),
                       "flat-ish" if ratio < 3 else "grows with questions",
                       "  ".join(f"{n}q={ms:.0f}ms" for n, ms in med.items()))


def run_all(client: DecisionClient) -> list[ProbeResult]:
    return [order_sensitivity(client), sibling_isolation(client), irrelevant_option(client), fanout_latency(client)]


if __name__ == "__main__":
    from triage.jev_client import client_from_env

    c = client_from_env()
    print(f"backend={c.backend}")
    for r in run_all(c):
        print(f"{r.name:18} {r.value:>7}  {r.verdict:24} {r.detail}")
DECISION_BACKEND=kev uv run python -m triage.probes          # local Kev
DECISION_BACKEND=jev uv run python -m triage.probes          # hosted Jev
DECISION_BACKEND=adapter uv run python -m triage.probes      # an LLM through the adapter, for contrast
KEV_BASE_URL=http://127.0.0.1:8000 DECISION_BACKEND=kev uv run python -m triage.probes   # OpenDecision

Sample output from the test fake (it mimics the reported behaviours; it isn’t a measurement of any model, and it has no real latency):

order_sensitivity    0.171  ok                       p(technical) forward=0.542 reversed=0.713 threshold=0.9
sibling_isolation     0.05  ok: isolated             p(code | sibling)=0.05  p(code | state)=0.90
irrelevant_option   -0.229  options interact         log-odds technical:billing +0.80 -> +0.57
fanout_latency        0.03  flat-ish                 1q=0ms  10q=0ms  50q=0ms  100q=0ms

How to act on the results:

Probe resultAction
order_sensitivity crosses your thresholdFix option order in the contract, raise that action’s gate, or average over permutations (Kev /permute) for high-stakes Choices
sibling_isolation shows leakageYour backend isn’t isolating branches (the adapter often won’t). Don’t rely on independence; split requests
irrelevant_option shows interactionTreat every option change as a contract version bump and re-run Chapter 5/12 evals
fanout_latency grows steeplyCap questions per request at the knee and split the rest

The probe tests run offline with a fake that mimics these behaviours:

# capstone-project/tests/test_probes.py
"""Probes run against a fake backend that mimics the behaviours reported for Jev."""
from __future__ import annotations

import math
from typing import Any

from triage.jev_client import ChoiceResult, FakeDecisionClient, NoulResult
from triage.probes import choice_confidence, run_all

BASE = {"technical": 2.0, "billing": 1.2, "sales": 0.1, "other": 0.0, "facilities": -0.5}


def mimic(state: Any, questions: Any) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for qid, q in questions.items():
        if q["type"] == "noul":
            out[qid] = NoulResult(0.2)
            continue
        labels = list(q["criteria"])
        if "PELICAN-42" in labels:                       # isolation: only the state is visible
            hit = "PELICAN-42" in str(state)
            p = {l: (0.9 if (l == "PELICAN-42") == hit and l != "not_stated" else 0.05) for l in labels}
            if not hit:
                p = {l: (0.9 if l == "not_stated" else 0.05) for l in labels}
        else:                                            # order bias + set-dependent temperature
            temp = 1.0 + 0.4 * (len(labels) - 4)
            bonus = {labels[-1]: 0.6}                    # last option gets a small boost
            z = {l: (BASE.get(l, 0.0) + bonus.get(l, 0.0)) / temp for l in labels}
            m = sum(math.exp(v) for v in z.values())
            p = {l: math.exp(v) / m for l, v in z.items()}
        best = max(p, key=p.get)
        out[qid] = ChoiceResult(best, p, choice_confidence(list(p.values())))
    return out


def test_probes_detect_reported_behaviours() -> None:
    r = {x.name: x for x in run_all(FakeDecisionClient(mimic))}
    assert r["order_sensitivity"].value > 0
    assert r["sibling_isolation"].verdict.startswith("ok")
    assert r["irrelevant_option"].verdict == "options interact"


def test_confidence_formula() -> None:
    assert abs(choice_confidence([0.8, 0.1, 0.1]) - 0.7) < 1e-9
    assert choice_confidence([1.0]) == 1.0 and choice_confidence([0.25] * 4) == 0.0

A.12 Checklist

  • You can explain to a reviewer why a generated "confidence": 0.9 isn’t a probability
  • LLMs write; Jev/Kev decide; code computes. Each request goes to the right tool
  • Every architecture claim you rely on is labelled: published, contract, inferred or proposed
  • Big context in state, many independent questions per call; dependencies are a second stage
  • Option order fixed and permutation-tested for threshold-critical Choices
  • Option changes versioned as contract changes
  • Thresholds calibrated per backend and model version; full distribution logged
  • triage.probes run on your backend, region and version before go-live
  • Engines compared on the same scenarios with a rules baseline, including outage behaviour
  • Safety interlocks independent of every decision engine