Log Every Jev Decision — and Pin the Version the Record Carries
A Jev decision log needs one record per call: state reference, versioned questions, answers, reported model and policy. Pin jev-1.13.0, never jev-latest.
Go deeper. Build your own.
swap if/else prompt soup for typed decisions the session bus can log.
Sooner or later someone asks why a gate let a write through, and they arrive with a transcript, a diff and a deadline. What they need is a Jev decision log: one record per call saying what state the classifier saw, which exact questions it answered, what it returned, which model version answered and which policy turned that into a verdict. Without it, the only move left is to ask the model again, and the model you ask may not be the one that answered.
This is the runbook for making that record boring. By Tuesday every Jev call writes one record with the gated tool call and session attached.
The state sits behind an access-controlled reference, hash beside it. The questions carry a version, not just a key. The model is pinned to jev-1.13.0 rather than an alias, with a plan for the day it goes away. And your team knows that replaying policy over a stored answer is deterministic and asking Jev again is not.
Agents made this a records problem. A gate’s score is a number code acts on within a second, between an agent and a shell or a task and a model tier. Typed answers are the good news: a probability under a key you chose can be logged, diffed and replayed in a way model prose never could. The record is how you collect on that.
Sep 11–21: aliases, dated IDs and a model swap under an unchanged name
TypeSafe’s models page lists one Jev version, jev-1.13.0, behind two aliases. jev-latest points at it and is the SDK default; jev-preview points at the same build, and no preview build exists right now. The page is candid about the cost: an alias moves when a new release ships, so answers can change with no change on your side, while the response’s model field reports the versioned ID that answered. Once you have tuned thresholds against a version, it says to “pin that version’s ID instead of the alias”.
Screenshot: TypeSafe AI, “Models - TypeSafe AI” (undated), captured Sep 21, 2026.
GET /v1/models lists only the aliases; versioned IDs are accepted anyway. What the page doesn’t publish is a retirement policy: no support window for a pinned version, no notice period, no changelog from 1.12 to 1.13. TypeSafe promises nothing either way, which makes the plan for losing jev-1.13.0 yours to write.
The doors report versions differently: TypeSafe direct and Cloudflare report jev-1.13.0. OpenRouter’s TypeSafe SDK guide routes jev-1.13 as typesafe/jev-1.13 and jev-latest as ~typesafe/jev-latest, the tilde marking the alias, and its sample responses report a dated typesafe/jev-1.13-20260917. It doesn’t say whether that dated string, or TypeSafe’s three-part jev-1.13.0, is accepted in a request. Vercel AI Gateway’s TypeSafe-compatible docs use typesafe-ai/jev in every request and sample response, and the gateway lists no versioned Jev.
The precedent belongs to another vendor. On Sep 11, Kimi’s changelog announced K2.8 Preview fully rolled out in Kimi Code with the model ID unchanged, still kimi-for-coding, so clients needed no configuration changes. Nothing went wrong, and the note was explicit about the swap. But a lane that logged only kimi-for-coding can’t tell from its records which model answered on Sep 10 and which on Sep 12.
The clients moved too. TypeSafe’s JavaScript client config falls back to TYPESAFE_DEFAULT_MODEL, then jev-latest, when a call names no model; the Python default is also jev-latest. The Python SDK changelog lists breaking releases on Sep 15 (0.6.0) and Sep 18 (0.7.0), and 0.7.1 landed today.
A gate log line isn’t a classifier record yet
Pre-action gates, not dashboards already argues for one evidence line per gate decision, stamped with the policy version, and the provider-cutoff rehearsal already says to pin versions and keep the effective model in run records. Take both as settled.
A Jev gate adds three things those lines weren’t built for. Its verdict is a probability from a model that can change behind a name. Its question is prose someone will reword. And the obvious way to check an old decision, calling Jev again, returns a new sample, not the old answer.
Build the Jev decision log in eight steps
Step 1: Write one record per call, on every path
Every Jev call writes exactly one record, whether the gate allowed, denied, routed, timed out, hit a 429 or fell back. A log that exists only on the happy path is a sample of your successes.
| Field | What it holds | Why it’s there |
|---|---|---|
decision_id, ts |
Your ID and a UTC timestamp | The key everything else joins on |
session_id, tool_call_id, lane |
The agent session and the exact call the verdict gated | Joins the record to the transcript and the tool log |
state_ref, state_sha256 |
A retrievable, access-controlled pointer to the exact state sent, plus its hash | The reference is the evidence; the hash only proves nobody changed it |
question_set, question_sha256 |
Question set ID and version, and a hash of the full text, criteria and options sent | A key alone hides edits (step 3) |
answers |
Raw answers keyed by question ID: Noul noul; Choice choice, probabilities, confidence; Score score, legend, probabilities, confidence |
Noul carries a probability only |
model_requested, model_reported, version_known |
What you sent, what the response says answered, and whether that string names a version | The pin, and proof it held (step 4) |
door, sdk |
Endpoint used, and client library with exact version | Two breaking SDK releases in one week |
request_ids |
x-typesafe-request-id; OpenRouter id; Vercel generationId |
The vendor’s handle for a support ticket |
policy_version, verdict, outcome |
Threshold table version, what the gate decided, what actually happened | Replay needs the policy; audits need the outcome |
path |
ok, retried, 429, 529, timeout or fallback, with retry count and latency |
Audits start on the failure paths |
TypeSafe’s confidence docs say Noul answers don’t carry a confidence, so a Noul row stores noul alone; a confidence on a Noul row means someone’s code invented it. policy_version names the per-tool-class threshold table, versioned like code. Field names differ by door, so normalize before you write: the endpoint-dialect runbook maps every door into one shape, and its model_reported and version_known names keep the records joinable.
An illustrative record, one JSON line per call (values made up):
{"decision_id":"jd-000481","ts":"2026-09-22T14:03:11Z","lane":"billing-migration","session_id":"s-7f3c","tool_call_id":"tc-0192","state_ref":"decisions-store://2026/09/22/jd-000481.json","state_sha256":"9b1e...","question_set":"write-gate@7","question_sha256":"41c0...","answers":{"destructive":{"type":"noul","noul":0.12},"scope":{"type":"choice","choice":"repo_local","probabilities":{"repo_local":0.93,"shared_infra":0.05,"other":0.02},"confidence":0.9}},"model_requested":"jev-1.13.0","model_reported":"jev-1.13.0","version_known":true,"door":"typesafe","sdk":"typesafe-sdk==0.7.1","request_ids":{"typesafe":"req_..."},"policy_version":"gate-table@12","verdict":"no_decision","outcome":"ran","path":{"status":"ok","retries":0,"latency_ms":184}}
Decide now what the gate does when the record can’t be written. For write and destructive classes, no record means no action: deny and alarm; for read-only calls, proceed and alarm. The log is evidence, not the wall; permission rules and the sandbox behind the gate keep working when the logger, the classifier or the hook fails, which is why they stay. What each gate does on a 429, a 529 or a timeout belongs in the fail-mode table, and path is where those choices show up afterward.
Step 2: Store the state as a reference, and scrub it once
The state is the evidence; its hash isn’t. A SHA-256 proves a stored blob is the one you sent, but it can’t show an auditor what the gate saw. So the record points into a store with its own access control and retention at least as long as your audit window, with the hash beside it as an integrity check.
- Scrub before you send, then store what you sent. Credentials and customer secrets come out once, before the call, so logged and sent state are the same bytes. Scrub only the logged copy and the hash stops matching.
- Keep it out of application logs. TypeSafe’s JavaScript client logs bodies at
debugand redacts only known credential headers. A debug log in a shared sink is a leak with timestamps. - Size it on purpose. Jev takes at most 64K tokens per request, 32K for state plus the longest question, so one blob is bounded; a year of them isn’t. Retain long for destructive gates, short for triage.
If a lane switches on zero data retention, assume the vendor keeps nothing for you. Your store is the only way back.
Step 3: Version the questions, not just their keys
TypeSafe’s API reference is precise about keys: you choose each one, answers come back under it, and the key “is not sent to the underlying model”. Good for your code, bad for your records. The key destructive can front three different instructions in a month, and the model only ever saw the instructions.
So question text lives in a versioned file, the record stores the version plus a hash of the text sent, and CI refuses any change to instructions, criteria or options that doesn’t bump the version.
# questions/write-gate.yaml (illustrative)
id: write-gate
version: 7
questions:
destructive:
type: noul
instructions: "Would this command delete or overwrite data outside the working tree?"
scope:
type: choice
instructions: "Where does this command take effect?"
criteria:
repo_local: "Only files inside this repository"
shared_infra: "Shared infrastructure, cloud resources or other repositories"
other: "Anything else, or it cannot be told from the command"
Hash the rendered request, not the file, so a template variable that changes the text changes the hash. Old answers stay valid records of the old question; they just aren’t evidence about the new one.
Step 4: Record the model the response reports, and mark the doors that don’t say
model_requested is what you asked for. model_reported is the response’s model field: jev-1.13.0 on TypeSafe direct and Cloudflare, typesafe/jev-1.13-20260917 on OpenRouter. Store both verbatim, then set version_known to true when the reported string names a version and false when it’s a slug.
On Vercel AI Gateway the documented answer is false: sample responses report typesafe-ai/jev, and no page shows a way to pin a version. The docs don’t settle whether one exists, so write “not documented” in your door sheet and test it. Until a Vercel lane’s records can name a version, don’t give it a gate whose thresholds were tuned against one.
Alarm on novelty. A model_reported value your fleet has never seen is the cheapest drift detector you’ll build. On a pinned lane it should never fire; when it does, hold that lane’s gated actions and read the records.
-- illustrative: model strings not seen before, last 24 hours
SELECT lane, model_reported, count(*) AS calls
FROM jev_decisions
WHERE ts > now() - interval '24 hours'
AND model_reported NOT IN (SELECT model_reported FROM jev_models_seen)
GROUP BY lane, model_reported;
Step 5: Pin the model, the default and the SDK
Pin three things, not one:
- The model on every call. Send
jev-1.13.0to TypeSafe andtypesafe/jev-1.13to OpenRouter, which uses author-prefixed IDs as-is. Neverjev-latest,jev-previewor~typesafe/jev-lateston a lane that gates actions. - The fallback. Set
TYPESAFE_DEFAULT_MODEL=jev-1.13.0in every lane’s environment, so a call that forgetsmodellands on the pin. Both SDKs read it. - The client. Lock
typesafe-sdkand@typesafe-ai/sdkto exact versions and record them insdk. A breaking client release changes the wire without touching the model: 0.6.0 madeScore.criteriaan ordered sequence.
Then fail the build on a moving name:
# illustrative CI check: no moving Jev names in lane config
if rg -n '(jev-latest|jev-preview|~typesafe/jev-latest)' lanes/ config/; then
echo "moving Jev alias in lane config; pin jev-1.13.0" >&2
exit 1
fi
TypeSafe has published no model change since launch; the code that calls it has changed four times. Dates from TypeSafe’s docs and SDK changelog, PyPI, Vercel, OpenRouter and Kimi, read Sep 21, 2026.
From Sep 11 to today TypeSafe published no model change, while the Python client shipped four releases, two of them breaking. The next change might be the model, and an unchanged name won’t warn you.
Step 6: Replay policy over stored answers; re-evaluate only on purpose
Audits go wrong when these two operations are treated as one.
Policy replay reruns your code over the stored answers. Tighten the destructive threshold, replay last month, and you get the exact calls the new table would have handled differently. No Jev call happens, so the result is deterministic: same answers, same policy, same outcome, every run. Use it for threshold changes, incident review and proving a policy bug.
Re-evaluation sends the stored state and questions to Jev again, which draws a new sample that isn’t guaranteed to match. OpenRouter’s gate cookbook reports that identical repeats moved probabilities by up to eight hundredths, one check ranging from 0.35 to 0.43 over four runs, and “the outcome held on every repeat”. TypeSafe’s parallel-questions cookbook found most answers identical over five repeats on jev-1.12, two Nouls drifting with standard deviations under 0.01. Far from a threshold, drift is harmless; near one, it flips the outcome.
Screenshot: OpenRouter, “Gate Agent Tool Calls with Jev” (undated), captured Sep 21, 2026.
The same cookbook says to store the whole decision object with the ticket as the audit record. Add four rules:
- Never overwrite the original. A re-evaluation writes a new record with
reevaluates: <decision_id>and its ownmodel_reported. - Report re-evaluations as drift. “Jev would say 0.87 today” is a fact about today’s model, not about what the gate saw.
- Re-evaluate on the pinned version, unless you are measuring a bump (step 8).
- No state, no re-evaluation. Once the store purges a state you can replay but not re-ask, hence step 2’s retention rule.
Replay asks what your code would do. Re-evaluation asks what the model would say today. Only one of them reproduces the past.
Step 7: Plan for the pinned version’s retirement before anyone announces it
TypeSafe publishes no support window for a versioned ID, and OpenRouter doesn’t say whether jev-1.13 stays listed after a newer Jev ships. Plan as if the pin can go at short notice, beside your other model deprecations; the deprecation ledger is the shape.
- An owner and a watch. One named owner per pinned version and a weekly read of the models page.
GET /v1/modelslists aliases only, so diffing it won’t show a withdrawn version. - A daily canary. One fixed fixture, pinned ID, every door you use. Alert on an error, a changed
model_reportedor an answer outside its usual band. - A written bump procedure. Step 8, with a named approver, ready before any notice arrives.
- A fallback that isn’t the alias. If the pin fails before the bump lands, the gate takes its written fail mode, closed for destructive classes, rather than quietly riding
jev-latest.
Step 8: Re-run the shadow set on every bump
A new Jev version is a new classifier. TypeSafe says to pin once you’ve tuned thresholds and move on your own schedule, and its jaggedness notes are published per version; the current page applies to jev-1.13 and was last reviewed Sep 17. Nothing TypeSafe publishes says a 0.9 on one version means a 0.9 on the next.
So the bump runs in shadow, the way the shadow ladder runs any Jev gate before it enforces:
- Build the set from your records. Every deny, every destructive-class call, every answer within 0.1 of a threshold, plus a random sample.
- Re-evaluate on the new version, each result a linked record with the new
model_reported. - Replay the current policy over the new answers and count flips per tool class, both directions.
- Retune where flips cluster, then change
policy_versionandmodel_requestedin one commit, so no record carries a new model under an old table. - Shadow live traffic for a fixed period, then switch. The old pin stays the rollback until sign-off, while the vendor still serves it.
Jev decision log failure modes, and the signal for each
The alias that crept back. A new lane copies an example using jev-latest. Signal: model_requested values other than your pin, or null. Fix: step 5’s CI check and a required model argument in the gate wrapper.
A hash with nothing behind it. State retention is shorter than the audit window. Signal: a weekly fetch of twenty random state_ref values comes back with misses. Fix: align retention per lane.
Same key, new question. Someone rewords destructive and leaves version 7. Signal: question_sha256 changes while question_set doesn’t. Fix: CI refuses the merge.
Re-evaluation passed off as replay. An incident review calls Jev again and quotes today’s probability as the gate’s. Signal: review evidence whose request IDs postdate the incident. Fix: replay tooling with no Jev credentials.
Records only on the happy path. Timeouts and 429s write nothing. Signal: gated tool calls in transcripts with no matching tool_call_id. Fix: write the record in a finally block, path included.
An unversioned door carrying a tuned gate. Signal: version_known: false on a lane whose policy names a version. Fix: move the gate to a door that reports one, or retune there and label the risk.
Decision records belong to the fleet, not to the gate that wrote them
One gate’s log answers one gate’s questions. The questions after an incident are fleet questions: which lanes ran jev-1.13.0 last Tuesday, which sat on a door that couldn’t say, how many decisions a threshold change would flip across every agent and CLI, and what each session was doing when its gate fired. That join from decision to tool call to session lives above any single harness. Fleet replay covers the transcripts; the decision log is the typed index into them.
Keeping that index is operating-layer work: one inventory of which model, door and policy version each lane runs, and one place the records land and outlive the session that wrote them. The classifier hands you typed answers. Agentic operations turns them into evidence someone can use a month later.
FAQ
Should I use jev-latest in production?
Not on a lane that gates actions. TypeSafe says an alias moves when a new release ships, so answers can change with no change on your side, and it advises pinning the versioned ID once you tune thresholds. Send jev-1.13.0, set TYPESAFE_DEFAULT_MODEL to match, and move deliberately.
Does Jev return the same answer for the same input?
Mostly, not always. OpenRouter saw one probability range from 0.35 to 0.43 across four identical calls with the outcome unchanged, and TypeSafe’s own repeats showed small drift on a few Nouls. Treat a repeat call as a new sample, and replay stored answers when you need the original decision.
Sources
- TypeSafe: Models — aliases, pinning advice, context budgets
- TypeSafe: API reference — keyed questions and answers, response
model - TypeSafe: Confidence — Noul carries no confidence
- TypeSafe JS SDK: TypeSafeClientConfig — default model, debug logging
- TypeSafe Python SDK changelog — 0.6.0, 0.7.0, 0.7.1
- OpenRouter: TypeSafe SDK integration — model-ID routing, dated IDs
- OpenRouter: Gate Agent Tool Calls with Jev — repeat-call drift
- Vercel: TypeSafe-compatible API —
typesafe-ai/jev - Kimi Code: What’s new — K2.8 Preview, Sep 11, 2026
- TypeSafe: Parallel questions cookbook — repeat stability on
jev-1.12
