跳转到主要内容
adapter 的 send 返回一个 Turn,其中 events: StreamEvent[]断言的唯一数据源t.calledTool()t.replytoolOrdernoFailedActions……全部从这条流上读。把你的 agent”这一轮做了什么”翻成这条流,整套断言就都能用。

Turn:send 的返回值

interface Turn {
  events: StreamEvent[];                          // 本轮事件,按真实发生顺序
  data?: unknown;                                 // 结构化输出 → outputEquals / outputMatches
  status: "completed" | "failed" | "waiting";     // waiting = 停下等人(HITL)
  usage?: { inputTokens: number; outputTokens: number };  // → maxTokens / maxCost / 成本报表
}
data 放结构化输出,不要序列化塞进 eventsusage 拿得到就带,拿不到就不填——别编数字

事件总表

事件说什么消费它的断言 / API
messageagent(或用户)说了一段话t.replymessageIncludes、judge 的材料
action.called发起一次工具调用calledTooltoolOrdermaxToolCallsnotCalledTool
action.result该次调用的结果calledToolstatus 匹配、noFailedActions
subagent.called委派一个子 agentcalledSubagent
subagent.completed子 agent 返回calledSubagentstatusnoFailedActions
input.requested停下来等人输入(HITL)t.parked()t.requireInputRequest()
thinking思考文本event("thinking")、view 展示
compaction上下文被压缩event("compaction")(需声明 compactionObservability
error本轮出错event("error")、view 展示
任何事件都能被通用断言消费:event(type) / notEvent(type) / eventOrder(types) / eventsSatisfy(label, predicate)

逐事件说明

message —— 说了什么

{ type: "message", role: "assistant" | "user", text: string }
每段助手文本吐一条 role: "assistant"message工具结果不是助手消息——不要把工具输出包成 message,否则 t.reply 会读到错误内容。用户输入的 message 由 NiceEval 自动记录,adapter 不用吐。

action.called / action.result —— 调了什么工具、结果如何

{ type: "action.called", callId: string, name: string, input: JsonValue }
{ type: "action.result", callId: string, output?: JsonValue,
  status: "completed" | "failed" | "rejected" }
  • 每个 action.called 配一个callIdaction.result——并发调用靠它不错配。你的 agent 返回里有显式 id(AI SDK 的 toolCallId、Anthropic 的 tool_use.id)就直接用;实在没有再按顺序合成。
  • status 如实填:工具执行失败是 "failed"noFailedActions() 会响);人否决是 "rejected"noFailedActions() 依然通过,calledTool(..., { status: "rejected" }) 可精确断言)。两回事,别混。
  • name 用工具的原始名字。

subagent.called / subagent.completed —— 委派了谁

{ type: "subagent.called", callId: string, name: string, remoteUrl?: string }
{ type: "subagent.completed", callId: string, output?: JsonValue,
  status: "completed" | "failed" }
被测系统把任务委派给子 agent(等它返回)时吐这一对,callId 配对规则同上。喂 calledSubagent("researcher") 这类断言。

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

{ type: "input.requested", request: {
    id?: string,
    action?: string,        // 停在哪个动作上(如工具名)
    input?: JsonValue,      // 该动作的入参
    prompt?: string,        // 问人的问题
    options?: { id: string, label?: string }[],   // 可选项(approve / deny…)
} }
agent 停轮等人时,每个待回答的问题吐一条,同时整轮 status 返回 "waiting"t.requireInputRequest(filter) 的 filter 逐字段匹配这个 request——能填的字段尽量填,否则 eval 侧筛选不到。接法见接入教程的 HITL 部分

thinking / compaction / error

{ type: "thinking", text: string }
{ type: "compaction", reason?: string }   // 上下文压缩;吐它才声明 compactionObservability
{ type: "error", message: string }
有就吐,没有不硬造。compaction 主要来自 coding agent CLI(上下文满了自动压缩)。

映射的三条纪律

  1. 时序即事实:事件按真实发生顺序排。toolOrder / eventOrder 靠子序匹配,顺序错了断言就失真。
  2. callId 配对:每个 called 都要有同 id 的 result,缺一半会让 calledTool(..., { status }) 匹配不上。
  3. 完整性:声明了 toolObservability: true 就意味着所有工具调用都在流里。只吐一部分时,notCalledTool / maxToolCalls 这类负断言会静默通过——没有报错,比失败更难发现。手工映射做不到完整就不要声明这个能力

一个完整的映射示例

agent 返回里带步骤记录时,映射就是一段小循环:
import type { StreamEvent } from "niceeval";

function toStreamEvents(body: MyBotResponse): StreamEvent[] {
  const events: StreamEvent[] = [];
  for (const step of body.steps) {
    if (step.type === "tool_call") {
      events.push({ type: "action.called", callId: step.id, name: step.tool, input: step.args });
      events.push({
        type: "action.result",
        callId: step.id,
        output: step.result,
        status: step.error ? "failed" : "completed",
      });
    }
    if (step.type === "text") events.push({ type: "message", role: "assistant", text: step.text });
  }
  return events;
}

相关阅读