send 返回一个 Turn,其中 events: StreamEvent[] 是事件断言的事实来源。turn.calledTool(...)、turn.toolOrder(...)、turn.event(...) 与 turn.succeeded() 都从本轮的标准事件和逻辑工具 occurrence 派生,调用时直接登记 Boolean Assertion。把你的 agent“这一轮做了什么”如实翻成这条流,整套事件断言就都能用。
Turn:send 的返回值
data 的语义是“本轮的结构化产物”:应用的回答本身是结构化对象(抽取、分类、表单填充)时才填,评估作者可以用 t.check(turn.data, ...) 比较它。只回文本的应用不填——不要把原始响应 body 或 message 文本复制进去凑数,也不要反过来把结构化输出序列化后写进 events。usage 拿得到就带,拿不到就不填——别编数字。
usage 的完整字段(Usage 类型):
inputTokens
outputTokens
cacheReadTokens
cacheCreationTokens
reasoningTokens
requests
costUSD
Turn.usage.costUSD 显式带回,从不从
token 用量或 OTel span 反推得到)。与顶层 estimatedCostUSD(价目表估算)是两个
相互独立的事实,单向字段契约:本字段只存 observed 值;estimatedCostUSD 恒等于
estimateCost(model, usage, pricing) 的估算,即使 observed 存在也照常独立计算——
两者互不覆盖、互不兜底;observed 值从不替代或触发 estimate。
成本边界:Usage.costUSD只表示 Provider / Adapter 返回的 USD observed 成本,绝不由 token、模型目录或本地价目表推导。Runner 即使已有 observed 成本,也会从model、上报的 token 用量和 Config/runtime price table 独立计算estimatedCostUSD。Experiment 的预算与t.maxCost()使用这个 estimate,Usage.costUSD不驱动它。Report 成本投影只接受显式PricingProfile与 sealed Usage,不读取 Runner estimate。
StreamEvent 变体一览
StreamEvent 的十种变体,逐字段列出(消费它们的断言 / 使用细节见下面的「事件总表」和「逐事件说明」):
message
message
operation.started
operation.finished
operation.finished
skill.loaded
input.requested
thinking
context.injected
compaction
error
事件总表
事件断言共用三个入口:
event(match)、notEvent(match) 与 eventOrder(matches);它们都接收 eventMatch(...)。eventMatch 对 message、工具 operation.started 与工具 operation.finished 构造 occurrence Match;其数量由 Match 自己的 .atLeast()、.exactly() 等 quantifier 指定。其它原始 StreamEvent 仍可作为 check(turn.events, valueMatch) 的普通 Value subject。
逐事件说明
message —— 说了什么
role: "assistant" 的 message。工具结果不是助手消息——不要把工具输出包成 message,否则 t.reply 会读到错误内容。用户输入的 message 由 NiceEval 自动记录,adapter 不用吐。
工具 operation.started / operation.finished —— 调了什么工具、结果如何
- 每个 tool
operation.started配一个同operationId的 tooloperation.finished——并发调用靠它不错配。你的 agent 返回里有显式 id(AI SDK 的toolCallId、Anthropic 的tool_use.id)就直接用;实在没有再按顺序合成。 status如实填:工具执行失败是"failed";人否决是"rejected"。评估作者用ToolMatch的状态条件精确区分,两回事不要混。name用工具的原始名字。- 只有 started 时,逻辑 occurrence 的状态是
pending,输出为 unavailable。finished 省略output也表示 unavailable,不能把缺失写成空 JSON 或普通不匹配。
将工具事件交给 ToolMatch
同一个 shell 工具可以先后运行 git status 和 pnpm test。名称只能说明工具类别;需要辨认命令时,评估作者使用已归一的 command projection:
ToolMatch 每次比较一条逻辑 occurrence。完整 selector、JSON、路径、输出与计数规则见 Scoped assertions。
原生协议直接给出单一 invocation 的 structured argv 时,从 niceeval/adapter 调用公开构造器:
commandProjection() 保留 Adapter 已确认的 original tokens,并调用同一份 normalizeLogicalCommand() 生成 logical-command/v1 投影。pnpm exec niceeval view 能由 commandMatch("niceeval", { argsStart: ["view"] }) 精确匹配。
只有原生协议已经给出 argv,或协议 grammar 能无歧义地产生单一 invocation,才能把 original 标为 available。协议只给 shell source、内容已截断或脱敏时使用 opaqueCommandProjection(reason);能确认不是 command 时使用 notCommandProjection()。无法确认 command / not-command 时降低 actions coverage,不能从 tool name、input 或 shell 文本猜测。
子 agent operation.started / operation.finished —— 委派了谁
operationId 配对规则同上。它们会保留在原始事件流与报告中。需要检查它们时,对 turn.events 使用普通 Value Match;工具 occurrence 的 eventMatch(...) 不把子 agent operation 当成工具调用。
input.requested —— 停下等人(HITL)
status 返回 "waiting"。t.requireInputRequest(filter) 的 filter 逐字段匹配这个 request——能填的字段尽量填,否则评估用例侧筛选不到。接法见接入教程的 HITL 部分。
thinking / compaction / error
compaction 主要来自 coding agent CLI(上下文满了自动压缩)。这些原始事件保留给报告与诊断;需要断言时,把 turn.events 作为普通 Value subject 交给 check。
映射的三条纪律
- 时序即事实:事件按真实发生顺序排。
toolOrder/eventOrder用单调 cursor 匹配不同 occurrence 的子序列,顺序错了断言就失真。eventOrder只排公开eventMatch支持的事件。 operationId配对:每个 started operation 都要有同kind、同 id 的 finished operation。只有配对完成,框架才能把状态与 input 归到同一个逻辑工具 occurrence。- 完整性必须显式:官方转换器按实际 adapter 能力声明 evidence coverage。负向断言在相关输入或 action coverage 不完整时是
unavailable,不会把“没观察到”冒充“没有发生”。手工映射同样要如实声明覆盖范围,见能力位参考。
一个完整的映射示例
agent 返回里带步骤记录时,映射就是一段小循环:相关阅读
- 接入你的 agent —— 从零跑通的教程。
- 能力位 —— 声明”事件流是完整的”意味着什么。
- 编写评估用例 —— 消费这条流的断言全集。