send returns a Turn, and its events: StreamEvent[] is this turn’s standard event stream; event assertions read their facts from it. turn.calledTool(...), turn.toolOrder(...), turn.event(...), and turn.succeeded() all judge from this turn’s events and the tool calls within them, and register an assertion directly when called. Translate what the agent did in this turn truthfully into the stream, and every event assertion works.
Turn: the return value of send
data holds this turn’s structured output. Fill it only when the application’s answer itself is a structured object, such as extraction, classification, or form filling, so an Eval author can compare it with t.check(turn.data, ...). An application that returns only text leaves data unset. Do not copy the raw response body or message text into it, and do not serialize structured output into events. Fill usage when you have it and leave it empty when you do not; do not estimate a number to fill it.
The full fields of usage (the Usage type):
inputTokens
outputTokens
cacheReadTokens
cacheCreationTokens
reasoningTokens
requests
costUSD
Turn.usage.costUSD, and is never derived from
token usage or OTel spans). It and the top-level estimatedCostUSD (a price-table estimate) are two
independent facts under a one-way field contract: this field stores only the observed value; estimatedCostUSD always equals
the estimate from estimateCost(model, usage, pricing), computed independently even when an observed value exists.
Neither overrides or backs up the other; an observed value never replaces or triggers the estimate.
How cost is calculated:Usage.costUSDrecords only the USD cost actually returned by a Provider or Adapter; it is never derived from token counts, a model catalog, or a local price table. Whether or not that value exists, the Runner separately computes an estimated cost,estimatedCostUSD, from themodel, the reported token usage, and the configured price table. The Experiment budget andt.maxCost()look at this estimate, notUsage.costUSD. Costs in reports are computed only from aPricingProfileyou specify explicitly and the recorded usage; they do not read the Runner’s estimate either.
StreamEvent variants at a glance
StreamEvent has ten variants, listed field by field below. For which assertions read them and what to watch for when using them, see the “Event table” and “Event details” sections that follow.
message
message
operation.started
operation.finished
operation.finished
skill.loaded
input.requested
thinking
context.injected
compaction
error
Event table
Event assertions have three entry points:
event(match), notEvent(match), and eventOrder(matches), all of which take a match condition built with eventMatch(...). eventMatch supports three events: message, tool operation.started, and tool operation.finished. To require a number of occurrences, use the match condition’s own methods such as .atLeast() and .exactly(). For other events, treat turn.events as an ordinary value and check it with check(turn.events, valueMatch).
Event details
message — what was said
message with role: "assistant". A tool result is not an assistant message; do not wrap tool output in message, or t.reply will read the wrong content. NiceEval records user-input message events automatically, so the Adapter does not need to emit them.
Tool operation.started / operation.finished — which tool ran and what happened
- Every tool
operation.startedneeds a tooloperation.finishedwith the sameoperationId; concurrent calls are matched up by this ID. Use the ID already in the agent’s return when there is one, such as AI SDK’stoolCallIdor Anthropic’stool_use.id; generate one by order only when there is none. - Set
statusto what actually happened:"failed"when the tool execution failed,"rejected"when a person rejected it. Eval authors useToolMatchstatus conditions to tell the two apart, so do not mix them up. - Set
nameto the tool’s original name. - With only started and no finished yet, the call’s status is
pendingand its output is recorded as unavailable. A finished event withoutoutputalso means the output is unavailable. Do not write missing output as empty JSON, and do not let it look like an ordinary mismatch.
Pass tool events to ToolMatch
The same shell tool might first run git status and then pnpm test. The tool name only says which kind of tool it is. To tell the specific commands apart, Eval authors match on the normalized command information (command projection):
ToolMatch compares one tool call at a time. For the complete filter conditions, and the JSON, path, output, and counting rules, see Scoped assertions.
When the native protocol directly provides a command’s structured argv, generate the command information with the constructor exported from niceeval/adapter:
commandProjection() keeps the original arguments the Adapter confirmed and normalizes them into the logical-command/v1 format by NiceEval’s shared rules. For example, pnpm exec niceeval view is matched exactly by commandMatch("niceeval", { argsStart: ["view"] }).
Mark the original command available (state: "available") only when the native protocol itself provides argv, or when one command can be split out unambiguously by the protocol’s grammar.
When the protocol provides only shell source, or the content is truncated or redacted, use opaqueCommandProjection(reason). When you can confirm the call is not a command, use notCommandProjection(). When you cannot tell whether it is a command, downgrade actions coverage; do not guess from the tool name, input, or shell text.
Subagent operation.started / operation.finished — who was delegated to
operationId pairing rule is the same as above. They stay in the raw event stream and reports. To check them, use an ordinary value match on turn.events; the tool matching in eventMatch(...) does not treat a subagent as a tool call.
input.requested — stop and wait for a person (HITL)
input.requested, and the Turn’s status is "waiting". t.requireInputRequest(filter) matches request field by field, so fill in as many fields as you can, or the Eval cannot select it. For how to wire it up, see the HITL section of the connection tutorial.
thinking / compaction / error
compaction mostly comes from coding agent CLIs, which compact automatically when the context fills up. These events are mainly for reports and diagnostics; when you need to assert on them, pass turn.events to check as an ordinary value.
Three rules for mapping events
- Keep the real order: event order is the order things happened.
toolOrderandeventOrdermatch different calls from front to back, so the wrong order gives the wrong assertion result.eventOrdercan only order events thateventMatchsupports. - Pair with
operationId: every started needs a finished with the samekindand ID. Only with complete pairs can NiceEval attribute status and input to the same tool call. - Declare completeness honestly: official converters declare event coverage (evidence coverage) according to what the Adapter can actually do. When the relevant input or action coverage is incomplete, a negative assertion (such as “did not call a tool”) results in
unavailable; “not seen” is never treated as “did not happen.” Manual mappings must also declare coverage honestly; see the Capabilities reference.
A complete mapping example
When the agent’s return includes step records, the mapping is a simple loop:Related reading
- Connect your agent — the tutorial for getting it working from scratch.
- Capabilities — what declaring “the event stream is complete” means.
- Authoring Evals — the full set of assertions that read this stream.