Evals for agents and workflows in one call: run each case, check it, print a summary, fail when a check misses or drops more than allowed.
bun add @corbits/evalsBun only (>=1.2.0).
import { mentions, runEvaluation, under } from "@corbits/evals";
const report = await runEvaluation({
name: "refund-agent",
task: refundAgent, // an @intx/agent Agent, workflow(), or (input) => output
data: [{ name: "duplicate charge", input: "I was charged twice" }],
scorers: {
refunds: mentions(["refund"]),
readyToSend: "Is this reply ready to send to the customer?",
fast: under({ ms: 5_000 }),
},
allow: { readyToSend: 1 },
compare: "evals/refund.last.json",
about: { model: "qwen2.5:7b" },
});
console.log(report.markdown());scorers maps a name to a check. Without scorers, one matches check
compares output with expectedOutput. Every check gets the run
r = { input, output, expectedOutput, steps, ms }; output is whatever the run
returned.
- Code:
(r) => boolean | number, sync or async. A number is 0..1 and passes only at 1; return a boolean to pick your own threshold, or{ pass, value, explanation }for detail. - Sentence: a yes/no question for Jev about the run. Passes at
p(yes) >=
yes, fails at <=no(0.7 / 0.3 by default), fails asunsure (p=…)in between.{ question, yes, no }sets its own bars. - Helpers:
| helper | passes when |
|---|---|
matches(field?) |
output deep-equals expectedOutput, or the same field (key or accessor) of each |
includes() |
both strings, and output contains expectedOutput after NFKC, trim, and whitespace collapse; value is 1 or 0 |
fitsSchema(schema) |
output satisfies a Standard Schema (arktype, zod, valibot) |
mentions(words) |
output contains every word, case-insensitive; value is the fraction found |
avoids(words) |
output contains none of the words |
ranSteps(names) |
tool and step names match names, order ignored; value is their F1. Agent trajectory comes from stream(). ranSteps([]) fails when steps is empty. An observed model-only run (llm steps, no tool or step names) still passes |
stepsAtMost(n) |
the run took at most n steps |
under({ ms }) |
the run finished within ms |
ask(q, { options, pass }) |
Jev picks an option named in pass |
ask(q, { options, passAt }) |
Jev picks an option; its credit (0..1) is the value and passes at passAt |
ask(q, { levels, pass }) |
Jev grades at level pass or above (levels lowest first) |
mentions and avoids also take (r) => words for per-case words.
mentions and ranSteps take { atLeast } to pass on partial credit.
ask fails as unsure when Jev's top probability is below sure
(0.6 by default; a check's own sure wins).
A check that throws, hangs past timeout, or returns anything else fails
itself closed; its siblings still run. Jev transport failures fail closed.
Jev sees { input, output, expectedOutput, steps }, and a case's string and
ask checks answer in one call. The model is not configured on systemOne().
Build it with judgeModel({ model, invokeStep, thresholds, escalate, timeout })
from @corbits/evals/workflow. model is one workflow step (or a one-step
workflow). invokeStep runs that step; its input is { state, questions }
and its output is { decisions } or { fallback: true, reason }.
systemOne({ thresholds, escalate }) only sets bars and an escalation agent.
jev is an alias of systemOne.
Thresholds: a yes/no check passes at p(yes) >= yes and fails at
p(yes) <= no (0.7 / 0.3); ask fails as unsure when its top
probability is under sure (0.6). Retune them per judge with
judgeModel({ thresholds: { yes, no, sure }, ... }) or per check as above.
Thresholds are tuned to a judge version, so pin the step agent's model
when they must stay stable.
Escalation: sure doubles as the escalation gate. Give the judge an
agent — escalate on systemOne or judgeModel, anything with
send(prompt) returning { type: "reply", reply } or
{ type: "suspended" }, e.g. an @intx/agent agent from a @corbits/*
adapter — and every unsure decision is re-asked with the exact answer
space. The reply goes through the same pass rules minus the confidence
gate, so a primary unsure can still fail after escalation. A thrown,
suspended, or off-answer-space reply fails the case closed. Validate the
escalated model per workload too: Jev's unsure says nothing about the
fallback's accuracy, and reasoning models may answer in prose the answer
space cannot absorb — those fail closed by design.
- Function:
(input, signal) => output.signalaborts ontimeout. - Agent: an
@intx/agentAgent, detected bysend. Input is sent as a message; output is the reply. When the agent hasstream(), steps are recorded from that stream (inference.doneasllm,tool.start/tool.doneastool). A policy deny emitstool.donewithouttool.start; the tool is still recorded wheninference.tool_call.endor the assistanttool_callblock names it. A reply-only send withoutstreamleavesstepsempty, which failsranSteps([]). An observed model-only run still passes. - Workflow:
workflow(definition, { invokeStep })from@corbits/evals/workflow. A definition cannot run locally without a step invoker, and the root never loads@intx/workflow, so a bare definition is refused. Each step lands inr.steps.outputpicks the result from the finished run (default: the last listed step's output),triggermaps the input to the trigger payload. Without them, type the run asworkflow<I, O>(...). The same entry exportsjudgeModelfor string andaskscorers. A code-only eval needs no judge.
repeats runs each case several times; a check passes only if every
attempt passes. concurrency (default 4) bounds cases in flight;
timeout ("60s" or ms) bounds each run and each check.
An array of { name?, input, expectedOutput?, must?, skip? }, or the path of a
JSONL file of the same objects. The ground-truth key is expectedOutput.
expect and expected throw. name keys the case in reports and
compared runs and defaults to the input.
await runEvaluation({
name: "judge",
task: run,
data, // a case can say { must: ["tests"], skip: ["docs"] }
scorers,
compare: "evals/judge.last.json", // read: measure drops against it
save: "evals/judge.last.json", // write: store this experiment, pass or fail
allow: {
tests: { miss: 1, drop: 0 },
docs: { miss: 5, drop: 2 },
focused: 3, // { miss: 3 }
},
});Per check, allow sets (default 0):
- miss: cases that may fail.
- drop: cases that passed in the
comparerun and may fail now. A fixed case does not offset a broken one.
A case's must checks must pass whatever allow says; its skip checks
are not run or counted.
runEvaluation prints each scorer's passes, misses and drops, and rejects with
EvalFailure naming the limit each failing scorer broke and its failed
cases; .report holds every result.
An experiment is each case × scorer pass and value — plus a Jev scorer's
probability distribution and confidence — and about, no times.
Saved files are format 2: { format: 2, name, cases }. Any other format,
including format 1, is rejected.
compare only reads; a missing file skips drops. save only writes, on
every run. One path for both compares with the last experiment, then replaces
it. compare also takes an experiment from an earlier runEvaluation of the same
evaluation, with no file: runEvaluation({ ...spec, compare: before.experiment }).
Flips print as fixed / broke, new and gone cases as added /
removed. quiet: true skips the summary.
The package bin is corbits-eval (./dist/cli/index.js).
corbits-eval run ./evals/refund.ts
corbits-eval run ./evals/refund.ts --format jsonThe file is an ES module path resolved from the current working directory.
corbits-eval loads the evaluation export when it is a spec object, otherwise
the default export when that is a spec object, otherwise a zero-argument
function on either of those exports. The function must return a spec. No other
exports are read.
A spec is the object runEvaluation accepts (name, data, task,
scorers, and the rest). Case rows use expectedOutput. The CLI calls
runEvaluation; it does not score on its own.
Exit codes:
| code | when |
|---|---|
| 0 | the run passed its gate |
| 1 | the evaluation failed its gate |
| 2 | the file is missing, the import failed, there is no spec export, or the evaluation did not run |
--format json (and --json) come from incur and print only that result as JSON on stdout, with no human summary. Exit 1 is only a failed gate (EVAL_FAILED). Config that stops the run before it starts, such as empty data or a string or ask scorer with no judge.invoke, exits 2.
@corbits/evals/workflow also exports four prebuilt one-step judges. Each
takes a pinned { provider, model } and does not call a model by itself:
faithfulness, answerRelevancy, correctness, and trajectoryAccuracy.
Each returns { definition, payload, failure }. Run definition with
workflow, using payload(run) as the trigger. An outcome from payload
is the result; failure is the closed miss when the run cannot be judged.
parsePrebuilt(id, output) reads the step's category or score JSON.
judgeModel expects { decisions } or { fallback } and does not run
these rubrics. includes stays
a code check on the root package.
- Saved experiments must be format 2 (
{ format: 2, name, cases }). Format 1 is rejected. - JSONL rows use
expectedOutput.expectandexpectedthrow. ranSteps([])fails whenstepsis empty. An observed model-only run still passes.
Thresholds are per-workload starting points, not constants. calibrate
runs a labeled sample through Jev once and prints, per check, how many
verdicts agree, land unsure, or confidently disagree at each threshold:
const table = await calibrate("refund-agent", {
run: refundAgent,
cases: labeledCases, // { input, expectedOutput?, labels: { readyToSend: true } }
checks: { readyToSend: "Is this reply ready to send to the customer?" },
judge,
});
console.log(table.markdown());Yes/no checks sweep yes with no mirrored; ask sweeps sure. Paste
the recommended override into the check or judge. Zero confident
disagreements is the goal; if none exists, the fewest wins.
@corbits/evals:@intx/inferenceand@intx/types, peers of@corbits/system-one.@corbits/evals/workflow:@intx/workflow(optional).
examples/ runs against local Ollama (bun examples/<name>.ts).
examples/ports/ reimplements common eval patterns and prints agreement
with ground truth against a naive baseline: ABCDE factuality, keyword
overlap, faithfulness, pairwise battle, 1-10 helpfulness.
bun run check = typecheck + lint + format:check + unit + e2e.
Behavior lives in e2e/; src unit tests stay thin.