Skip to main content
adapter 的 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 文本复制进去凑数,也不要反过来把结构化输出序列化后写进 eventsusage 拿得到就带,拿不到就不填——别编数字 usage 的完整字段(Usage 类型):

inputTokens

未命中缓存、按全价计费的输入 token;与两个 cache 桶互斥。

outputTokens

输出(completion)token 数。

cacheReadTokens

从提示缓存命中的输入 token;独立计价桶,不包含在 inputTokens 里(省略表示该 agent 不上报此项)。

cacheCreationTokens

写入提示缓存的输入 token;独立计价桶,不包含在 inputTokens 里(省略表示该 agent 不上报此项)。

reasoningTokens

推理(thinking)token 数,outputTokens 的已含明细,单列展示用;只在协议真实提供时存在。

requests

真实发生的模型请求数。协议不提供请求计数就省略,绝不写 1 凑数。

costUSD

网关/adapter 实测的真实美元成本(只能由 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 会读到错误内容。用户输入的 messageNiceEval 自动记录,adapter 不用吐。

工具 operation.started / operation.finished —— 调了什么工具、结果如何

  • 每个 tool operation.started 配一个operationId 的 tool operation.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 statuspnpm 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 shownpx 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 —— 委派了谁

被测系统把任务委派给子 agent(等它返回)时吐这一对,operationId 配对规则同上。评估作者用 eventMatch("operation.started", ...) / eventMatch("operation.finished", ...) 比较对应事件。

input.requested —— 停下等人(HITL)

agent 停轮等人时,每个待回答的问题吐一条,同时该 Turn 的 status 返回 "waiting"t.requireInputRequest(filter) 的 filter 逐字段匹配这个 request——能填的字段尽量填,否则评估用例侧筛选不到。接法见接入教程的 HITL 部分

thinking / compaction / error

有就吐,没有不硬造。compaction 主要来自 coding agent CLI(上下文满了自动压缩)。

映射的三条纪律

  1. 时序即事实:事件按真实发生顺序排。toolOrder / eventOrder 用单调 cursor 匹配不同 occurrence 的子序列,顺序错了断言就失真。
  2. operationId 配对:每个 started operation 都要有同 kind、同 id 的 finished operation。只有配对完成,框架才能把状态与 input 归到同一个逻辑工具 occurrence。
  3. 完整性必须显式:官方转换器按实际 adapter 能力声明 evidence coverage。负向断言在相关输入或 action coverage 不完整时是 unavailable,不会把“没观察到”冒充“没有发生”。手工映射同样要如实声明覆盖范围,见能力位参考

一个完整的映射示例

agent 返回里带步骤记录时,映射就是一段小循环:

相关阅读