~/satyajit

The middle tier is not in the middle

mdjsonmcp

2026-09-19 · 23 min · explainer · architecture · agents · llm · calibration

On September 18, Sydney Runkle of LangChain published Building a Harness with Jev: tool calling and structured outputs made LLMs safe for software, "but even with those in place, the agent loop is still slow and costly: every decision requires another model call." Her prescription is a division of labour — "use an LLM for open-ended reasoning and generation, and Jev for fast, structured decisions along the way."

The next morning 0xRicker published the same idea harder, as a discipline with a name. Jev Engineering: "An LLM creates the work → Jev decides what happens next → code executes the decision. That split is the whole discipline."

Read together they describe a three-tier stack, and the natural way to hear it is as a spectrum: deterministic scripts at one end, reasoning models at the other, decision models in between. Work slides down from the expensive end as it gets easy enough.

In fairness to both authors: neither actually writes the word. Runkle says "complement"; 0xRicker says "layer." The spectrum is the reading, not the text, and it is the reading I keep hearing repeated — which is why it is worth testing rather than the posts themselves.

I spent a day reading the systems people have actually built this way. The three-tier split is real and the evidence for it is better than either post claims. The spectrum is not. On the properties that make you choose one tier over another, the middle is not between the ends — on two of them it is below both, and that is the single most load-bearing fact about building with these models.

Their own rule is a type test, not a difficulty test

Notice what the sorting rule actually says. From step one of the Jev Engineering guide:

If the operation creates text, it stays with the LLM. If it picks, scores, or answers yes or no, it goes to Jev. An exact rule, such as stopping after ten actions, belongs in code, not in either model.

There is no difficulty in that test anywhere. Nothing about how hard the decision is, how much context it needs, how much judgment it takes. The test is entirely about the shape of the output: one value determined by the input, one of k known options, or an unbounded string.

That is a type signature, not a position on a line. And a type signature does not have a middle. () => 1, () => (0..k) and () => string are three different things, and the reason a decision model exists is not that it is a weaker reasoner — it is that it returns the second type natively, with a trained probability on it, which neither of the others does.

So: six properties people cite when they explain the split. Three tiers. Which of them actually rise from left to right?

six properties, three tiers — three lines rise, three do not
deterministic codedecision modelreasoning modeloutput cardinalitymonotonicexactly 1k, fixed (≤255)unboundedcost per callmonotonic0$0.000083$0.028latency per callmonotonicµs0.71 s3.23 srelational reachLOWEST in the middlesees everything0 / 100sees everythingexplanation offeredLOWEST in the middlethe code itselfone scalarprose (unverified)probability attachedHIGHEST in the middlenone — it is certainnative, trainedself-reportedCost and latency are measured per-call figures from the xArm7 run (Jev 1.13 vs GPT-6 Astra, lowreasoning effort). The other four rows are categorical; the heights order them, they do not scale.

Three do. Output cardinality, cost and latency all climb — those are the properties the spectrum reading is built on, and on those it is fine.

Three do not, and the three that do not are the ones you design against.

The axis where the middle is below both ends

An earlier piece here documented the sharpest number in this whole story: Jev scores 0 of 100 on relational choice — questions where the content of one option decides another — on a suite where the same model scores 100% on rule judgment and 98% on ARC-Challenge. Not low. Zero, across a hundred attempts, far below what you can reach by guessing.

That number has now been independently restated three more times by people who, as far as I can tell, have never seen it.

The Jev Engineering guide, explaining how to batch questions for speed, states it as an engineering rule:

The one rule: questions cannot read one another's answers. If a decision depends on a fresh search result, run the search first.

Stanley — a Jev-first coding agent I'll come back to — states it as a limitation of its own product, in the list of things its report tells you it did not check:

hunks are judged individually; intent spread across unlinked hunks is not modeled

And the xArm7 control harness states it by construction: it never asks the model for a movement, only for four separate signs. More on that below.

Put the axis on the picture and it is a V. Deterministic code sees every option at once — that is what a program is. A reasoning model sees every option at once, because they are all tokens in one context. The decision model, alone in the stack, evaluates each option in isolation and cannot form a relation between them.

A quantity that is at its minimum in the middle column is not "in between." On relational reach, the middle tier is not a compromise between code and reasoning; it is strictly worse than both, and no amount of making it cheaper or faster moves it.

What actually moved: Stanley, read line by line

stanley-code is the best evidence I found, because it is a whole agent built on the three-way split and it is small enough to read: 11,199 lines of source across 55 files, with 5,881 lines of tests. Its own summary of itself is the thesis in one sentence: "Each workflow is deterministic code that gathers bounded evidence and asks Jev small fixed-choice questions about it; code, not a model, makes the decisions."

I counted what is delegated. Across ten built-in workflows, Stanley asks Jev exactly 23 questions (22 distinct names; one is reused), every one of them a noul, choice or score over criteria fixed at author time. Not one of them is open-ended. Against that, its eight policy blocks hold 69 hard-coded constants — 41 bare probability thresholds, two more aliased to a shared one, 25 caps and a minimum count — and those constants, not the model, decide what every answer means.

Here is one request going through the router, drawn from the real source.

stanley-code · src/cli/router.ts — "Audit these changes for security vulnerabilities"
TIERSTAGEWHAT COMES BACKnatural-language request — no subcommand, no flagsCODEdeterministic factsdiff = present · input = none · safe untracked files countedCODEavailability gateeach workflow's own available({diff, input}) —a false value is a hard constraintMODELone Jev choicecriteria = the eligible ids + cannot_tellbudget: 2 requests · 16k input tokens · 30 sCODEacceptance policyconfidence ≥ 0.60 · p(pick) ≥ 0.55margin over the runner-up ≥ 0.15· find· check· review· test_gaps· summarize· security· performance· compatibility× triage_failures× triage_commentssecurity.62review.21check.09cannot_tell.05others (6).03illustrative vector; thresholds are realconfidence0.71 ≥ 0.60p(security)0.62 ≥ 0.55margin0.62 − 0.21 ≥ 0.15run `security` — or, if any test fails, cannot_telland a clarification instead of a guessthe model never runs a workflow;it only narrows what code may run.

The router is 160 lines and I am going to quote most of the part that matters, because the whole argument of this piece is in it. Repository at commit 85f39e7, src/cli/router.ts. First, what the model is allowed to answer:

// src/cli/router.ts — the option set is the registered workflows, and nothing else
const criteria: ChoiceCriteria = {};
for (const candidate of context.candidates) {
  const routing = dependencies.redaction.json(candidate.routing);
  criteria[candidate.id] = routing.value;
  redactions += routing.count;
}
criteria[CANNOT_TELL] = CANNOT_TELL_WHEN;
const labels = Object.keys(criteria);
 
const frame = createFrame({
  template: "route-intent@1",
  // …scope, provenance…
  state: { request: redacted.text, context: { /* diff, input, capabilities */ } },
  questions: { route: choice(ROUTING_INSTRUCTIONS, criteria) },
  parse(answers) {
    expectKeys(answers, ["route"]);
    return readChoice(answers, "route", labels);   // outside labels ⇒ throw
  },
});

One question. Its criteria are workflow ids the registry produced, plus the router's own cannot_tell. The request text is redacted before it is sent and the answer is parsed against the same labels array that built the question.

The shape is a sandwich, and it is worth being precise about which slice is which. Before the call, deterministic code computes facts (is there a diff? is stdin a failure log?) and each workflow's available gate turns those facts into hard constraints on what may even appear as an option. After the call, three fixed tests decide whether the answer counts: minConfidence: 0.6, minProbability: 0.55, minMargin: 0.15.

The minMargin test is the one I'd point at. It is not a confidence check — confidence is already checked separately. It is a check that the top two options are far enough apart, and it exists because a calibrated distribution is the only thing the model returns. The tier below has to read the shape of that distribution to recover what the tier above would have said in words.

Here is the whole of it — every line between the model returning and the router returning:

// src/cli/router.ts — everything that happens to the answer
const answer = result.value;
const selected = answer.choice;
const selectedProb = answer.probabilities[selected] ?? 0;
const alternatives = labels
  .filter((label) => label !== selected)
  .map((label) => answer.probabilities[label] ?? 0);
const margin = selectedProb - Math.max(...alternatives);
let outcome: string = selected;
let reason: RoutingDecision["reason"] = "selected";
if (selected === CANNOT_TELL) reason = "cannot_tell";
else if (context.capabilities[selected] !== true) {
  outcome = CANNOT_TELL;                                  // the override
  reason = "unavailable";
} else if (
  answer.confidence < ROUTING_POLICY.minConfidence ||     // 0.6
  selectedProb < ROUTING_POLICY.minProbability ||         // 0.55
  margin < ROUTING_POLICY.minMargin                       // 0.15
) {
  outcome = CANNOT_TELL;
  reason = "model_uncertain";
}

selected and outcome are separate fields, and only outcome is acted on. The three constants are declared right above with a comment that says what they are: "These are fixed product thresholds, not configuration." If the model picks a workflow that the deterministic gate already marked ineligible, the pick is discarded. The model's answer is an input to a decision that code makes. That is not a tier in the middle of a pipeline; it is a subroutine.

The same pattern runs all the way down. In the check workflow, nine regexes find skip markers (it.skip, @pytest.mark.xfail, #[ignore], t.Skip…), one finds assertions, and a whitespace-squash equality finds formatting-only hunks. Those fire before any model call and are reported as findings with source: "deterministic". Jev is then asked four bounded questions per source hunk — how the hunk relates to the task on a 4-level scale, which of seven kinds of change it is, and two yes-or-nos — and code turns those into flags at 0.7, 0.7, 0.5, 0.6. Jev never writes a review comment. The README says so as a design statement: "Jev does not write free-form review comments."

The one place work actually moves between tiers

Everything above is a static split. Stanley has the only mechanism I found where work migrates, and it moves in the direction the whole argument predicts — upward first, then permanently down.

When a request matches no workflow, Stanley delegates to Pi, a general coding agent, with a 600-second hard limit. That is the reasoning tier, used as a fallback rather than a default. It reports the agent's account of what it did and states in notChecked that Stanley did not verify it — the tier is used, and explicitly not trusted.

Then the interesting part. In the background, Stanley queues an improvement job and starts a detached worker that asks an agent to write a Stanley workflow for that kind of request — deterministic code plus judge() calls, in the documented contract. The candidate is validated by the same loader that loads real workflows, and the record of that validation is a five-field struct:

// src/workflows/improve.ts — what a promotable candidate has to survive
export interface CandidateChecks {
  /** The candidate directory contained exactly one loadable workflow. */
  readonly loaded: boolean;
  readonly quarantined: readonly string[];
  readonly duplicateId: boolean;
  /** Repository paths the agent changed outside its candidate directory. */
  readonly outsideWrites: readonly string[];
  /** Whether the router selected the candidate for the original request. */
  readonly routing: "selected" | "not_selected" | "skipped";
}
 
// src/adapters/improvements.ts — the verdict
status: reasons.length === 0 ? "validated" : "rejected",

Then it stops. Nothing activates — the module comment says so in those words: "Nothing is activated automatically: promoteCandidate moves a validated candidate into .stanley/workflows/ on request." A human runs --promote-candidate, and from then on "that request kind is handled by the promoted workflow: deterministic code plus judge, no agent."

That is the three-tier thesis as a running loop: a reasoning model does a job once, writes the deterministic-plus-decision version of itself, and a person decides whether it graduates. The guardrails around it are the tell — agent-written files that appear under .stanley/workflows/ are moved to .stanley/quarantine/, every agent subprocess runs with STANLEY_NESTED=1 so it cannot start another delegation, and the queue holds 20 jobs with at most two attempts each. The system treats its own reasoning tier as the untrusted one.

Worth noting what this is not: Stanley is at 0.1.0, unreleased, and its README says so twice. I am describing a mechanism I read, not a result I measured.

The workaround is the architecture

If the decision tier genuinely cannot relate two things, how does any of this work? Diffs are relational. Robot arms are relational.

Both systems solve it the same way, and once you see it once you see it everywhere.

two systems, one workaround — code builds the relation, the model scores one thing at a time
stanley-code · check"hunks are judged individually" — its own notChecked listhunk Ahunk Bhunk Chunk Dhunk ECODE, not themodel: A declaresan id that Dreferences. Oneregex, no call.one frame, two hunkscandidateHunk: AdependentHunk: Dnoul: does A addsomething D needs?p ≥ 0.60 → clear flagThe pair never competes as two options in one question.It arrives as one state with a yes-or-no about it — theone shape the model can answer.Rationed, too: only a weak hunk linked to a strongly-related one gets a follow-up, and it gets exactly one.jev-robot-control · one control cyclethe vector the model is never asked for"pick a move" — 3×3×3×3= 81 joint optionsasked instead, in parallel, none reading another's answer:question 1: Xneg · hold · posquestion 2: Yneg · hold · posquestion 3: Zneg · hold · posquestion 4: gripopen · hold · closeshared executorCODE supplies:step 18 / 4 / 2 mminverse kinematicsworkspace bounds0.32 s physics stepMODEL supplies:four signs.The model chooses direction. Code chooses distance.Every relation between the axes lives in the executor.Same move, twice: a relation the decision tier cannot form is built by the tier below it,then handed down as an independent question. The workaround is the architecture.

Stanley wants to know whether a hunk that looks unrelated to the task is actually there because another hunk needs it. It cannot ask that as a relational question. So enabledHunks does it deterministically: regex out the identifiers each hunk declares on added lines, regex out the identifiers each hunk references, intersect. src/workflows/hunks.ts, same commit:

// src/workflows/hunks.ts — the relation, built by regex, with no model involved
const DECLARATION =
  /\b(?:function|class|interface|type|enum|const|let|var|def|fn|func|struct|trait|module)\s+\*?\s*([A-Za-z_$][\w$]{2,})/g;
// declaredIdentifiers(hunk): DECLARATION matches on "+" lines only.
// referencedIdentifiers(hunk): every identifier on "+" and context lines.
 
/** For each hunk, the other hunks that reference identifiers it declares. */
export function enabledHunks(hunks: readonly Hunk[]): Map<string, string[]> {
  const references = new Map(hunks.map((h) => [h.id, referencedIdentifiers(h)]));
  const result = new Map<string, string[]>();
  for (const hunk of hunks) {
    const declared = declaredIdentifiers(hunk);
    const linked: string[] = [];
    if (declared.size > 0) {
      for (const other of hunks) {
        if (other.id === hunk.id) continue;
        const refs = references.get(other.id)!;
        if ([...declared].some((name) => refs.has(name))) linked.push(other.id);
      }
    }
    result.set(hunk.id, linked);
  }
  return result;
}

That produces a candidate pair — built by code, from a regex, with no model involved. Only then does Stanley open a frame containing exactly two hunks and ask one yes-or-no: does A add something D uses and needs?

The budget on that is the instructive part, and it is one method call:

// src/workflows/check-task.ts — a weak hunk that enables a strong one gets ONE question
const followUps: Array<{ weakId: string; dependentId: string }> = [];
for (const id of weak) {
  const dependent = (links.get(id) ?? []).find((other) => {
    const otherResult = results.get(other);
    return (otherResult?.taskRelation?.highMass ?? 0)
      >= CHECK_TASK_POLICY.enablerMinHighMass;                       // 0.5
  });
  if (dependent) followUps.push({ weakId: id, dependentId: dependent });
}
// …one enablesFrame per pair, judged, then:
if (outcome.value >= CHECK_TASK_POLICY.enables) {                    // 0.6
  result.flags = result.flags.filter((f) => f !== "weak_task_relation");
  result.flags.push("enables_linked_hunk");
}

.find, not .filter: only a hunk already flagged weak, already linked to a hunk already scoring well, gets a follow-up — and it gets exactly one, against the first match. The relational capability is manufactured by code, one pair at a time, and rationed.

The xArm7 harness does the identical thing in a domain where it is impossible to miss. A movement is a joint choice: X, Y, Z and the gripper together, 81 combinations, and the right value on one axis depends on the others. The harness never asks for it. Each control cycle is two requests — one choice over eight intents, then one request carrying four independent choice questions, one per channel, each with three options. The model returns four signs. incremental_policy.py builds those four questions in a loop and writes the constraint into each one's instruction text: "This question controls ONLY X … Use ONLY this axis for this answer; do not answer for another axis."

The executor supplies everything continuous. At commit 7a4ed8b, incremental_env.py:

# incremental_env.py — the model's whole contribution is the sign
direction = {'negative': -1., 'hold': 0., 'positive': 1.}
# …
# Magnitude-only scaling near geometric targets. Never changes the selected sign.
reference = self.fruit_position + [0, 0, .002]
if intent in ('carry', 'lower', 'release'):
    reference = np.r_[PLATE[:2], RELEASE_Z if intent != 'carry' else TRAVEL_Z]
if intent in ('lift', 'withdraw'): reference = np.r_[current[:2], TRAVEL_Z]
distances = np.abs(reference - current)
sizes = np.where(distances < .012, .002, np.where(distances < .025, .004, .018))
delta = np.array([direction[motor[a]] for a in ('x', 'y', 'z')]) * sizes
target = current + delta
if np.any(target < [.20, -.30, .025]) or np.any(target > [.65, .38, .32]):
    rejection = 'Workspace bound: requested increment rejected.'

Three signs in; a 2, 4 or 18 mm increment, a workspace check, fixed-orientation inverse kinematics and a 0.32-second physics step out. The comment above reference is the design statement: the executor scales magnitude and "never changes the selected sign."

The model chooses direction. Code chooses distance. Every relation between the axes lives in the executor's geometry, where it can be read, tested and stepped through.

A three-column comparison of one apple-to-plate task. Jev 1.13, GPT-6 Astra and GPT-4.1 mini each show a final simulation frame, the final tool-centre-point coordinates, the chosen intent with a probability, and four separate columns for X, Y, Z and fingers, each with its own probability. Jev's are 98, 96, 93 and 86 percent and labelled native Jev probabilities; both GPT columns read 100 percent across the board and are labelled self-reported JSON probabilities. A sidebar gives cumulative cost of $0.0188, $5.9336 and $0.2885, decision cycles of 113, 106 and 160, and wall times of 181.8, 707.3 and 704.3 seconds.
The decomposition, drawn by the source itself: X, Y, Z and FINGERS are four separate columns with four separate probabilities, because they were four separate questions. Note the probability rows — Jev's are native model outputs; both GPT columns are self-reported and read 100% four times in a row. (openroboto-ai/jev-robot-control, media/final.png.)

A number is not a reason, and somebody has to write the reason

The second non-monotonic axis is the one I underrated before reading this code.

Deterministic code explains itself completely: the regex is the explanation, and you can read it. A reasoning model explains itself fluently, in prose, and that prose is legible even though it is famously not guaranteed to be the actual cause. The decision model returns a probability vector. That is all. It is the most faithful output of the three — there is no gap between the number and what the model did — and simultaneously the least explanatory, because a number does not say why.

So the audit trail has to be built somewhere else, and in Stanley you can watch it being built. Every finding carries source: "deterministic" | "jev" | "policy", so you can tell which tier produced it. Every frame carries provenance — the file and line range the evidence came from. Every report carries coverage, how much was actually examined; parked, the items it could not decide; limits, where a cap truncated the work; and notChecked, a plain-English list of what it did not do. The README's sharpest line is about exactly this: "An empty findings list is not an approval."

None of that is decision-model output. All of it is deterministic bookkeeping wrapped around decision-model output, and it is a large fraction of why Stanley is 11,000 lines rather than 2,000. The honest version of the three-tier claim has to include this: moving a decision down a tier does not move its explanation down with it. You inherit the obligation to manufacture one.

That is a cost, and it is not on anyone's comparison table.

The multipliers, measured

TypeSafe's homepage headline is "up to 193.6x faster, up to 444.6x cheaper." Runkle's post relays it as "up to 200x faster inference and 400x lower cost." This site itemized those claims when they were made — self-graded workflow evals, raw data unreleased, and TypeSafe's own docs cookbook showing 20x–114x speed against real LLM prices — so I won't re-argue it.

What is new is that there is now a matched head-to-head someone else can replay. The xArm7 repository publishes both arms of a seed-0 trial with per-call costs and per-call model wait times, an offline verifier that re-executes every recorded motor command in MuJoCo, and max_qpos_error = 0.0. Nobody in this ecosystem has put those numbers next to the headline. Here they are.

receiptscaptured 2026-09-19

TypeSafe's homepage headline is 'up to 193.6x faster, up to 444.6x cheaper.' The xArm7 repository is the only place in this ecosystem that publishes a matched, independently replayable head-to-head with per-call costs and per-call model wait times for both sides. Read against it, the cost multiplier is roughly the right order of magnitude and the speed multiplier is not: the measured per-call latency ratio is 4.57x, about 42 times smaller than the headline.

quantityJev 1.13GPT-6 Astra (low)ratio
model API calls2262120.94xmeasured
total model cost$0.018825$5.933624315.2xmeasured
cost per call$0.0000833$0.027989336.0xmeasured
total model wait159.81 s685.75 s4.29xmeasured
latency per call0.7071 s3.2347 s4.57xmeasured
wall time, whole task181.85 s707.27 s3.89xmeasured
control cycles to place the apple1131060.94xmeasured
headline cost multiplier444.6xclaimed
headline speed multiplier193.6xclaimed
LLM latency 193.6x would require (vs Jev at 0.707 s, xArm7)0.7071 s136.9 s193.6ximplied
LLM latency 193.6x would require (vs Jev at 178 ms, browser)0.178 s34.5 s193.6ximplied

This is ONE seed-0 trial per controller, which the repository states plainly and which is the reason it is evidence about magnitudes rather than a success rate. Both sides are remote HTTP endpoints over OpenRouter, so both latencies include a network round trip; that inflates Jev's absolute number and therefore makes the 4.57x ratio a conservative floor for Jev, not a ceiling. GPT-6 Astra ran at low reasoning effort with 4,096 max completion tokens — a long-reasoning-trace configuration would move the latency ratio up and is the only regime in which a three-figure speed multiplier is reachable.

method Every figure in the 'measured' rows is read directly from verification/gpt6-comparison.json in openroboto-ai/jev-robot-control, pair 20260919-200239-715974-0, and divided. cost_usd and model_wait_seconds are per-run totals; api_calls is the per-run count of model requests. Per-call figures are total divided by api_calls. The repo's own offline verifier re-executes every recorded motor command in MuJoCo and reports max_qpos_error = 0.0 and cost_sum_verified = true for both arms. The last two rows invert the headline multiplier: they are the per-call LLM latency that would be required to reach 193.6x against Jev's measured latency in each of the two published harnesses.
data /articles/three-tiers/data/ratios.json (11 rows, 3.6 KB)

The cost multiplier broadly survives: 336x per call measured, against 444.6x claimed — same order, and the gap is the kind of thing task mix explains.

The speed multiplier does not. 4.57x per call measured, against 193.6x claimed: off by a factor of 42.

Invert it and you can see what the headline would require. At Jev's measured 0.707 s per call in this harness, a 193.6x ratio needs the LLM side to spend 136.9 seconds on every call. Take Jev's best published latency instead — the 178 ms median from the browser harness — and it still needs 34.5 seconds per call. Neither is a classification call. Both are a model writing a long reasoning trace.

Which is the quiet joke in the whole comparison: the three-figure speed multiplier is only reachable against a baseline that is doing the thing the entire argument says you should stop doing. Against an LLM asked the same bounded question at low reasoning effort, the honest number is single digits.

Two more things in that table deserve saying out loud. GPT-4.1 mini — a cheap model, the one a spectrum reading would place just above the decision tier — spent 15.3x Jev's total cost and failed the task, exhausting the 160-cycle budget with one grasp attempt behind it and the apple still off the plate. And GPT-6 Astra placed the apple in seven fewer cycles than Jev. Per decision, the frontier model was better. It cost 336x more per decision to be better, and the cheaper LLM was not better at all. There is no single line these three sit on.

What did not move, and one roster entry that does not exist

jev-recruiter is circulating with the claim that decision models kill all prospecting and sourcing work. The repository is a genuinely clean instance of the pattern — Jev screens titles, picks an operation and a target, and chooses a status plus an indexed excerpt per criterion, while code resolves the quotation and validates that it exists in the observed text, and "model output never becomes a selector or executable code."

It also publishes no recruiting numbers at all. No precision, no recall, no N, no denominator. And the README is the thing that told me so, in a paragraph its author had every incentive to leave out:

The inherited Flights examples and measurements describe the upstream agent, not recruiting performance.

It goes further. A potential_match "is not an independently verified qualification or a hiring decision"; "a quotation can be real while the model's interpretation is wrong, especially when adding years across multiple jobs"; the interface "samples at most seven visible screens per profile"; "a completed run does not prove every candidate meets the brief." That is a project refusing to let its own artifact be read as a benchmark. Credit it — and then notice that the distance between that README and "kills all prospecting work" is the entire distance between an architecture and a result.

One correction to the roster I was given. dimensionalOS/dimos is cited as evidence for this architecture. At commit c1c3cdc the string jev does not appear in it, nor typesafe, nor systemone. It is a robotics operating system with agent modules; it has no decision-model integration. It belongs in a different article.

So what is the honest picture?

Three tiers, yes. The decomposition is real, three independent codebases and two independent write-ups converged on it, and the sorting rule the practitioners actually use — does this operation create text, pick from a list, or follow an exact rule — is a good rule.

But it is a type distinction, not a spectrum, and three of its six defining properties are not monotonic. The decision tier is the only one that cannot see across its own options, the only one that cannot explain itself, and the only one that comes with a trained probability. You do not reach for it because a task got easy enough to demote. You reach for it when the output is genuinely one of k known things and you genuinely want a number attached — and you accept, in exchange, that you will write the code that builds every relation it needs and the code that manufactures every explanation it owes.

Stanley's 64 constants and the executor's millimetres are not scaffolding around the interesting part. In a three-tier system they are the interesting part. The decision model is the smallest piece of code in every repository I read, and that is the claim worth making: not that decisions moved to a cheaper model, but that making a decision cheap enough to call constantly pushes the hard work back down into deterministic code, where — for once — you can read it.

What would change my mind

5 claims above, and what would falsify each

  1. The three-tier split is a type distinction, not a spectrum, because three of its six properties are non-monotonic.

    The whole frame collapses if the relational gap is a training artefact rather than a structural one. Ship a System One model whose options share one context — a single forward pass that scores all k options jointly and still returns a calibrated distribution — and relational reach stops being a V. Bespoke's contrastive-pair approach is the nearest thing to an attempt. If such a model matches Jev on latency and cost while solving Chopra's relational-choice set, the middle tier really is just "cheap reasoning" and the spectrum reading is the right one.

  2. Stanley delegates only bounded questions, and deterministic code makes every decision.

    I counted 23 question definitions and 64 policy constants by reading the source, not by running it. Run stanley against a real repository with --json and diff what the model returned against what the report says. If any finding's severity or text varies with something other than a threshold applied to a returned probability — if a Jev answer reaches the output unmediated — the "code, not a model, decides" claim is weaker than the README and I have over-read it.

  3. The measured per-call latency ratio is 4.57x, against a 193.6x headline.

    This is one trial, both arms remote, GPT-6 at low reasoning effort with a 4,096-token cap. Run the same harness against a locally-served LLM and a locally-served decision model, removing the network round trip from both sides, and the ratio will move — possibly a lot, since Jev's 0.707 s is almost certainly dominated by transport. If a like-for-like local comparison on the same bounded question lands anywhere near three figures, my arithmetic is measuring OpenRouter rather than the models.

  4. Moving a decision down a tier means writing the explanation yourself.

    Straightforwardly falsified by a decision model that emits an attribution alongside its probability — the evidence span it scored highest, an influence map over the input state, anything faithful and machine-checkable. Nothing in the architecture forbids it; per-option scoring arguably makes it easier than it is for an LLM. If TypeSafe or an open reimplementation ships one, the provenance / coverage / notChecked bookkeeping in Stanley becomes redundant rather than load-bearing, and the cost I'm describing disappears.

  5. Jev Engineering's 'questions cannot read one another's answers' describes the same constraint as the 0/100 relational-choice score.

    These could be two different things: one a batching rule about parallel questions within a request, the other a failure within a single question's option set. If a probe shows Jev handling cross-option reference fine inside one question while only failing across questions in a batch, then the guide's rule is about request parallelism and the benchmark result needs a different explanation — and my claim that four sources describe one constraint is an over-reading of three of them.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "The middle tier is not in the middle", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026threetiers,
  author = {Satyajit Ghana},
  title  = {The middle tier is not in the middle},
  url    = {https://ai.thesatyajit.com/articles/three-tiers},
  year   = {2026}
}
share