A deeper guide to how the agent works, how to tune it, and how the benchmark measures it.
The diagram source is docs/diagrams/flow.html. To re-render it after
editing, take a 2x screenshot of the page (for example with headless Chrome and
--force-device-scale-factor=2) and save it as assets/flow.png.
The standard agent used for comparison skips steps 1 to 3: it receives every database in the catalog and decides by itself.
Jev is an evaluation model: it does not generate text, it answers typed questions (boolean,
choice, score) about a shared state with calibrated probabilities. The router asks it
one boolean question per candidate in a single call (src/router.ts).
Booleans are used instead of a single choice on purpose. Several databases overlap (for
example two death registries), and a choice would split the probability between them so
that none clears the threshold. Independent booleans let two databases both score high, and
both are passed to the executor.
| Knob | Effect of raising it | Effect of lowering it |
|---|---|---|
router.databaseThreshold |
fewer databases, fewer tokens, more "no data" misses | more databases, more tokens |
router.tableThreshold |
fewer preloaded tables | more columns in the prompt |
router.maxPreloadedTables |
more context for the executor | smaller prompt |
Without a cap, table routing once preloaded 27 tables and pushed a prompt to 137K tokens, more than the standard agent. Keep the cap.
catalog.json is built by pnpm catalog (scripts/build-catalog.ts) from the live
Postgres instance: database comments, and every table in every schema with its comment.
Tables matching catalog.descriptionIgnore (migrations, shared lookup tables) are left out
of the descriptions. Which databases are included comes from postgres.databases (an
allowlist) or postgres.exclude.
The description of each database is the only thing Jev reads to decide, so it is the strongest lever on accuracy. Rewrite weak descriptions by hand; rebuilding the catalog keeps your edits.
- Every query runs in
BEGIN READ ONLYwith astatement_timeout(15 s by default) and is always rolled back (src/db.ts). - The executor can only target the databases chosen by the router: the
databaseargument of its tools is a closed enum. - At most 100 rows are returned to the model per query.
pnpm dev serves the UI on port 3000 (override with PORT).
/: type a question and both agents answer in parallel. Each card shows the routing decision, the answer, every SQL query with its row count or error, the time spent in each stage, and input and output tokens./?q=...: runs the comparison on load, handy for sharing a question.
| File | Purpose |
|---|---|
benchmark.questions in config.yaml |
the questions, written like a user would ask them |
benchmark.answers in config.yaml |
gold answers: verified reference SQL, expected values, accepted alternatives |
bench/run.ts |
runs every question in both modes, records latency, tokens and SQL |
bench/grade.ts |
grades each answer and writes a report, a summary and charts |
bench/refresh-answers.ts |
re-runs the gold SQL to detect data changes |
Write two JSON files and point benchmark.questions and benchmark.answers at them. Every
question id needs a matching answer.
questions.json: what a user would ask. Write them before looking at the schemas, so they
include questions your data cannot answer: that is how hallucinations get measured.
[
{ "id": 1, "category": "count", "question": "How many customers signed up in 2024?" },
{ "id": 2, "category": "out_of_scope", "question": "What is the weather today?" }
]answers.json: the correct answer for each question.
[
{
"id": 1,
"answerable": true,
"database": "sales",
"goldSql": "SELECT count(*) FROM customers WHERE extract(year FROM created_at) = 2024",
"expectedValues": [1834],
"expectedAnswer": "1,834 customers signed up in 2024",
"acceptable": [],
"notes": "Counts by signup date, not first purchase"
},
{
"id": 2,
"answerable": false,
"expectedValues": [],
"expectedAnswer": "Not available: there is no weather data",
"acceptable": []
}
]| Field | Meaning |
|---|---|
answerable |
false when the data cannot answer the question; the agent should say so |
goldSql, database |
a query that produces the answer; pnpm bench:refresh re-runs it |
expectedValues |
numbers or names that must appear in a correct answer |
expectedAnswer |
the correct answer in one sentence, read by the judge |
acceptable |
other answers that also count as correct (ambiguous questions) |
category |
free text; the report and charts group accuracy by it |
Run pnpm bench:refresh after writing the answers: it checks that every goldSql still
returns its expectedValues.
Each answer gets two independent signals:
- Numeric check: the expected values must appear in the answer, tolerant to rounding
and to both
1,234.5and1.234,5formats. - Jev as judge: a boolean question comparing the answer with the expected and accepted answers. For unanswerable questions, it checks that the agent says the data is not available instead of inventing figures.
When the two signals disagree the answer is marked review instead of guessing.
From the UI at /bench, pick 5, 20 or 100 questions and press Run. From the CLI:
pnpm bench -- --limit 20 --concurrency 3
pnpm bench -- --resume bench/results/<run>.jsonl # retry failed runs only
pnpm bench:grade -- --results bench/results/<run>.jsonlResults go to bench/results/ (ignored by git): the raw runs, the graded runs, a Markdown
report, a JSON summary for the UI, and the charts as SVG. The UI can export the report to PDF
and each chart to PNG.
Set benchmark.watermark in config.yaml to a png/svg path to stamp every chart with a small
logo in the top-right corner. Leave it unset for unwatermarked charts. pnpm bench:png -- <charts dir> renders every SVG in a folder to PNG at 2x
using a local headless Chrome/Chromium, useful for sharing charts where SVG is not supported.
Use --label on pnpm bench to name the series shown in the report and charts, instead of the
default mode name. This is how separate runs (e.g. different agent variants) get compared side
by side after merging their result files with bench:grade --results a.jsonl,b.jsonl,c.jsonl:
pnpm bench -- --mode standard --label Sol
pnpm bench -- --mode jev --label "Sol + Jev"
pnpm bench -- --mode jev --config bench/luna.config.yaml --label "Luna + Jev"Every run records what it cost, and bench:grade writes <run>.costs.csv with one row per
question and mode:
- Executor: the billed cost that OpenRouter returns in each response (
usage.cost), summed over every step of the tool loop. OpenAI's API does not report cost, so it is empty with theopenai:provider. - Jev: tokens and the list price (
marketCost) that AI Gateway reports for both routing calls. The judge calls made while grading are not included.
The report adds total cost, cost per question and cost per correct answer, which is the fairest comparison when the two modes differ in accuracy.
- Latency includes the AI SDK's internal retries, so gateway outages inflate the numbers, mostly the p95. The report also shows Jev's time inside the provider (from the gateway's own timestamps), which is what a self-hosted Jev would cost in latency.
- The standard agent sends about 75K tokens per question and can hit per-minute rate limits
under concurrency. Failed runs are excluded from the metrics and can be retried with
--resume.
jev-bon mode routes the question with Jev exactly like jev mode, then generates N
candidate answers with the executor in parallel instead of one, and lets Jev pick the best.
- Route (
src/router.ts): same database and table routing asjevmode. If no database is selected,jev-bonreturns the same "no data" answer asjevmode and skips candidate generation entirely. - Generate (
src/pipeline.ts):selector.candidatescalls toanswerQuestionrun withPromise.allSettled. Candidate 0 uses the executor's normal settings; the rest useselector.temperatureand rotate through hints that nudge them toward different readings of the question (default period, literal interpretation, official totals vs. sums). Failed candidates are dropped; if every candidate fails, the first error is thrown. - Select (
src/selector.ts): consensus first, for free. If a strict majority of candidates agree on the same top 3 numbers (rounded to 3 significant digits), or a majority agree there is no data, that candidate wins with no Jev call. Otherwise Jev answers onechoicequestion over the candidates (shuffled, so label order carries no signal), each described by its answer text, final SQL and a 5-row preview of its last successful query.
Cost and tokens in the benchmark and comparison UI are the sum of every candidate's executor
usage plus the selector's Jev usage (reported separately under jev.selector), so jev-bon
is directly comparable to jev and standard on cost per correct answer.
selector:
candidates: 4 # number of candidate answers generated in parallel
temperature: 0.7 # sampling temperature for candidates after the firstbench/run.ts accepts --config to point at an alternate config.yaml, so jev-bon can be
benchmarked with a different executor model without touching the default config:
pnpm bench -- --mode jev-bon --config config.luna.yaml \
--ids 9,17,31,33,55,74,79,92,54,51,1,2,3,4,5,6,18,19,20,21The extra ids are 6 questions the existing jev run already answers correctly (1, 2, 3, 4,
5, 6) and 4 unanswerable questions it already refuses correctly (18, 19, 20, 21), picked from
bench/results/2026-09-23T23-22-35-970Z.graded.jsonl. They give the comparison a floor: if
jev-bon regresses on questions jev already gets right, best-of-N is not paying for itself.
bench:grade accepts a comma-separated --results list to merge the new run with the
existing one before grading, and --out to name the merged output:
pnpm bench:grade -- \
--results bench/results/<jev-bon-run>.jsonl,bench/results/2026-09-23T23-22-35-970Z.jsonl \
--out jev-bon-vs-jevIf the merged files used different executor models for the same mode, the report and charts
split that mode into one series per model, labeled ${mode} (${model}).
