send returns a Turn, and its events: StreamEvent[] is the only data source for assertions: t.calledTool(), t.reply, toolOrder, noFailedActions, and more all read from this stream. Once your adapter translates “what the agent did this turn” into this stream, the whole assertion surface becomes available.
Turn: the return value of send
data is “this turn’s structured product”: only fill it when the application’s answer itself is a structured object (extraction, classification, form filling); outputEquals / outputMatches read from it. Applications that only reply with text should leave it unset — do not copy the raw response body or message text into it to pad it out, and conversely do not serialize structured output into events. Include usage when you have it, and leave it unset when you don’t — never invent numbers.
The full field set of usage (the Usage type):
inputTokens
outputTokens
cacheReadTokens
inputTokens (omitted means the agent does not report this).
cacheCreationTokens
reasoningTokens
requests
costUSD
Turn.usage.costUSD; it is
never inferred from token usage or an OTel span). This is a separate fact from the top-level estimatedCostUSD (price-table
estimate): when present, it takes priority over the cost estimated from the price table (defineConfig({ pricing })) — see
the fallback order usage.costUSD ?? estimateCost(...) in estimateCost.
StreamEvent variants at a glance
The ten variants of StreamEvent, listed field by field (see the “Event table” and “Event details” sections below for the assertions / usage details that consume them):
message
operation.started
operation.finished
operation.finished
skill.loaded
input.requested
thinking
compaction
error
Event table
Any event can also be consumed by the generic assertions:
event(type) / notEvent(type) / eventOrder(types) / eventsSatisfy(label, predicate).
Event details
message — what was said
message with role: "assistant" for each chunk of assistant text. Tool results are not assistant messages — do not wrap tool output as message, or t.reply will read the wrong thing. message events for user input are recorded automatically by NiceEval; the adapter does not need to emit them.
Tool operation.started / operation.finished — which tool was called, and what happened
- Pair each tool
operation.startedwith a tooloperation.finishedthat has the sameoperationId— this is what keeps concurrent calls from getting mixed up. Use the explicit id your agent’s response gives you (AI SDK’stoolCallId, Anthropic’stool_use.id) directly; only synthesize one by order as a last resort when there truly isn’t one. - Fill
statustruthfully: an actual tool execution failure is"failed"(noFailedActions()fires on it); a human denial is"rejected"(noFailedActions()still passes, andcalledTool(..., { status: "rejected" })can assert on it precisely). These are two different things — don’t conflate them. - Use the tool’s original name for
name.
Subagent operation.started / operation.finished — who was delegated to
operationId pairing rule is the same as above. This feeds assertions like calledSubagent("researcher").
input.requested — paused for human input (HITL)
status returns "waiting". The filter in t.requireInputRequest(filter) matches this request field by field — fill in as many fields as you can, or the eval side won’t be able to filter it. See the HITL section of the connect-your-agent tutorial.
thinking / compaction / error
compaction mostly comes from coding-agent CLIs (automatic compaction when context fills up).
Three mapping rules
- Order is fact: events are laid out in the order they actually happened.
toolOrder/eventOrdermatch by subsequence, so a wrong order makes the assertion misleading. - Pair
operationIds: every started operation needs a finished operation of the samekindwith the same id; missing half of the pair makescalledTool(..., { status })fail to match. - Completeness has no declaration layer — it is proven by the source: with official converters (
createClaudeSdkEventStream,turnFromAiSdk, etc.), the return value itself guarantees every tool call is in the stream; with manual mapping, completeness depends entirely on whether your mapping code covers everything. When only part of the stream is emitted, negative assertions likenotCalledTool/maxToolCallspass silently — with no error, which is harder to notice than an outright failure. Confirm your mapping code covers every tool call before relying on manual mapping — see the capabilities reference.
A complete mapping example
When the agent response contains step records, the mapping is a small loop:Related reading
- Connect your agent — the from-scratch integration tutorial.
- Capabilities — what it means to say “the event stream is complete.”
- Authoring evals — the full set of assertions that consume this stream.