Skip to main content
An adapter’s 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

The meaning of 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

Total input tokens at billing granularity (whatever the protocol reports; includes cache reads as-is, no conversion).

outputTokens

Output (completion) token count.

cacheReadTokens

The portion of input served from cache hits; same accounting basis as inputTokens (omitted means the agent does not report this).

cacheCreationTokens

Tokens written to create a prompt cache entry (omitted means the agent does not report this).

reasoningTokens

Reasoning (thinking) token count, present only when the protocol actually reports it (omitted means the agent does not report this).

requests

The actual number of model requests that occurred. Omitted when the protocol doesn’t provide a request count — never padded to 1.

costUSD

The actual dollar cost measured by the gateway/adapter (can only be brought back explicitly via 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

Emit one 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.started with a tool operation.finished that has the same operationId — this is what keeps concurrent calls from getting mixed up. Use the explicit id your agent’s response gives you (AI SDK’s toolCallId, Anthropic’s tool_use.id) directly; only synthesize one by order as a last resort when there truly isn’t one.
  • Fill status truthfully: an actual tool execution failure is "failed" (noFailedActions() fires on it); a human denial is "rejected" (noFailedActions() still passes, and calledTool(..., { 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

Emit this pair when the system under test delegates a task to a subagent (and waits for it to return); the operationId pairing rule is the same as above. This feeds assertions like calledSubagent("researcher").

input.requested — paused for human input (HITL)

When the agent pauses for a person, emit one event per pending question, and the whole turn’s 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

Emit them when you have them; don’t fabricate them when you don’t. compaction mostly comes from coding-agent CLIs (automatic compaction when context fills up).

Three mapping rules

  1. Order is fact: events are laid out in the order they actually happened. toolOrder / eventOrder match by subsequence, so a wrong order makes the assertion misleading.
  2. Pair operationIds: every started operation needs a finished operation of the same kind with the same id; missing half of the pair makes calledTool(..., { status }) fail to match.
  3. 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 like notCalledTool / maxToolCalls pass 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: