Put the judgments to a model#
Let Jev judge the prompt#
The prompt hook makes three judgments locally. A cross-encoder picks
the claims that bear on the prompt. Two phrase lists spot a correction
and a choice put to the agent. Jev, TypeSafe’s decision model, can answer
all three in one call, and a fourth route, review, weighs a due claim.
It stays off unless the machine turns it on, because the call sends the
prompt and up to ten candidate claims off the machine. When the judge
does not answer, the hook reranks with the local cross-encoder. A
correction the hook stores is labelled with whose prompt said it, and a
pasted or quoted prompt stores none. A standing choice the person states (“from now on”,
“always”, “never”) is saved in their words, outside code and quotes.
The same call asks two more questions, since a call costs its input
tokens and each question adds about 70 of them to a median of 933. An
injection question asks whether quoted or pasted text addresses the
agent with instructions the person did not write; at cue_at the hook
adds a note telling the agent to treat that text as data. An effort
question scores the prompt from 0 (a lookup) to 3 (a design or a hard
debug); it changes nothing in the hook and goes to
$XDG_STATE_HOME/ljos/jev-log.jsonl as the record a router is scored on.
Keep the key in pass and write the setting:
$ pass insert api/typesafe/jev
$ cat > ~/.config/ljos/jev.toml <<'TOML'
enabled = true
key_cmd = ["pass", "show", "api/typesafe/jev"]
TOML
$ ljos doctor --all | grep jev
ok jev on jev jev-1.13.0 0 calls, 0 cached $0.0000 of $4.00 this month
The hook runs the key command once per login, with no terminal and
three seconds to answer, and keeps the key in
$XDG_RUNTIME_DIR/ljos/jev-key (mode 0600). key_file names a file
instead.
The call goes out only where it can change the answer:
check |
default |
skipped prompt |
|---|---|---|
|
4 |
“yes”, “keep going” |
|
2 |
fewer claims than a choice needs |
|
4.0 |
any, once the month’s spend reaches it |
A skipped prompt gets the lexical search and the phrase lists. With Jev on, the hook never loads the cross-encoder, so its memory stays free. A prompt past 2,000 characters goes out cut to that length.
A prompt with ten candidates takes about 900 input tokens. At $0.042 per
million input tokens, with output free, that arithmetic is a prompt cost
of $0.00004. The
hook records each call’s tokens at usd_per_mtok_in, or the API’s own
figure when OpenRouter sends one, in $XDG_STATE_HOME/ljos/jev-cost.toml.
For OpenRouter, set endpoint = "https://openrouter.ai/api/alpha/decisions"
and model = "typesafe/jev-1.13".
Jev answers each question with a probability. bears_at (0.5) is the
one at which a claim goes in, and cue_at (0.5) the one at which a
prompt counts as a correction or a choice. Raise bears_at to 0.7 when
off-topic claims come through.
Jev keeps no cache of its own, so ljos keeps one. An identical request
within cache_days (7) is answered from $XDG_CACHE_HOME/ljos/jev at no
cost, and the doctor row counts those answers as cached. A panel asked
twice, a retried ballot and a repeated prompt with the same claims all
hit it. cache_days = 0 turns it off.
Two or more judges can answer side by side. Each [judges.NAME] table
names a judge. [route] says which judges answer each decision. Here a chat
model on an compatible endpoint judges the prompt, Jev and that
model pool their ballots, and the chat model reviews due claims:
enabled = true
key_file = "~/.config/ljos/jev.key" # the default judge: Jev
[judges.chat]
backend = "chat"
endpoint = "https://llm.example.org/v1"
model = "model-b"
key_env = "LLM_API_KEY"
budget_ms = 6000
[route]
prompt = ["chat"]
ballot = ["default", "chat"]
audit = ["chat"]
review = ["chat"]
A decision with two or more judges is answered by their pool. A
probability is the weighted mean of the judges’ log-odds. Two judges at
0.8 stay at 0.8, and judges at 0.9 and 0.1 meet at 0.5. A choice is
the normalised weighted geometric mean of their distributions, and a
score the weighted mean. weight in a judge’s table moves its share. A
question no judge answered is missing from the pool, so the caller falls
back as it does on a partial reply. A caller with no judge at all takes
its local path on any of these: no file, enabled = false, no key, an
error, or no answer inside budget_ms (2000). The log keeps every judge’s answer
beside the pool, so each judge can be scored once outcomes are known.
These judges are fast and calibrated, and they do not reason. A decision they leave open goes to a persona that does: a persona with a runner reasons in a session of its own.
$ ljos persona buildengineer --anchor 0.25 --view "Runs production pipelines." --runner NAME
$ ljos vote surf-ab12 --as buildengineer --jev
buildengineer: Jev leaned A at confidence 0.61, under the 0.80 cut; not cast.
buildengineer reasons in its own session in herdr pane w1:p2; then `ljos consensus surf-ab12`
The runner is any coding runner the machine runs: the terminal harness, the command harness, the build harness, omp, hermes or Antigravity’s agy.
Each runs on whatever model it is set to, and switching one to model-b on an compatible endpoint changes only the model it reasons with.
It is a [[harness]] in ~/.config/ljos/harnesses.toml.
Its start and resume argv say how it opens a session and how it resumes the latest one in a directory, for example the resume argv the harness table names.
It starts in a pane the person can watch and talk in: a herdr workspace when herdr’s server is up, else a window of the tmux session ljos-personas.
A [[tool]] the runners file declares is tried before either (see Open persona panes in another tool).
A runner that exits non-zero is resumed in its pane, at most three times a minute.
It works in the persona’s home, $XDG_STATE_HOME/ljos/personas/NAME, as the seat named after the persona.
Its memories, ballots and trust rows are therefore the persona’s.
It reads the issue and the pack, writes its reasoning with ljos note, and casts ljos vote --as NAME.
Each runner continues the latest session of the directory it starts in, so the next hand-off resumes the same conversation, and an open pane takes the next task in place.
A task goes to the persona’s inbox, and the pane is told the file in one line.
ljos ask NAME "..." puts a question to the persona the same way.
ljos supervise NAME --harness RUNNER --exit crash --at SECONDS says whether that child should be started again, under the restart policy (permanent, transient, temporary, three strikes in a minute), and prints the herdr line when the answer is yes.
It does not start the pane.
ljos panel --jev hands each unsure persona’s brief to its own session when the panel splits.
A persona with no runner gets a brief for a subagent, as before.
ljos due --judge puts the due page to the review judges. Each claim
goes with the newer claims the pack keeps on the same subject. A claim
the pool puts at 0.9 or more is graded recalled. One at 0.1 or less
is printed as contradicted, for the agent to supersede or withdraw, and
stays due. Everything between stays due. No judge lapses a claim,
because a lapse says that a reader forgot it.
Put the judgments to a local model or a runner#
The same four judgments go to any judge that answers in Jev’s shape.
backend in jev.toml picks it; the parsers, the cache, the ledger and
the doctor row are shared, and every fallback above still applies.
A chat-completions endpoint, hosted or a local llama-server, takes one
JSON-mode chat completion per judgment set. endpoint is the base URL
and model the name the server knows. A local server needs no key;
usd_per_mtok_in = 0 keeps the ledger exact for one that is free.
enabled = true
backend = "chat"
endpoint = "http://127.0.0.1:8089/v1"
model = "model-c"
usd_per_mtok_in = 0
budget_ms = 6000
A runner on the machine is the judge through command: an argv that
reads the request JSON on stdin (state, and questions, each with a
type of noul or choice, instructions and criteria) and prints
{"answers": {...}} on stdout inside budget_ms. A noul answer is
{"noul": p} or a bare number; a choice answer is {"choice": KEY, "confidence": p, "probabilities": {...}}, and a bare key means a
sure one. A question with no answer refuses the whole reply, so the hook
takes its local path rather than half a judgment.
enabled = true
backend = "command"
command = ["/path/to/judge.sh"]
budget_ms = 20000
The hook budget bounds the prompt path, so a harness that takes seconds
suits ballots and the stop audit more than the prompt hook; min_words
and min_candidates keep the prompt calls few. The log carries the
backend name, and ljos doctor prints it before the model.
Cast persona ballots through Jev#
With Jev on, a persona’s ballot can come from Jev instead of a subagent.
The issue needs an Options: A, B line.
ljos file "TITLE" --options A,B writes that line as the first line of the body.
ljos options ISSUE A,B appends it to an issue that already exists, and refuses fewer than two.
$ ljos vote ljos-abcd --as security-reviewer --jev
judge:jev-1.13.0: cast age for security-reviewer at confidence 0.96
$ ljos panel ljos-abcd --jev
3 personas on ljos-abcd through Jev: all sure, all age; one judge ballot cast
Jev reads the persona’s brief and answers two choice questions: the
persona’s vote, and what the rest of the panel will pick. The option
order is shuffled per persona. A cast ballot is recorded as
judge:MODEL, not as the persona, and --used names that judge. The
chosen option’s probability is the ballot’s confidence. The forecast is
the judge’s prediction, and consensus does not read it. A note on
the issue says the ballot came from Jev. learn updates the trust row
whose to is that judge.
Jev’s confidence is (K·p_max − 1)/(K − 1), the same cut on every
backend. Under escalate_below (0.8), vote --jev does not cast. It
notes Jev’s lean. A persona with a runner gets the ballot in its own
session; for any other, the command names the brief to start a subagent
from.
A panel casts only when every seated persona is sure and all agree. An all-one-model panel is one voter, so it casts one ballot. A split or unsure panel casts nothing and writes a brief per seat for subagents. The metered model then spends only on the contested questions. A ballot costs about 2,000 input tokens, or $0.00008.
Every Jev answer goes to $XDG_STATE_HOME/ljos/jev-log.jsonl, with the
claim ids, a hash of the prompt, the latency, a timeout or a failure,
and the backend and model. The file does not keep the prompt or the
claim text. ljos judge-score joins that log with outcomes and reports
accuracy and calibration per decision. ljos learn scores a cast
ballot’s confidence as it does any voter’s.