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

Input tokens that did not hit cache and are billed at the full rate. They are mutually exclusive with the two cache buckets.

outputTokens

The number of output (completion) tokens.

cacheReadTokens

Input tokens served from a prompt-cache hit. This is a separate billing bucket and is not included in inputTokens. Omission means the Agent does not report it.

cacheCreationTokens

Input tokens written to a prompt cache. This is a separate billing bucket and is not included in inputTokens. Omission means the Agent does not report it.

reasoningTokens

The number of reasoning (thinking) tokens. It is a detail already included in outputTokens and is listed separately for display. It exists only when the protocol actually supplies it.

requests

The number of model requests that actually occurred. Omit it when the protocol does not provide a request count; never write 1 just to fill the field.

costUSD

The actual USD cost measured by the gateway or Adapter. It can only be explicitly returned through 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.costUSD represents 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 calculates estimatedCostUSD from the model, reported token usage, and the Config/runtime price table even when an observed cost exists. The Experiment budget and t.maxCost() consume that estimate; Usage.costUSD does not. A Report cost projection accepts only an explicit PricingProfile and sealed Usage; it does not read the Runner estimate.

StreamEvent variants at a glance

The ten StreamEvent 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

Emit one 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.started with a tool operation.finished that has the same operationId. 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’s toolCallId or Anthropic’s tool_use.id—and synthesize it by order only as a last resort.
  • Set status truthfully. A failed tool execution is "failed"; a human rejection is "rejected". Eval authors use ToolMatch status 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 has pending status and unavailable output. A finished event that omits output also 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

When the system under test delegates a task to a subagent and waits for it to return, emit this pair. The 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)

When the Agent stops a turn to wait for a person, emit one event for each pending question and return "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

Emit these events when you have them; do not fabricate them when you do not. 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

  1. Order is fact: lay events out in the order they actually occurred. toolOrder / eventOrder use a monotonic cursor to match subsequences of different occurrences. The wrong order makes assertions misleading. eventOrder only orders events supported by public eventMatch.
  2. Pair operationIds: every started operation needs a finished operation with the same kind and ID. Only a complete pair lets the framework assign status and input to the same logical tool occurrence.
  3. 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: