The Jev layer
A classifier that
returns numbers.
Harness decisions are often small and repeat: is this tool output still worth keeping, are these two reports the same issue, which source should come first. A chat model answers in prose after a full round trip. Jev answers with a number. The layer then decides in code.
The whole layer ships off. With the master switch off there is no network call, no key read, no consent read and no prompt change: the harness behaves exactly as it did before the layer existed. Everything below describes what happens once someone turns it on.
What it is
01Jev is a System One model from TypeSafe. It does not write text. You send it a state object and a set of questions. It returns a typed answer for each one, all read off that one state. Three answer shapes exist:
- Choice: pick one of up to 255 options. Returns the choice, a probability for every option, and a confidence.
- Score: rate against two to ten ordered levels. Returns a possibly fractional score, per-level probabilities, and a confidence.
- Noul: yes or no, as a bare probability between 0 and 1. It has no confidence field. A gate must not wait for one, because the model never sends it.
Output is free and input is $0.042 per million tokens. A decision costs whatever it takes to describe the state. The answer itself adds nothing. Questions are batched for this reason: several questions on one state cost the same as one.
Where it sits
02
The layer has two halves that share a key and nothing else. The advisor is
first-party SpecPi code that answers questions about session state and never holds authority:
it cannot call a tool, grant a capability or allow an action. The guard is a
separate pinned package, specpi-jev-guard, and the one exception: it scores
commands before the permission system sees them, and can refuse one.
Six gates before anything leaves
03A call reaches the network only after passing every gate below, in order. If any gate says no, the attempt ends quietly and the harness follows its normal path. No advice is always an acceptable outcome. A timeout, a missing key, a refused consent, or a low confidence answer all look the same from outside.
Consent is separate from the switch. A config flag records a setting. It cannot record that a person was told what it costs. The first time any system would send state, a dialog names the destination host, the shape of the data, and the byte limit. Without a person at the keyboard, nothing is sent. An unattended run cannot start transmitting. The grant is tied to the host it was given for. Pointing the client elsewhere asks again.
Seven systems
04One rule covers all seven: check local state first. Spend a Jev call only on a question local state cannot answer. That rule removed the largest measured waste here. Six of SpecPi's ten tools were offered on every request and never called. Two of them need a human to select an improvement first, which is already recorded on disk. They stay hidden until one exists.
- Retention decides whether a large read-only tool result is still worth keeping, before it is saved. Jev answers yes or no. Code does the shortening, so the replacement is a fixed head-and-tail digest. No model-written text enters the transcript.
- Compaction adds guidance at the one point where the prompt cache is dropped anyway. The advice causes no extra cache loss.
- Gap groups capability-gap reports about the same problem and scores impact on its own. Before this, identical reports missed the repeat count because their keys differed, and the ranking used the reporter's own impact guess.
- Sources sorts the sources a delegation batch will freeze. It reorders only. It never drops one. It also reuses two answers the same call already produced, and warns before up to 200 files and 8 MiB are frozen for a child that looks unable to answer the question.
- Progress targets turns instead of input tokens. Output is 59% of spend at tier 3 and 66% at tier 4, so turns matter most on hard tasks. It looks for a session that has stopped moving: a repeated tool call, a run of errors, several turns with nothing written. Only then does it ask if the run is stuck. It tells the person, not the model. A wrong nudge to the model would waste the turn it tries to save.
- Untrusted content adds a fixed warning line to a fetched page that reads like instructions for an agent. It covers web and browser results only, never the agent's own shell output, and it never blocks. It needs no extra call while retention is on, because the same digest answers both questions in one request.
- Capability arming reads the request once, before anything is sent to the model, and offers a hidden tool group when the task looks like it will need one. It turns nothing on. It moves a confirmation the person would have seen at turn six to turn zero. Section 06 shows why that timing matters.
Why the decision happens on arrival
05One way is to let results pile up, then rewrite them in a batch. Tested on the recorded token history, that saves about −12% of long-attempt cost. Deciding on each result before it is saved saves about −61%. The reason is the cache: a rewrite throws away the cached prefix, and about 94% of prompt tokens are cache reads.
Pricing the rule that shaped everything
06Every choice above follows one claim: changing the tool set mid-session drops the provider's cached prompt prefix. About 94% of prompt tokens here are cache reads priced at a fiftieth of fresh input, so that drop moves most of the spend. That is why there is no tool router, why capability arming runs before the first request or not at all, and why advice is never applied to past turns.
This had not been measured here. It matched Pi's docs and general reasoning, but no number
backed it. The test used three arms of
t3-cascade-ledger with one difference: when Browser QA's fourteen
tools reach the request. Never, at turn six, or from the first request. The rule was
written down before the run: if cached tokens drop and re-warming costs more than 10%
of attempt cost, the rule stands. Otherwise it goes.
| Arm | Tool schema | Fresh tokens | Cache hit | Model cost |
|---|---|---|---|---|
| Never armed | 6,966 | 27,652 | 93.1% | $0.01212 |
| Armed from turn 1 | 15,214 | 28,227 | 94.2% | $0.01411 |
| Armed at turn 6 | 6,966 | 52,420 | 89.5% | $0.01777 |
The result cleared the bar. In all three attempts, the request after the switch saw cached tokens fall to 3,200 (from 25,984, 17,792 and 21,120) while the prompt kept climbing. One request went from 3,673 fresh tokens to 31,337: the whole conversation so far, re-read at fresh-input prices. The re-warm cost 14.6%, 21.6% and 23.9% of the attempt, 20% on average against the 10% bar set in advance.
Arming the same group from the first request cost 16% more than never arming it. Switching it on mid-session cost 47%. Asking up front cost much less than switching later. That timing gap is what capability arming exists to use.
Limits. Three attempts per arm show an effect this size but cannot rank the arms closely. The turn-1 arm also took more turns than the control (18.7 vs 16.3), and some of its extra cost comes from that, not schema size. The plan for this experiment also said activating Browser QA rebuilds the system prompt as well as the tool schema. That is no longer true: the pinned 0.3.0 release moved its guidance into tool descriptions, and the measurement confirms it. System prompt length matches between the control and armed arms. The old claim is removed. Web access, a third-party package, still carries prompt metadata.
Calibrating the gate, and what it found
07The gate turns a probability into a decision. Until now every number in it was a placeholder. The layer shipped before its own gate was set. Calibration asked two questions, because a threshold can fail in two ways.
Does confidence tell right from wrong? Tested on labels this repo already had: predicting an attempt's outcome, task type, and tier from behaviour alone, with the labels hidden. For a Score the answer is yes, weakly: about two thirds right and 96% within one level, on a fifth of answers, against a 43% majority. For Noul and Choice on that set, the answer is no: both stayed near their base rate. No pre-registered precision target was met on those curves. The artifact records that instead of a fitted number.
Can the gate fire at all? This question found a real defect. The retention gate needed Score confidence of 0.80 and a value within 0.15 of a level. On a clear-cut case (a directory listing of vendor icons during a changelog edit), Jev answered “spent” every time, at confidence 0.73 to 0.85, mostly under 0.80, with the value drifting past the line too. So retention could approve keep this result but almost never this result is spent. The branch that does anything was out of reach. Running the layer longer would not have shown it, because a silent system and a system with nothing to say look the same.
The same check caught a second gap in compaction, and a third in new code for this
release: the capability threshold sat above what the model sends even on
a clear yes. Each fix read the working point off recorded
results instead of picking a cautious-looking number. Thresholds are now pinned by a
test to evals/runs/jev-calibration.json, with the figures quoted on this
page. Changing one needs new evidence.
On the live questions, where the state holds the material under review instead of counters, the gap is wide: a planted credential scores 0.96 vs 0.04 for a clean report, a page with an injected instruction 0.97 vs 0.04, a thrashing session 0.89 vs 0.30 for steady work. The gates sit in that gap.
The guard, and why it fails closed
08
The command guard is not one of the seven systems above. It is
specpi-jev-guard, a separate pinned package with its own gate, its own
configuration file and its own commands, and it is the only thing SpecPi installs that can
take a tool call away. SpecPi pins it and ships it inert: the package's own
default is on, so a fresh install would otherwise start gating shell and file calls on day
one. SpecPi writes enabled: false instead. Turning it on is a human act, and
/jev-guard setup is the command that does it — no switch in SpecPi will.
It fails closed, and that is the trade to understand before switching it on.
With no key, an unreachable endpoint, or an answer it cannot parse, the call does not go
through. In a session with no one to ask, the middle band blocks too unless
uncertain is set to allow. So an outage stops gated work until the
guard is switched off again. There is no setting that hands the decision back to
@gotgenes/pi-permission-system instead — which is what decides every call
while the guard is off, exactly as it did before the package existed.
-
Local rules first, free. Read-only commands and ordinary project
writes never leave the machine. A hard-deny list covers the clearly catastrophic cases
— deleting the filesystem root, writing a raw disk device, piping a download into a
shell — and your own
safeCommands,allowedCommandsanddisallowedCommandsare consulted before anything is sent. -
Jev scores what is left, 0 to 1. At or above
blockThresholdthe call is blocked; at or aboveaskThresholdyou are asked; below, it runs. Set those from evidence rather than instinct:/jev-guard checkscores a command without running it, so you can put the thresholds in the gap between the commands you must stop and the ones you must not. -
Writes are judged by target. Ordinary project files pass locally. Paths
outside the workspace, and paths matching
protectedPaths(.env*, keys,.ssh/), go to Jev. - Once a person is asked, silence means no. A dismissed dialog is not consent. A question with no answer cannot resolve to yes.
From 0.4.0 the guard publishes a per-session counter — classifier calls, blocked
calls once there are any, and the last verdict. In the terminal it is a footer line; in
SpecPi Chat it sits in the session footer beside the token and cache readouts, showing the
count and turning amber once something has been blocked, with the full line in the tooltip.
Chat renders the package's own string and nothing more: it is a readout, not a control, and
it disappears when the guard is off. Every judged call is also written to Pi's session file,
which is the durable audit trail. /jev-guard audit off turns the line off
without touching that record.
SpecPi asserts only three fields in ~/.pi/jev-guard.json and merges them into
whatever is there, so your thresholds, command lists and protected paths survive:
enabled: false, uncertain: "ask" and
backend: "openrouter". That happens at install and update and nowhere else, so
between installer runs the switch is entirely the package's: /jev-guard on for
this session, --global to save it, and a saved choice survives every restart.
The next specpi update does assert the off posture again, and says so when that
switched something off — otherwise a stale enabled: true from before the
package was last unpinned would arm the gate the moment it came back.
What it has actually measured
09The layer's row has been re-run since the gate was set, so there are two Jev columns. The middle column is the first full matrix (every harness, every tier, one model, one sitting) where three systems were on and retention could not shorten anything. See section 07. The right column is the same 37 attempts re-run with the set gate and five systems on, the setup described on this page.
| Measure | SpecPi | + Jev, before | + Jev, after |
|---|---|---|---|
| Score | 0.878 | 0.895 | 0.870 |
| Cost per attempt | $0.0042 | $0.0046 | $0.0046 |
| Tool calls per attempt | 9.6 | 10.0 | 10.0 |
| Turns per attempt | 8.6 | 9.0 | 8.9 |
| Tool error rate | 6.8% | 6.0% | 5.1% |
| Context growth per turn | 628 | 590 | 631 |
| Cache hit rate | 93% | 94% | 93% |
The turns row used to read 10.0 and 10.1, and it was wrong. The advisor reaches the model through the same proxy as the agent, deliberately, so that its spend is counted rather than hidden — and the runner recorded the whole proxy log as the attempt’s turn count, folding the layer’s own questions into the conversation. The corrected figures are 9.0 and 8.9 against a control of 8.6: a 4% difference where the page previously showed 17%. The per-attempt token and cost columns were never affected, because those are per-request accounting.
The layer has not paid for itself. It costs about 9% more per attempt and scores close to the base either way. The one figure that once suggested the mechanism was working (context up 6% less per turn) did not hold on re-run: 590 became 631 against a control of 628. That was noise, and the first reading was too kind. The tool error rate is lower in both Jev columns, but nothing here touches a tool call before it runs, so there is no mechanism behind that link. It is not claimed as one.
The ledger now records each call: was the advice used, and what did it save. The eval runner reads that out of each attempt's folder. Over the same 37 attempts the layer made 43 calls, none failed, none changed anything. They cost $0.0013 in total, 0.77% of the run's model spend. Call price is not the problem.
| System | Asked | Changed something | Why not |
|---|---|---|---|
| Tool-result retention | 38 | 0 | confidence below the gate, 38 times |
| Progress detection | 5 | 0 | not stuck, 5 times |
| Untrusted content | 0 | 0 | never reached |
| Compaction, gap, sources | 0 | 0 | never reached |
Retention asked 38 times and acted zero times. That is now a finding. The first time, the gate was out of reach, so silence proved nothing. This run used the set threshold, where a test spent result (a listing of vendor icons during a changelog edit) answers 0.75 to 0.85 and would pass. The gate opens. On 38 real reads the model stayed below it each time. On the real reads of a repair chain, where each result feeds the next step, it reports doubt, and the record shows that.
The threshold stays where it is. Keeping a result costs tokens. Dropping the wrong one can cost the task. Acting without confidence is what this system must avoid. The number now supports a plain claim: on this workload the main system does not pay off. Sessions with truly disposable output (wide greps before settling, pages read once) are still untested.
The ledger showed two more things a cost column hides. Progress once used twelve calls on one healthy attempt. It now uses five across all 37. Its local gate used to ask when any single signal fired. A 120-step repair chain re-runs its check command throughout, so that signal was normal for that task, not a warning. The attempt it spent its budget on scored 0.978. Asking only on two weak signals together, with a four-turn pause, fixed it. Jev answered “not stuck” all five times, on sessions that were fine. The untrusted-content system has never been called. It only joins when a result came from a web or browser tool. No task here produces one, so this suite says nothing about it. That is noted here so a missing row is not read as a zero.
A count mismatch found one more bug: the proxy saw 44 calls leave the machine and the ledger held 43. A system that is not awaited can still be running when the session ends. Its answer was dropped with its log line. The answer has to go (it belongs to a session that no longer exists). The line has to stay, because the ledger claims to list every send. Fixed. The fix came from comparing the two counts.
The first sessions long enough to compact
Everything above is a short session. Across every tier and every published run, including
Terminal-Bench, no session had ever compacted — so
compaction, the system that guides where a compaction cuts, had never been asked
anything. A row of zeroes that reads as “did nothing” was really “was never
in the room”.
Tier 6 changes that by declaring a 24,000‑token context window instead of a 200,000‑token one. Growing the corpus does not work — an agent with a shell chunks or greps rather than loading it, and a 254,000‑token corpus still produced no compaction at all. A small window overruns on the transcript, which no shortcut avoids.
| System | Asked | Changed something | Outcome |
|---|---|---|---|
| Compaction | 108 | 70 | consulted on nearly every compaction that occurred |
| Tool-result retention | 144 | 0 | confidence below the gate, every time |
| Command guard (native) | 266 | 0 | headless, so every ask degrades to a defer |
| Progress detection | 15 | 1 | fourteen healthy, one tool-error loop |
The guard row is kept as measured and labelled for what it was: this run used the
short-lived native guard, and command gating has since returned to the
specpi-jev-guard package, which these runs do not install. The other three rows
describe systems the layer still ships.
Seventy applied verdicts, against zero in every run before it. The layer
does act; it had simply never been placed in a situation where it could. Progress also
returned a tool-error-loop verdict — the one situation a purpose-built
task had entirely failed to produce, which appeared unprompted once sessions ran long.
Whether that work pays is a separate question, and sixteen attempts an arm cannot settle it. The direction is not even consistent: on the longer task the layer compacts less than plain SpecPi and costs less, while finishing correctly less often; on the shorter one it compacts more and finishes the same. Neither pass-rate difference can be told from chance. The evaluations page carries the comparison with its p‑value. What can be said is narrower and worth saying: the situation the layer was built for finally exists, and in it the layer is no longer inert.
Both Jev columns cover one harness on one model. The re-run happened separately from the
matrix the control comes from, so drift between sittings lands on the layer. The
per-system counts avoid that: they are the layer's own record of what it did.
scripts/jev-effect.mjs rebuilds
every figure above from the stored reports.
Turning it on
10
Nothing here is on after install. Switching the layer on switches its systems on with it.
A layer with every system off runs and does nothing. The choice is saved, so a
switch set once stays set. --session limits a change to the current session
and leaves the default alone.
/jev: show the layer state and change it.-
/jev on: the whole layer. Master switch plus every system that is off. Saved for new sessions;/jev on --sessionfor this one only. Nothing under/jevtouches the command guard, which is a separate package with its own commands. -
/jev status: current posture, plus each place a key can come from, with the active one marked. -
/jev ledger: local record of every payload sent. Each call is stored intransmissions.jsonlon this machine. /jev forget: revoke consent.
In SpecPi Chat the same switches are toggles under package settings. They write the same
file the extension reads. Each budget shows the advisor's call count: the total, one row
per system, and how many calls changed anything. A bare call count hides
“asked six times, changed nothing”. That split is shown instead. Those counts live in a
separate usage.json with numbers only, which keeps it safe for a second process
to read. Chat reads it and never writes it. The advisor writes it only once the layer
is on. Like the settings nearby, the counts belong to the agent folder,
not one chat, so each entry carries a date and a running or ended mark.
The default cap is 2048 calls per session, with per-system caps that add up past it, so the total still binds. Treat it as a backstop. A full tier-3 task uses 4 to 7 calls. The busiest attempt on record used 12. Only a loop can reach 2048, so reaching it signals a bug. Cost is not the point (a call runs about $0.00003, so the whole cap is about six cents). The cap limits how much summary leaves the machine and how much waiting a looping run can add. Hitting the cap is announced once per system. Quiet is normal here, so a system gone quiet from an empty budget would otherwise pass for one with nothing to report.
Jev is on OpenRouter, so an OpenRouter key covers the layer. It is
resolved the way Pi resolves other provider keys, in Pi's documented order:
the openrouter entry from
/login openrouter in
auth.json, then OPENROUTER_API_KEY from the environment. There is
nothing extra to set up. The advisor once read the
environment only and reported a missing key for people whose OpenRouter login worked
fine. JEV_BACKEND=typesafe selects the TypeSafe direct API instead, which
needs a TYPESAFE_API_KEY. The two keys are not interchangeable. The wrong one
returns a bare 401.
The command guard used to be the exception: before its 0.3.0 the package read
OPENROUTER_API_KEY from the environment and nothing else, so a key stored by
/login served the advisor and was invisible to it. It now resolves the same
credential in the same order, so one key covers both halves and there is no second thing to
configure.
Retention, storage, and logging of sent data follow OpenRouter and TypeSafe rules, not SpecPi's. The layer ships off, asks before its first send, caps payload size, and keeps a local record of each one.