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 从 Config/runtime price table 独立计算estimatedCostUSD,即使 observed 成本存在也照常计算,且只有maxCost消费它。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(...),不另造 selector object 或匿名事件 predicate。
逐事件说明
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 show 和 npx niceeval show 因此都能由 commandMatch("niceeval", { argsStart: ["show"] }) 精确匹配。
只有原生协议已经给出 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 配对规则同上。评估作者用 eventMatch("operation.started", ...) / eventMatch("operation.finished", ...) 比较对应事件。
input.requested —— 停下等人(HITL)
status 返回 "waiting"。t.requireInputRequest(filter) 的 filter 逐字段匹配这个 request——能填的字段尽量填,否则评估用例侧筛选不到。接法见接入教程的 HITL 部分。
thinking / compaction / error
compaction 主要来自 coding agent CLI(上下文满了自动压缩)。
映射的三条纪律
- 时序即事实:事件按真实发生顺序排。
toolOrder/eventOrder用单调 cursor 匹配不同 occurrence 的子序列,顺序错了断言就失真。 operationId配对:每个 started operation 都要有同kind、同 id 的 finished operation。只有配对完成,框架才能把状态与 input 归到同一个逻辑工具 occurrence。- 完整性必须显式:官方转换器按实际 adapter 能力声明 evidence coverage。负向断言在相关输入或 action coverage 不完整时是
unavailable,不会把“没观察到”冒充“没有发生”。手工映射同样要如实声明覆盖范围,见能力位参考。
一个完整的映射示例
agent 返回里带步骤记录时,映射就是一段小循环:相关阅读
- 接入你的 agent —— 从零跑通的教程。
- 能力位 —— 声明”事件流是完整的”意味着什么。
- 编写评估用例 —— 消费这条流的断言全集。