~/satyajit

Five harnesses for a model that cannot write

mdjsonmcp

2026-09-22 · 34 min · explainer · architecture · agents · llm · systems

Three days ago I wrote that the middle tier is not in the middle: a decision model is not a cheap reasoning model, it is a different kind of component, and the interesting engineering is the deterministic code that feeds it. That was an argument from three codebases. Five more landed this week, four of them younger than the argument, and together they are a better instrument than the argument was, because their authors did not read each other.

I cloned all five and read the code. Every one of them has to build four things: an enumerator that turns a situation into a finite option list, a question format, a confidence gate, and a composer for the strings the model cannot emit. Four decisions, five answers each. Where they agree is the shape of the pattern. Where they disagree is where the design is still open.

four design decisions · five independent answers to each

enumerator

what turns a situation into a finite option list?

SystemOneHarness

declared, or compiled

YAML action space, MCP tool schemas, Browser Use's DOM index, a 4-bit canvas quantiser

webctl

search + LSH

3 engines fused by reciprocal rank, MinHash over 3-word shingles, a 2,000-char chunker

fastbrowse

page capture, then Jev

capture.js indexes ≤320 controls; past 160 a noul per control shortlists them

djev

none — it is the server

the caller enumerates; for a span the enumerator is the text's own token ids

cadence

a callback

actions(S) → Sequence[A], supplied by the application

four of five contain no model at all

question format

what shape does one question take, and how many options fit?

SystemOneHarness

choice / noul / score

≤255 options; over that, truncate and record the drop in the trace

webctl

choice / noul / score

score over a 4-level rubric by default; a custom rubric is a CLI flag

fastbrowse

choice / noul / score

≤240 for headroom; over that, groups of 30 and a second call

djev

choice / noul / score / span

≤26 options — the labels are A…Z, one token each

cadence

a float

evaluate(S) → float; no language model anywhere

everyone took TypeSafe's three primitives; the ceilings differ by 10x

confidence gate

what happens when the answer is not confident enough?

SystemOneHarness

refuse

per-risk thresholds on the weakest judgment; 3 refusals in a row ends the run with a handoff packet

webctl

drop

one cut derived from the rubric size; the result never reaches the agent's context

fastbrowse

hand it to the LLM

under 0.55 the step goes to LLM recovery; 11 named thresholds, each with a calibration note

djev

look again

first-read entropy over 0.1 buys three more noise draws, averaged, with a standard error

cadence

spend more nodes

the budget is visited nodes per tick; no probability is involved

five different answers — this is where the projects actually disagree

composer

who writes the strings a decision model cannot emit?

SystemOneHarness

nobody — the operator

a parameter needing free text fails to compile; text is supplied by name before the run

webctl

nobody by default

--summarize adds a small general model over the kept chunks

fastbrowse

an LLM, then code

the reader cites block ids; code slices the quote and builds the #:~:text= link

djev

the model, bounded

a span returns character offsets, so the string is a substring by construction

cadence

n/a

the output is an action from the supplied set and a predicted cost

the only axis where a second model is the common answer

A shaded cell is the minority answer on its row. Read the rows, not the columns: the enumerator row is nearly unshaded because four of five enumerators contain no model, and the gate row is entirely shaded because no two projects do the same thing below the threshold. cadence is in the table because it has the same four parts, not because it is a decision-model harness — it is not.

First, which of these are real

Four of the five did not exist a week ago, so the honest thing is to say which ones I would deploy and which are sketches. This matters for reading the rest: a convention three repositories share means something different if two of them are two commits deep.

first commitcodetestsCIpublished measurement
fastbrowse2026-09-179,269 lines Python401yes14 tasks × 3 passes against Browser Use, per-run data committed
webctl2026-09-2019,600 lines Go226yes30 questions × 7 arms, blind judge, cells.jsonl per cell
SystemOneHarness2026-09-194,485 lines Python50no15 live runs on its own toy environment
djev2026-09-211,526 lines Python0noone table on an unmerged branch
cadence2026-09-07254 commits, NumPy503yesnot a decision-model project

fastbrowse and webctl are real systems. Both ship binaries, both have CI, both publish per-run evidence rather than a headline. fastbrowse is on PyPI and its README labels itself pre-alpha, which given 401 tests and 189 commits in six days is the right amount of modesty. webctl is in Homebrew and apt.

agent-labs-dev/fastbrowse@3588580 · snapshot 2026-09-22
tracked files
151
license
MIT
branch
main
tests
56 files
source
1.5 MB
commit date
2026-09-22
source by language
Python1.0 MB(92)JavaScript473.5 kB(3)HTML26.1 kB(25)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-22 at 3588580 branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

dorkitude/webctl@e9bc54a · snapshot 2026-09-22
tracked files
203
license
MIT
branch
main
tests
29 files
source
626.8 kB
commit date
2026-09-22
source by language
Go622.2 kB(91)JavaScript2.6 kB(2)Shell2.0 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-22 at e9bc54a branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

SystemOneHarness is a real design with a thin benchmark. The code is careful — the loop, the compiler, the gate and the encoder are each one tight file, and docs/design.md is 551 lines of measured reasoning rather than marketing. But pytest runs against recorded model answers, there is no CI at all, and the benchmark is five runs each of three scenarios against an order-fulfilment state machine the project wrote itself. It proves the loop executes. It does not prove the loop is good. The README says exactly that: "The benchmark proves the controller, compiler, gate, and model can complete these small deterministic tasks. It does not claim the same result for ambiguous state, arithmetic, dates, or long irrelevant context."

HarnessRouter/SystemOneHarness@ab8e8f0 · snapshot 2026-09-22
tracked files
54
license
Apache-2.0
branch
main
tests
10 files
source
247.4 kB
commit date
2026-09-21
source by language
Python237.7 kB(31)JavaScript6.6 kB(2)CSS1.6 kB(1)HTML1.4 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-22 at ab8e8f0 branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

djev is a sketch, and a good one. Two commits, one file, no unit tests. What it has instead is a live battery you point at a running server, and the thing it demonstrates is worth more than tests would be.

mmastrac/djev@442eab6 · snapshot 2026-09-22
tracked files
4
license
Apache-2.0
branch
main
tests
none found
source
55.8 kB
commit date
2026-09-21
source by language
Python55.8 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-22 at 442eab6 branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile

cadence is not a decision-model harness. I looked for one: there is no jev, no typesafe, no systemone, no language model anywhere in 254 commits. It is a NumPy research library about equilibrium world models, with a bundled Lean development of 169 theorems. Somebody put it on a list of decision-model projects and it does not belong there. It stays in this piece for one reason, which I will come back to at the end.

muellerberndt/cadence@ffe0ba0 · snapshot 2026-09-22
tracked files
198
license
MIT
branch
main
tests
79 files
source
1.2 MB
commit date
2026-09-22
source by language
Python1.2 MB(112)JavaScript51.1 kB(1)HTML6.2 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

The repository's own language breakdown and topics are the quickest check on the claim: this is a NumPy research library, not a harness.

local clone, 2026-09-22 at ffe0ba0 branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, testFileCount

The enumerator, and why it is almost never a model

A decision model picks from a list. Something has to build the list, and that something is the part nobody advertises.

On jev-linkmap I said the best thing in the project was its candidate generator: TF-IDF cosine with headings weighted 4x, a chrome filter dropping any page half the site already links to, anchors restricted to text the author actually wrote. No model touches any of it, and it is what makes the model's job small enough to be a yes/no question. Five more repositories later, that generalises harder than I expected.

webctl's enumerator is three search engines and a hash function. Up to three providers are queried, their rankings fused by reciprocal rank, exact duplicates collapsed on a normalised URL. Then near-duplicates are proposed by MinHash LSH over three-word shingles — 64 hashes in 16 bands of 4, with the band arithmetic documented in a comment that tells you what collision probability that buys:

// internal/dedupe/dedupe.go
// 64 hashes in 16 bands of 4 rows makes a pair with Jaccard similarity 0.5
// land in a shared bucket with probability ~0.65 and one at 0.8 with ~0.99,
// while pairs at 0.2 collide about 1% of the time.

Jev never proposes a pair. It is handed the few pairs that collided and asked, one noul each, whether they are the same document. The page chunker is the same shape: fixed 2,000-character chunks with 20% overlap shown as context, and Jev answers one yes/no per chunk. Every option Jev ever sees was produced by code with no model in it.

fastbrowse's enumerator is a page capture, capture.js and snapshot.js running in the tab, indexing interactive controls with role and name. That is also model-free — until three days ago, when it stopped being. Commit 3588580 says why:

A page denser than 160 controls was cut in document order, so a result or filter drawn after a long header and sidebar never reached Jev's choice. The browser now indexes up to 320 controls (120 offscreen), and past 160 the policy asks Jev one Noul per control, batched under the token budget and sent together, then offers the highest-scoring controls that fit.

This is the one place in five repositories where a model entered the enumerator, and the reason is worth reading twice: a DOM-order cut dropped the control a task needed. The fix was not a better heuristic, it was a shortlisting pass. And note the primitive chosen — one noul per control, not one choice over all 240. The commit explains: "its two-decimal probabilities leave all but a handful of 240 options tied at zero." A choice over 240 options quantises to nothing useful. A yes/no per option does not.

SystemOneHarness's enumerator is a compiler, and the striking thing about it is what it refuses. Every parameter must be one of four declared kinds, and a fifth — free text — fails at load time:

# systemone_harness/actions.py
if p.kind == "free":
    raise ActionSpaceError(
        f"parameter {name!r} would need free text: declare `choices`, `from`, "
        "`flag` or `levels`. A System One model chooses; it does not write.")

That single raise is the most consequential line in the repository, and I will come back to it, because it is what buys the headline.

djev has no enumerator at all, because it is a server. The caller enumerates. Its one exception is the most interesting enumerator in the set, and it is also model-free: for a span question the option list is the token ids of the state's own text, so the answer is a substring by construction.

So: four of five put no model in the enumerator, and the fifth added one only after a truncation lost the answer. That is as close to consensus as five independent codebases get.

The enumerator is also where it fails, measured

SystemOneHarness ships something I have not seen anyone else publish: a probe that measures its own enumerator failing.

The browser environment reads a page's DOM. A page drawn on a <canvas> has no DOM to read, so the project wrote a quantiser — draw the canvas onto an offscreen canvas of cols × rows, read the pixels back, quantise each to 4 bits a channel, keep the K commonest colours, one character per cell. Then it asked Jev six questions about the resulting ASCII, over 30 frames of Super Mario Bros., at four resolutions, each plain and with the operator's colour names attached. Eight encodings, 48 measured cells, and the raw rows are in docs/reports/canvas-probe-2026-09-19.json.

canvas probe · 8 encodings × 6 questions × 30 frames · jev 1.13 · 2026-09-19
enemyaheadbase 1.00gapaheadbase 0.97obstacleaheadbase 1.00ongroundbase 1.00playerwherebase 1.00nextcontrolbase 0.97encodingtokens32x16 plain10380.030.970.001.001.000.0332x16 named11200.000.030.000.000.970.0364x32 plain15370.500.970.830.001.000.0364x32 named16860.000.430.000.000.630.0396x48 plain21970.900.970.331.001.000.0396x48 named23550.170.971.000.001.000.50128x64 plain32770.700.970.000.900.000.27128x64 named34420.200.030.970.001.000.4013 of 48 cells reach the majority-class baseline. None of them is in the boxed column.`base` is what you score by ignoring the state and always answering the commonest label. Thirtyframes held 0 enemies, 1 gap and 29 “run right”, so the baselines are high and the bar is honest.

Read the base row first. The frames were lopsided — 0 enemies in 30 frames, 1 gap, 29 of 30 with "run right" as the correct control — so always answering the commonest label scores 0.97 to 1.00 on every question. That is the bar.

Thirteen of 48 cells clear it, and none of them is in the column that matters. next_control is the only question the loop actually needs: which key do I hold now. Its best score across every encoding is 0.50 at 96x48 named, against a baseline of 0.97. Quadrupling the resolution from 32x16 to 128x64 more than triples the token cost, from 1,038 to 3,442, and moves next_control from 0.03 to 0.27 — still a quarter of what you get by not looking. Attaching colour names helps on some questions and destroys others: on_ground is 1.00 plain and 0.00 named at the same resolution.

None of that is a fact about Jev. It is a fact about the encoding. The design doc says so in plain words: "a page drawn on a canvas gives a DOM observation nothing but its DOM… Driving such a page well needs the page's own state as text, which is a page-side adapter."

Which is exactly what the project's headline demo is:

Jev playing a live browser game through System One Harness — one typed decision per step, no generated control text. The project's own recording at ab8e8f0, transcoded; terms in /articles/system-one-harnesses/NOTICE.txt. Note the caption under the game: the environment states where Mario is and what is ahead in tiles, as plain sentences. That is a page-side adapter, not the DOM environment and not the canvas quantiser the probe above measures.

The demo is real and the loop in it is real. But the thing making it work is the sentence generator on the page side, not the model and not the general browser environment. Which is the finding: the enumerator is the product. When it is good, a 1.13-generation decision model plays a platformer in real time. When it is a pixel quantiser, the same model cannot beat a constant.

The question format: everyone took the same three, then disagreed about the ceiling

All four decision-model projects use TypeSafe's primitives, because TypeSafe's wire defines them: choice (pick one of N named options, get a probability per option and a confidence), noul (yes or no, get a probability), score (a level on an ordered scale). There is no fourth, and there is no multi-select — a set is composed from one noul per member.

The agreement stops at the ceiling.

options per choicelevels per scoreover the ceiling
SystemOneHarness255unboundedtruncate to 255, record the drop in the trace
fastbrowse24010groups of 30, then a second call inside the chosen group
webctln/a (one item per question)4 by defaultpack questions by token budget, halve a rejected batch
djev2626split the answer template into chunk rows

djev's 26 is the one that tells you something. Its labels are A through Z, one token each, because the answer is read out of a single canvas position:

# structured_server.py
elif kind == "choice":
    opts = q.get("options") or []
    labels = [chr(ord("A") + i) for i in range(len(choices))]
...
if len(choices) > 26:
    raise SchemaError(f"question {qid!r}: at most 26 alternatives")

Jev's 255 is a serving limit somebody chose. djev's 26 is the alphabet. The option names never enter the readout at all; they appear in the system prompt, mapped to letters, and the model's answer is a distribution over those letters.

The other ceiling difference is which end you protect. SystemOneHarness truncates the option list and writes the truncation into the trace, which is honest and lossy. fastbrowse refuses to lose an option: past 240 it asks a choice over groups of 30, then a second choice inside the winner, and multiplies the two confidences, "because the element was only ever chosen from inside the group, so a doubtful group is a doubtful target." That is a correct treatment and it costs a round trip.

One quieter disagreement, and I think it is the deepest one in this section: webctl scores one result per request by default. Batching every result into one call is --batch, opt-in. Look at the state each mode sends and the reason is obvious. In per-result mode the state is {query, goal, result} — the model cannot be influenced by result 7, because result 7 is not in the request. In batch mode every result is in the state and the prompt says "judge only that one result", which is an instruction, not a guarantee.

SystemOneHarness batches everything into one call and is right to: its questions all read one observation, so sharing the state is not contamination, it is the point. webctl's questions are about different documents, and sharing the state is a leak. Whether to batch is not a performance decision. It is a question about whether your questions share a subject.

The confidence gate: five answers, no two alike

Every project reads the same probability and then does something completely different with it.

19 named thresholds · 3 codebases · one axis
0.300.500.700.90SystemOneHarnesssystemone_harness/actions.py · DEFAULT_GATEread / finish0.50write0.70destructive0.90one threshold per risk class, compared against the weakest judgment the action depends onwebctlinternal/jev/qualify.go · DefaultCutbackfill floor0.33--noul default0.50keep0.60dedupe, no model0.70one cut, derived from the rubric's size: 1.2 levels below the top, so a 4-level rubric gives 6.0 of 10. The last mark is a Jaccard estimate, not a confidencefastbrowsesrc/fastbrowse/config.py · Thresholdsrequirement / rewrite0.30irreversible / value stated0.50recover0.55login / bot / claim0.70done accept0.85sensitive act0.90eleven, each with the run that calibrated it in the docstring beside itdjev is not on this axis. Its gate is an entropy in nats over the first read, and clearing itbuys three more noise draws rather than permission to act.

Nineteen named thresholds across three codebases, chosen independently for unrelated jobs, and almost all of them land on 0.30, 0.50, 0.70 or 0.90. That is either convergence or round numbers, and I think it is round numbers. The one project that derives its threshold rather than picking it is webctl:

// internal/jev/qualify.go
// DefaultCut is the default keep threshold for a rubric with levels
// labels: 1.2 levels below the top, on the 0–ScaleMax scale. For the
// built-in four-level rubric that is 6.0 ("useful" or better).
func DefaultCut(levels int) float64 {
	top := float64(levels - 1)
	return (top - 1.2) / top * ScaleMax
}

That is a rule, not a constant: change the rubric's size and the cut moves with it. It is the only threshold in the five that survives a rubric rewrite.

What happens below the threshold is where the disagreement is total.

SystemOneHarness refuses. The gate compares the weakest judgment the action depends on — not the product; "the question is 'is there one shaky judgment'" — against a threshold picked by the action's risk class. Below it, nothing runs, the step is recorded as refused, and three refusals in a row end the run. The run record then carries a handoff: the state it refused on, the questions, its answers with their probabilities, the weakest judgment and the threshold it missed. That is the best artefact in the repository. It turns "the model was unsure" into a packet a reasoning model or a person can pick up.

webctl drops. A result under the cut never reaches the agent's context, and that is the whole product. Note what it does not gate on: Jev returns a confidence alongside every score, webctl prints it under --verbose, and no code path anywhere reads it for a decision. The keep rule is q.Value() >= minScore on the score itself. For a filter that is defensible — a confidently mediocre page and a doubtfully mediocre page are both pages you did not want — but it means the calibrated quantity the model is famous for is decoration here.

fastbrowse escalates. Under recover_below, 0.55, the step does not stop and does not run: it goes to LLM recovery, which looks at the page and directs the next action itself. The gate is a routing decision between two models, not a stop. And its thresholds are the best-documented numbers in any of these repositories, because each carries the run that set it:

# src/fastbrowse/config.py
done_accept_from: float = 0.85
"""With every action requirement confirmed, the holistic done answer that accepts
without the LLM verifier. Jev scores a page that is right 0.49 to 0.91 as done and
a near miss (the organisation instead of the repository, search results, a related
article) 0.04 or less, so doubt between the two is not a near miss."""

That docstring is how a threshold should be justified: here is the distribution of the right answers, here is the distribution of the near misses, here is the gap I put the number in.

djev resamples. Its gate is not a probability at all. It takes one read, computes the entropy at each answer slot, and if the largest exceeds auto_threshold — 0.1 nats, by default — it takes three more reads with different noise seeds and averages them. Clearing the gate buys another look, not permission to act. It is also the only one of the five that reports uncertainty about its uncertainty: with more than one read it returns a stderr over the top probability and an agreement fraction across draws.

cadence's gate is a node budget. No probability is involved anywhere.

Are they tunable without a re-run?

This is the question jev-linkmap made me care about, where a threshold sweep bought 19 of 20 recall points while a $15.51 rubric rewrite bought the rest. Sweeping is free if you kept the scores.

So all three that record decisions kept the data, and none of them ships the sweep. If I were contributing one thing to any of these repositories, it would be that.

There is one place where SystemOneHarness and jev-linkmap flatly disagree, and I think SystemOneHarness is right. Its §7.3 records a failure where the order-desk observation said "packed" and named the carrier but never said "not shipped"; the model put a tenth of its belief on finish and its probability on ship sat at 0.73 to 0.84 against a 0.8 gate. Two runs in five ended no_confident_action. The fix was adding "Shipped: no. Cancelled: no." to the observation — five in five, with no change to the gate. The rule they drew:

The gate is never tuned to fit the environment; the environment is made legible to the model.

Sweep the threshold to find out what your data supports. Do not sweep it to paper over a state the model cannot read. Those are different acts and they look identical in a diff.

One model call per step: it holds, and here is the bill

SystemOneHarness's headline is "one model call per step." I checked it against controller.py and it is exactly true. One self.provider.decide(enc.state, compiled.questions) per iteration, no other model anywhere in the loop, and the compiler packs the whole step into that one request: next_action over every feasible action plus finish and escalate, goal_reached as a second reading, every declared guard, and one question per parameter of every feasible action — not just the chosen one, because you do not know which one is chosen yet.

Compare WindTunnel, which needed two: Jev picks the tool, Mercury 2.5 writes the arguments, at a measured 0.93 Mercury calls per step. A decision model cannot emit a string, so somebody has to.

model calls behind one decision
01234callsSystemOneHarness1action, every parameter of every feasible action, goal check and guards in one requestfastbrowse1 + 1 generative+1 noul batch to shortlist a page over 160 controls, +1 when a choice over 240 goes group → element, +1 LLM only when a field needs textWindTunnel (measured)1 + 0.93 generativeJev picks the tool; Mercury 2.5 writes the arguments on 0.93 calls per step, skipped entirely when the tool takes nonedjev · diffusion1, up to 4 per schemaone denoise read answers the whole schema; first-read entropy over 0.1 buys three more noise drawsdjev · --engine ar1 per questionone restricted next-token read per question, in order, behind a cached prefix — so the floor is per question, not per schemawebctl1 per resultnot a step loop: one Jev call per search result, 8 in flight, plus one batch for the candidate duplicate pairsdecision model, alwaysdecision model, conditionalgenerative modelWindTunnel’s 0.93 is measured over 147 attempts; the rest are read from the code.

So what did SystemOneHarness give up? Not arguments — it fires actions with parameters all day. It gave up strings. Param.from_dict refuses at load time anything that is not a fixed choices map, a from reference to a candidate list the environment enumerates, a boolean flag, or ordered levels. There is no fifth kind, on purpose. So the text a form needs does not get written at run time; it is supplied by the operator, by name, before the run starts:

s1 run --browser --headless --start-url https://example.com/book \
  --text name=Customer --text email=user@example.com \
  --goal "Book a table at 19:30 with a window seat."

The model chooses type_text(field=…, value="name"). It never sees the string "Customer" as something to produce; name is an option in a list. WindTunnel pays a second model call at run time. SystemOneHarness pays a human at configuration time. That is the trade, stated plainly, and it is a real trade rather than a free lunch: it means the loop cannot handle a task whose text is not known in advance, and a search query, a message body or a file's contents are exactly that.

The project knows. docs/design.md lists it as open decision #2 — "parameter questions for every action every step, or two requests" — and recommends one request with a switch. The switch is even half-built:

# systemone_harness/actions.py
two_request: bool = False

It is parsed from YAML, serialised back out in to_dict, and read by nothing. Four references in the repository, all of them plumbing. The escape hatch from the headline is in the data model and not in the loop, which is either discipline or a TODO depending on how charitable you feel. I read it as discipline: the property is load-bearing enough that they would rather not have a code path that quietly gives it up.

The other cost is tokens, not calls, and the benchmark shows it. Every parameter question of every feasible action rides every request, and the answers for the five actions you did not pick are thrown away. ship_cheapest spends 6,304 input tokens over 6 steps — about 1,050 per step — for an action space of six actions with at most one parameter each. This is the same shape as the json-render finding, where an eight-element dashboard was two calls carrying the entire component catalog in both. The cost of one-call-per-step is that every call carries the whole space.

At Jev's $0.042 per million input tokens that is $0.000265 a run, so nobody cares yet. At 200 actions with four parameters each, somebody will.

fastbrowse: the composer done by code

fastbrowse is the one that takes the composer problem seriously, and its answer is the best in the set: the LLM names the evidence, code produces the string.

A flow diagram of a browser agent. A Task box, plain English plus a start URL, feeds two boxes that run at T=0 in parallel: Plan, checkable requirements from the task alone, and Open, the start page or a direct address. Open feeds a shaded panel headed 'each step: one batched Jev request' containing four boxes in a row — Observe, page indexed into candidates; Jev picks, operation and target with odds; Gate, irreversible? secrets by name only; Act, settle on DOM quiet — with an arrow looping from Act back to Observe. Two branches leave Jev picks: READ goes to Read, Jev picks a quote, LLM reads if none fits, then to Notes, facts with verbatim quotes from the page; DONE goes to Done?, Jev checks, LLM verifies when unsure, then to Answer, every claim cited or why it stopped. A legend colours each box by owner: Jev picks from what is on the page, LLM plans reads and writes, Code owns every gate and every quote.
fastbrowse's own diagram of a run, and the division of labour this section is about: the coloured bar on each box says who owns it. Read and Notes are the composer — the model picks a quote, code carries the text. (agent-labs-dev/fastbrowse, assets/architecture.svg at 3588580; MIT, terms in /articles/system-one-harnesses/NOTICE.txt.)

The reader is an LLM. It reads a page capture whose blocks carry ids, and it cites block ids. It does not retype the page. Code takes the id, slices the capture at that block's character offsets, and that slice is the quote:

# src/fastbrowse/retrieval.py
quote=capture.text[start:end],

Then the citation link is derived, also without asking a model, as a #:~:text= scroll-to-text fragment built from the quote's first and last five words. src/fastbrowse/citations.py is 21 lines and its docstring is the design: "Links derived from captured quotes, without asking a model for a URL."

fastbrowse doing a sign-in, a two-item cart, a shipping form and a checkout in 13 steps. The project's own recording at 3588580, transcoded; MIT terms in /articles/system-one-harnesses/NOTICE.txt. The last frame is the point: the answer carries a verbatim quote that code copied out of the page capture, not a sentence a model wrote about the page.

The last frame of that clip is the architecture. The number $43.18 arrives with "Total: $43.18" attached, and the quote was copied, not recalled. Whether the model hallucinated is not a question you have to ask, because the model was never holding the string.

Its published numbers, same 14 tasks three times each on 2026-09-22 against the Browser Use agent on the same class of cloud browser: 41/42 against 39/42, at $0.0041 median per task against $0.3668 — $0.36 for the whole suite against $26.01. It is also honest about the shape of that win: "Jev selects actions through classification; planning, field text and reading can still require LLM generation," and "the Browser Use agent is faster on four of the seven lookups." The cost ratio is real; the time ratio is only real on the long tasks.

djev: a diffusion model as a decision model

I argued in any model can be Jev that the System One readout is a serving feature, not a model feature — you need an endpoint that scores caller-supplied candidates in one forward pass, and any checkpoint with logprobs can be made to do it. djev is a direct test, because it is the same server wrapping two completely different models.

The diffusion mechanism first. DiffusionGemma denoises a whole canvas per forward pass. The vLLM patch adds four request fields that let you control that canvas: seed it with specific token ids, pin positions so denoising cannot rewrite them, cap the step count, and return temperature-1 logprobs at every position without decoding. The server then does this:

  1. Build the answer template as text — urgent: yes\nbucket: A\ntone: 2 — and tokenize it.
  2. Find each question's slot: the one token position that changes when that question's label changes. If a label is not a single token, or two labels of one question move different positions, the schema is rejected outright.
  3. Seed the canvas with the template, replace each slot with random noise, pin everything else.
  4. One denoise step. Read the logprobs at each slot, restricted to that question's label ids. Softmax over those. That is the distribution.

The readout is the pinning, and it is beautiful: the answer's fixed text is held, so the only thing the model is allowed to change is the labels, and the only thing you read is where they were. diffusion_read_only returns the logprobs and ends the request. No text is generated at any point.

Two branches take it further, and both matter.

span-answer-type adds a question whose answer is a piece of the state's text, returned as character offsets. The canvas gets a label and a blank, the blank is left free, four denoise steps run, and the logprobs of the text's own token ids are read at every blank position. The decode walks the text with those tokens, so the answer is a substring by construction and cannot be a value the text does not contain. That is the same guarantee fastbrowse gets by slicing a capture, except it is enforced inside the read instead of after it. If it holds up, it collapses the second model call for the entire extraction case — which is most of what WindTunnel's 0.93 Mercury calls are buying.

ar-engine is the one that settles the argument. It runs the identical schemas on an ordinary autoregressive model with no diffusion machinery: one restricted next-token read per question, in order, earlier answers prefilled behind a cached prefix. And it publishes a measured comparison:

GLM 5.3, --engine arDiffusionGemma, diffusion
22 label questions (noul, choice, score)2120
49 span fields, 11 texts4849
5 list questions55
a 1,096-character text in 4 windows, 4 fields3, in 89 s4, in 4 s
reads per label question1, about 0.27 sone joint read for the schema, about 0.1 s

The claim survives, and the table says exactly what the diffusion model is worth. On accuracy it is a wash — 21 against 20 on labels, 48 against 49 on spans, which is one item either way on small batteries. The difference is amortisation: the autoregressive engine pays one read per question, the diffusion engine pays one read per schema. Ask one question and they are equivalent. Ask forty and the canvas answers them in a single pass. That is the whole advantage, and the README states it without decoration: "The diffusion engine's edge is cost… The AR engine's edge is that it needs nothing beyond stock vLLM."

The gate changes too, and this is the part that made me stop and reread. On the AR engine, "reads are deterministic, so samples collapses to one" — no noise seed, nothing to resample, auto_threshold is meaningless. djev's confidence gate only exists because the diffusion read is stochastic. The uncertainty machinery is a property of the serving path, not of the decision.

Cadence, which does not belong here

There is no decision model in cadence. There is no model of any kind that speaks a wire protocol. It is a research library about bounded patches with local state, written in NumPy, with a Lean development beside it.

It is in this piece because when I went looking for its decision loop, out of diligence, I found this:

# src/cadence/circuits/deliberation.py
class Deliberator(Generic[S, A]):
    def __init__(
        self,
        actions: Callable[[S], Sequence[A]],
        transition: Callable[[S, A], S],
        evaluate: Callable[[S], float],
        terminal: Callable[[S], bool],

An enumerator. A scoring function. A budget that stops the search. A transition that executes what was chosen. The same four parts, with no language model anywhere, in a codebase whose author has never used the word Jev.

That is not a coincidence and it is not profound. It is what a controller over a finite action space looks like, and it is what SystemOneHarness's own design doc says it is building: "the harness is a controller over a finite state machine: the environment is the transition function, the model is the policy, the action space is the alphabet." The decision model is a policy you can swap into a structure that predates it by fifty years. The interesting engineering is the other three callbacks, which is the same conclusion I reached from a completely different direction three days ago.

What I would build

If I were putting a decision model behind something tomorrow, from five codebases:

  1. Spend the effort on the enumerator. It is the only part whose quality is not bounded by a vendor. SystemOneHarness's own probe is the proof: the same model scores 0.03 or 0.50 on the same question depending on how the state was written, and neither number beats a constant. It is also gate 2 of the four-question procedure this corpus arrived at from the deployment side.
  2. Make the observation state its predicates, including the false ones. Adding "Shipped: no. Cancelled: no." moved two failures in five to zero, with no gate change. It is the cheapest fix in any of these repositories.
  3. Batch questions only when they share a subject. One observation, many questions: batch. Many documents, one question each: do not, or you have turned a guarantee into an instruction.
  4. Keep the distributions, then ship the sweep. All three loops record every probability. None of them lets you re-threshold a saved run. That is a free axis on data you already have.
  5. Decide where your strings come from before anything else. They come from the operator (SystemOneHarness), a second model (WindTunnel), an LLM naming spans that code copies (fastbrowse), or the model itself bounded to a substring (djev). The fourth is the newest and, if it survives contact, the best of them.

What would change my mind

6 claims above, and what would falsify each

  1. SystemOneHarness really does make exactly one model call per step, and the price is that no parameter can be a string.

    Read from controller.py and actions.py at ab8e8f0, not from a run — I have no OpenRouter key pointed at a decision model. The loop calls provider.decide once per iteration and Param.from_dict raises on a free-text parameter, so the claim is structural rather than empirical. It falls if someone wires two_request to something, or if an environment slips a string in through a candidate list computed per step, which the compiler would happily allow: from: <list> accepts whatever the environment enumerates, including 255 generated strings. If a real deployment turns out to generate its candidate lists with an LLM, the second call moved upstream rather than disappearing, and "one model call per step" is bookkeeping.

  2. Four of five enumerators contain no model, and that is the pattern rather than an accident of small samples.

    Five repositories in six days is a sample of one week, and the four that are model-free are the four whose candidate sets are cheap to compute: URLs, DOM controls, YAML, token ids. The counterexample is already in the set — fastbrowse put a noul pass into its enumerator the moment a page got dense. The cleanest test is a domain where candidate generation is genuinely hard: retrieval over a corpus with no link structure, or action selection in a space that must be constructed rather than observed. If projects there reach for an LLM enumerator as a matter of course, "the enumerator is model-free" is a fact about easy domains and I have generalised from them.

  3. The canvas probe shows the encoding, not the model, is the ceiling — no encoding beats a constant on the question that matters.

    Thirty frames, one game, one model version, and baselines of 0.97 to 1.00 because the frames were lopsided. That last part cuts both ways: a baseline that high is nearly unbeatable, so "does not beat the baseline" is a weak bar to have set. Rerun the probe on a balanced frame set — equal numbers of enemy-ahead, gap-ahead and clear frames, and a next_control distribution that is not 29 of 30 one answer — and the accuracies become informative rather than degenerate. If next_control then lands above a 0.33 baseline at the higher resolutions, the encoding is workable and what I read as a null result is an artefact of the frames they happened to sample.

  4. djev's ar-engine table shows the readout is a serving feature: an ordinary model matches a diffusion model on accuracy and loses only on amortisation.

    It is 22 label questions and 49 span fields, on one branch, unmerged, with no unit tests, run by the author. 21 against 20 is not a difference. The load-bearing part is the per-schema-versus-per-question read count, which is structural and does not need a big sample — but the accuracy wash does. Run both engines over a few hundred questions with calibration measured, not just top-1 accuracy, and the picture could change completely: a next-token read on a chat-tuned model has a well-known prior over A and yes, and if GLM 5.3's probabilities turn out badly calibrated while DiffusionGemma's are not, "any model can be Jev" holds for the argmax and fails for the number, which is the half that matters.

  5. webctl never gates on Jev's confidence, so the calibrated quantity is decoration in that pipeline.

    Read from rank() and backfill() in cmd/webctl/cli/search.go at e9bc54a; the keep rule is a comparison on the score, and confidence reaches only --verbose and the JSON. If that is deliberate it is defensible, and the test is whether it costs anything: take a saved eval run, and re-rank with score × confidence or with a confidence floor. If quality moves, the field was load-bearing and ignoring it is a bug rather than a design. If it does not, confidence genuinely adds nothing to a relevance filter and every threshold in this article that reads a confidence is doing work that a score would do.

  6. fastbrowse and webctl are shippable and SystemOneHarness and djev are not yet.

    I judged this on tests, CI, release channels and whether the published numbers come with per-run data — proxies, all of them, and none of them is running the thing in anger. SystemOneHarness in particular could be better than its benchmark suggests; a toy environment is a weak instrument, not evidence of weakness. Point all four at one task that all four could do — a form fill with an enumerable field set, say — and measure completion, cost and refusal rate. If SystemOneHarness matches fastbrowse there, "thin benchmark" was a statement about the benchmark and I have let it stand in for the code.


Media: every image and clip here is the project's own, transcoded or rasterised and committed under /articles/system-one-harnesses/, with terms and source revisions in NOTICE.txt. fastbrowse publishes an architecture diagram and a demo recording; SystemOneHarness publishes a gameplay recording. webctl and djev publish neither — I read both trees at e9bc54a and 442eab6 and found no diagram, screenshot or recording in either, only text — so nothing of theirs is shown above, and the sections on them rest on the code and the tables they do publish.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Five harnesses for a model that cannot write", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026systemoneharnesses,
  author = {Satyajit Ghana},
  title  = {Five harnesses for a model that cannot write},
  url    = {https://ai.thesatyajit.com/articles/system-one-harnesses},
  year   = {2026}
}
share