defineEval is the main entry point for writing Evals. Call it once per Eval file, pass a description and test(t), and default-export the result.
For how to organize business criteria, evidence, and points, see NiceEval Taste.
Do not provide
id or name. NiceEval generates the Eval ID from the file path.defineEval options
description
niceeval list and the view; purely informational, it does not affect scheduling or scoring.
tags
--tag filtering and view categorization; a selection dimension independent of id-prefix filtering.
sandbox
plugins
judge
timeoutMs
metadata
diff
ignore adds exclusions on top of the default list; include has the highest priority and explicitly adds matching paths back.
The composition rule is fixed as “default ∪ ignore, then punched through by include”, and the list is frozen at the ledger anchor.
test
Test context: t
t.note(label, value) records a JSON value or Quantity for review, such as t.note("Latency", ms(512)).
It registers no Assertion and does not affect Verdict, score, or coverage. null is displayed as unavailable.
Labels must be unique within an Attempt. The limits are 64 notes and 64 KiB of JSON per note; violations throw TypeError.
show @<locator> and View display Notes after Assertions; JSON and Query Attempt results include notes.
show --exp <experiment-id> groups numeric notes by label and unit, displaying count, min, median, p90, and max.
t (TestContext) is the context an Eval author receives in test(t). Each call to a check method registers one assertion.
t.check(subject, match)checks a value you pass explicitly against a match condition.t.check(contextualMatch)takes no value; NiceEval hands the current read-only ctx to the match condition to read.t.judge(material, match)gives material to a Judge for scoring, through the same registration path ast.check.
judge on the root, a Session, and a Turn all require material passed explicitly. NiceEval ships four ready-made judges, which also build a match condition and hand it to check. closeQA(selector, question) first selects a set of material with the selector, then has the Judge answer an acceptance question. Agent scenarios also have closeQA(question), which reads the current scope’s full history by default; you can also pass an EventMatch or ToolMatch to look at only part of the facts.
How an assertion affects the result by default depends on the Eval type:
- In a pass/fail Eval, Boolean assertions decide the Attempt’s Verdict by default; in a scored Eval, Boolean assertions only record by default and do not affect the result.
- Scoring assertions (measurements) do not fail the Eval by default. In both Eval types, after adding
.gate(minimum), a score below the minimum counts as a failure. In a scored Eval, the same assertion can have both.gate()and.score(). .orStop()stops later steps when the assertion fails. A Boolean assertion uses its own result; a measurement writes.orStop(minimum), or a parameterless.orStop()after.gate(minimum)to reuse the gate’s minimum.
t.score(ratio, { weight: 50 }).label("Completion time"). ratio is a number between 0 and 1, and weight is a non-negative finite number. The result keeps the ratio, the corresponding points, and the actual score. When the numeric material it depends on is unavailable, the item’s score is recorded as unknown.
The ScoreTestContext of defineScoreEval also provides t.score(n), which registers a score item directly; existing assertions count toward the score with .score(n). In both places n must be a finite number not less than zero, and 0 is also an explicit score. For tool call matching and counting rules, see Match tool calls. All members are:
simulatedUser
note
evaluationKind
send
sendFile
requireInputRequest
respond
respondAll
reply
sessionId
events
eventOccurrences
newSession
signal
model
reasoningEffort
flags
progress
diagnostic
log
skip
group
check
agentJudge
judge
toolCalls
sandbox
observability
usage
succeeded
toolOrder
eventOrder
usedNoTools
maxToolCalls
noFailedActions
event
notEvent
elapsedMs
maxTokens
maxCost
closeQA
closeQA
closeQA
Judge scoring
First declare a rubric withdefineJudge, then pass it directly to t.check or t.judge:
judgeImage(); a snapshot is saved at registration.
Which model the Judge uses is decided in order: first the project config’s judgeProvider, then the Eval’s judge, and finally the Experiment’s judge. When a later layer gives a complete Provider, it replaces the service and execution configuration entirely; when it gives only a model string, it replaces just the model of the selected Provider. An Eval’s judge only decides which model or Provider this Eval uses; it does not restrict which match conditions can be used.
Both pass/fail and scored Evals can call .gate(minimum). Scored Evals can also call .score(n), and the item keeps its score when the gate fails.
Binary judges
When you only need to decide whether a condition is satisfied, importbinaryJudge from niceeval/expect:
binaryJudge(options: BinaryJudgeOptions) returns an ordinary ScoreMatch. BinaryJudgeOptions is JudgeOptions without anchors:
rubric is required; maxCalls and maxMaterialBytes are optional. Validation and budgets are the same as for defineJudge.
It classifies only as satisfied or not-satisfied, yielding 1 or 0 respectively. As a result, .score(n) awards either full points or zero; use .gate(1) when the condition must be satisfied.
Invalid output, insufficient budget, or service failures retain their errored or unavailable state and do not count as zero. The audit keeps the category, reason, and mapped result for review with show @<locator> --assertions.
Image material
ImportjudgeImage from niceeval/judge; it copies the original image data immediately. The limits are:
- Only PNG and JPEG formats are supported.
- Each image is at most 4 MiB and 16,777,216 pixels.
- Each assertion and each model request can reference images at most 4 times, totaling no more than 8 MiB. Referencing the same image again also counts.
- Each Attempt saves at most 32 MiB of images.
OpenAIProvider, VercelProvider, and OpenRouterProvider do not send images by default. Set supportsImages: true explicitly and choose a model that supports both images and tool calls. TypesafeProvider does not support images. When images are unsupported, the result is judge-capability-unavailable; images are never swapped for a text description before scoring.
judgeImage
JudgeImageInput
body
mediaType
Ready-made judges and custom Matches
defineJudge and the ready-made judges all return a ScoreMatch, which you can pass directly to t.check. t.judge is just shorthand for the same path, so registration, snapshot, budget, scoring, audit, and result storage each happen only once.
For example, using an
instructionFollowing() instance directly:
faithfulness first extracts facts from the output, judges them one by one, and finally computes the ratio in code. When extraction is incomplete there is no valid score. TypeSafe does not support extract, so on TypesafeProvider the result is unavailable; it is never switched to estimating one overall score.
pairwisePreference compares once in a fixed order and suits a comparison between one candidate and one reference.
closeQA gives all selected material to the Judge at once in original order, and the Judge answers the acceptance question based only on that material.
Agent scenarios can use turn.closeQA(question), session.closeQA(question), and t.closeQA(question). Passing an EventMatch looks only at the matching events; passing a ToolMatch looks at the complete information of the matching tool calls.
The state of the material affects the result: when the selected material is complete but empty, the score is 0 and the model is not called; when the material is incomplete, undecidable, or over the limit, the result is unavailable, and it is never truncated before scoring.
When you need to combine several model steps, write your own scoring logic with score(value, context) in defineScoreMatch. context.llm provides score, classify, extract, and batchClassify, all returning an Effect.
They share the call budget, timeout settings, and audit records with the ready-made judges.
- Static configuration validation sends no network requests. No request is sent when the Provider, model, or key is missing either.
- Chat-style services use forced-function requests, and TypeSafe uses the
/systemoneendpoint; both validate responses strictly. - Network transport errors or timeouts make the result unavailable; HTTP 400 or protocol incompatibility makes it errored.
The Turn return type
t.send(...) returns a TurnHandle. It contains common fields derived from the event stream, plus a full set of assertions scoped to this turn only.
calledTool and notCalledTool are also assertions on the Turn; for matching and counting rules, see Match tool calls. A Turn checks only the current turn, a Session checks all turns up to the call site, and root t checks the prefix of each Session already started when called. Later calls do not enter an already registered assertion.
turn.inputRequests is a read-only, deeply frozen projection of every input.requested request in this turn’s events, in event-stream order; it is [] when there are no requests.
Use it to read any number of requests; the existing requireInputRequest still requires exactly one matching request.
input
events
inputRequests
toolCalls
eventOccurrences
status
reply
data
usage
check
agentJudge
judge
succeeded
toolOrder
usedNoTools
maxToolCalls
noFailedActions
event
notEvent
eventOrder
elapsedMs
maxTokens
maxCost
closeQA
closeQA
closeQA
Test set exports
file/0000, file/0001, and so on.
When you already have stable business keys, you can also default-export a Record<string, EvalDef>. For example, the key 15193 in swelancer.eval.ts produces swelancer/15193. A key must be a non-empty single path segment; it cannot be . or .., and cannot contain /, \\, or control characters. Evals are ordered by key in lexicographic order.
Reading material from ctx, and official usage accounting
defineMaterialMatch<C,T>({ name, read, match, capture? }) defines a set of material read from ctx; import it from niceeval or niceeval/expect. The Adapter’s create provides the application’s own ctx. When registering in check or closeQA, NiceEval passes the current read-only ctx to read.
read returns { state: "complete", items: [{ id, value }] }, or a partial or unavailable state with a reason. Each item’s id must be unique within the set, at most 128 UTF-8 bytes.
capture controls how much material is saved: by default at most 256 items and 48 KiB, adjustable up to 16384 items and 4 MiB.
check(materialMatch) returns a Boolean assertion that by default passes when at least one item satisfies the condition. closeQA(materialMatch, question, options?) returns a Judge-scored assertion: question cannot be empty and is at most 8 KiB; options is of type JudgePresetOptions. All selected material is given to the Judge at once in original order. When the material is complete but empty, the score is 0 and the model is not called.
In Agent scenarios, read reads the tool calls (toolCalls) or events (eventOccurrences) NiceEval has organized from AgentMatchContext<Scope>, then matches them with a ToolMatch or EventMatch respectively.
defineContextMatch<C,T>({ name, read, match, capture? }) reads a single small value from ctx. read returns { state: "available", value } or { state: "unavailable", reason }. Paired with a BooleanMatch it yields a Boolean assertion; paired with a ScoreMatch it yields a scoring assertion. When the value is unavailable, the scoring logic is not called.
t.usage reads the usage officially recorded by NiceEval, fixed at the moment it is read. Failed and retried calls, cache buckets, and the basis for cost calculation are all kept.
- For an ordinary Adapter, the usage basis is every recorded call (
recorded-calls). - For an Agent, the usage basis is the numbers reported on each send (
reported-sends), viewable per Turn, Session, or Attempt. - Fields such as
usage.totalTokensare NumericMaterial. Unknown values stay unknown and are never filled in as 0.
t.maxTokens(max) limits total tokens. t.maxCost(usd) limits by USD cost: it prefers the actual charged amount, and a charge of 0 also counts; only when there is no actual charge does it use an estimate computed from prices you configured explicitly.
t.elapsedMs is the milliseconds elapsed from the start of the Attempt to the moment it is read, and can be passed to t.check(..., atMost(limit)). The business moment of “first completion” still has to be judged with the business’s own match condition.
Sandbox result assertions
These methods check workspace changes the agent produces between the endpoints of each send window. They return post-run assertion handles that support only key, label, group, optional, and gate configuration.changedPaths(["src/a.ts"]) requires exactly that file to change; changedOnlyWithin(["src"]) allows no changes or changes to only some files under src. The latter matches path components, so agents does not cover agents-old. Provide at least one scope; use noChanges() instead of an empty array.
Paths containing * or ? throw TypeError: globs are not supported, so write the directory name directly for a directory prefix.
unchangedWithin(["agents", "niceeval.config.ts"]) protects the run configuration while allowing changes to other files; it is not the logical inverse of changedOnlyWithin. The same path-component matching and rejection rules for globs and empty arrays apply as for allowed-scope checks.
For checks concerned only with file paths, use { content: "none" } with either method. It compares the regular expression only with agent-attributed changed paths and does not read changed lines. Binary or oversized content elision does not affect the result; failed or incomplete attribution collection still yields unavailable. The default both also scans changed lines, so elided selected content can leave the result undecidable.
inDiff does not scan unchanged context. When there is no match and the selected content is elided, the result is unavailable; a match in a path or known changed line still establishes that the condition is satisfied. Match evidence keeps the first matching path and a bounded preview. To match only content, choose a pattern that avoids matching paths.
These assertions follow diff.ignore and send-window attribution. Fixtures written by the author do not count as agent changes. Restoring the original state within one send leaves no endpoint change; changing and restoring across different sends still preserves the changes. Failed or incomplete material collection yields unavailable. When content is elided, only matches that depend on that content become undecidable; they are never reported as matched.