send returns a Turn. Its raw events: StreamEvent[] is the source of truth. NiceEval derives immutable toolCalls and eventOccurrences collections for managed Matches. Raw events remains an ordinary Value subject for check; occurrence quantifiers and order Matches use eventOccurrences. Translate what your Agent did in this turn truthfully into the stream, and the complete event-assertion surface is available.
Turn: the return value of send
data means “this turn’s structured output.” Fill it only when the application’s response itself is a structured object—for extraction, classification, or form completion—so an Eval author can compare it with t.check(turn.data, ...). An application that returns only text leaves it unset. Do not copy the raw response body or message text into it just to fill it, and do not serialize structured output into events instead. Include usage when it is available, and omit it when it is not—never invent numbers.
The full fields of usage (the Usage type):
inputTokens
outputTokens
cacheReadTokens
inputTokens. Omission means the Agent does not report it.
cacheCreationTokens
inputTokens. Omission means the Agent does not report it.
reasoningTokens
outputTokens and is listed separately for display. It exists only when the protocol actually supplies it.
requests
1 just to fill the field.
costUSD
Turn.usage.costUSD; it is never derived from token usage or an OTel span. It is separate from the top-level estimatedCostUSD (a price-table estimate). This is a one-way field contract: this field stores only the observed value; estimatedCostUSD is always the estimate from estimateCost(model, usage, pricing), computed independently even when an observed value exists. Neither value overrides or fills in for the other; an observed value never replaces or triggers an estimate.
Cost boundary:Usage.costUSDrepresents only observed USD cost returned by a Provider or Adapter. It is never derived from tokens, a model catalog, or a local price table. The Runner independently calculatesestimatedCostUSDfrom themodel, reported token usage, and the Config/runtime price table even when an observed cost exists. The Experiment budget andt.maxCost()consume that estimate;Usage.costUSDdoes not. A Report cost projection accepts only an explicitPricingProfileand sealed Usage; it does not read the Runner estimate.
StreamEvent variants at a glance
The tenStreamEvent variants are listed field by field below. For assertions that consume them and usage details, 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
eventMatch currently has three shapes: message, tool operation.started, and tool operation.finished. Apply occurrence quantifiers to this Match, then register it against the managed eventOccurrences collection. Use inOrder([...]) against a Turn or Session collection for ordering. You cannot use every raw StreamEvent as an occurrence selector, but you can still pass raw events to check with an ordinary Value Match.
Event details
message — what was said
role: "assistant" message for every segment of assistant text. A tool result is not an assistant message. Do not wrap tool output in message, or t.reply will read the wrong content. NiceEval automatically records user-input message events; an Adapter does not need to emit them.
Tool operation.started / operation.finished — which tool ran and what happened
- Pair each tool
operation.startedwith a tooloperation.finishedthat has the sameoperationId. That prevents concurrent calls from being paired incorrectly. Use the explicit ID in your Agent’s return when it has one—such as AI SDK’stoolCallIdor Anthropic’stool_use.id—and synthesize it by order only as a last resort. - Set
statustruthfully. A failed tool execution is"failed"; a human rejection is"rejected". Eval authors useToolMatchstatus conditions to distinguish them precisely, so do not conflate the two. - Use the tool’s original name for
name. - With only
started, the logical occurrence haspendingstatus and unavailable output. A finished event that omitsoutputalso means unavailable; do not turn missing data into empty JSON or an ordinary mismatch.
Pass tool events to ToolMatch
The same shell tool might run git status and pnpm test in sequence. Its name tells you only the tool category. When an Eval author needs to identify the command, use the normalized command projection:
ToolMatch compares one logical occurrence at a time. For complete selectors, JSON, paths, output, and counting rules, see Scoped assertions.
When the native protocol directly supplies structured argv for one invocation, call the public constructor from niceeval/adapter:
commandProjection() retains original tokens confirmed by the Adapter and uses the same normalizeLogicalCommand() to produce a logical-command/v1 projection. pnpm exec niceeval view can be matched precisely by commandMatch("niceeval", { argsStart: ["view"] }).
Mark the original command available only when the native protocol already supplies argv, or the protocol grammar can unambiguously produce one invocation. Use opaqueCommandProjection(reason) when the protocol supplies only shell source or when content is truncated or redacted. Use notCommandProjection() when you can confirm that it is not a command. When you cannot confirm command / not-command, downgrade actions coverage; do not guess from a tool name, input, or shell text.
Subagent operation.started / operation.finished — who was delegated to
operationId pairing rule is the same as above. These events remain in the raw event stream and Report, but the public assert-first API currently has no calledSubagent, and eventMatch(...) cannot select a subagent operation. Do not invent a different selector.
input.requested — stop and wait for a person (HITL)
"waiting" as that Turn’s status. The t.requireInputRequest(filter) filter matches this request field by field. Fill in as many fields as you can, or the Eval cannot select it. See the HITL section of the connection tutorial.
thinking / compaction / error
compaction mostly comes from coding-Agent CLIs when a full context is compressed automatically. These raw events are retained for Reports and diagnostics; the public eventMatch cannot currently select them.
Three mapping rules
- Order is fact: lay events out in the order they actually occurred.
toolOrder/eventOrderuse a monotonic cursor to match subsequences of different occurrences. The wrong order makes assertions misleading.eventOrderonly orders events supported by publiceventMatch. - Pair
operationIds: every started operation needs a finished operation with the samekindand ID. Only a complete pair lets the framework assign status and input to the same logical tool occurrence. - Completeness must be explicit: official converters declare evidence coverage according to actual Adapter capability. A negative assertion with incomplete relevant input or action coverage is
unavailable; it never turns “not observed” into “did not happen.” Manual mappings must likewise state their coverage honestly. See the Capabilities reference.
A complete mapping example
When an Agent return has step records, the mapping is a small loop:Related reading
- Connect your Agent — the tutorial for making an integration work from scratch.
- Capabilities — what declaring a complete event stream means.
- Authoring Evals — the complete assertion surface that consumes this stream.