> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 标准事件流参考

> StreamEvent 的十种事件：每种的字段、什么时候吐、哪些断言消费它。adapter 的核心工作就是产出这条流。

adapter 的 `send` 返回一个 `Turn`，其中 `events: StreamEvent[]` 是事件断言的事实来源。`turn.calledTool(...)`、`turn.toolOrder(...)`、`turn.event(...)` 与 `turn.succeeded()` 都从本轮的标准事件和逻辑工具 occurrence 派生，调用时直接登记 Boolean Assertion。把你的 agent“这一轮做了什么”如实翻成这条流，整套事件断言就都能用。

## Turn：send 的返回值

```ts theme={null}
interface Turn {
  events: StreamEvent[];                          // 本轮事件,按真实发生顺序
  data?: JsonValue;                               // 结构化输出，可交给 t.check(...)
  status: "completed" | "failed" | "waiting";     // waiting = 停下等人(HITL)
  usage?: Usage;                                  // → maxTokens / maxCost / 成本报表,完整字段见下方
}
```

`data` 的语义是“本轮的结构化产物”：**应用的回答本身是结构化对象**（抽取、分类、表单填充）时才填，评估作者可以用 `t.check(turn.data, ...)` 比较它。只回文本的应用不填——不要把原始响应 body 或 `message` 文本复制进去凑数，也不要反过来把结构化输出序列化后写进 `events`。`usage` 拿得到就带，拿不到就不填——**别编数字**。

`usage` 的完整字段（`Usage` 类型）：

#### `inputTokens`

```ts theme={null}
inputTokens?: number;
```

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

#### `outputTokens`

```ts theme={null}
outputTokens?: number;
```

输出(completion)token 数。

#### `cacheReadTokens`

```ts theme={null}
cacheReadTokens?: number;
```

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

#### `cacheCreationTokens`

```ts theme={null}
cacheCreationTokens?: number;
```

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

#### `reasoningTokens`

```ts theme={null}
reasoningTokens?: number;
```

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

#### `requests`

```ts theme={null}
requests?: number;
```

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

#### `costUSD`

```ts theme={null}
costUSD?: number;
```

网关/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`

```ts theme={null}
{ type: "message"; role: "assistant"; text: string; loc?: SourceLoc }
```

#### `message`

```ts theme={null}
{ type: "message"; role: "user"; text: string; loc?: SourceLoc; sourceOrder?: number }
```

#### `operation.started`

```ts theme={null}
{
  type: "operation.started";
  operationId: string;
  operation:
    | {
        kind: "tool";
        name: string;
        input: JsonValue;
        tool?: ToolName;
        /**
         * Adapter 对 command / not-command 的协议级分类。暂时可选以兼容尚未迁移的
         * 第三方 Adapter；缺失不得在 core 侧由 name、input 或 shell text 补造。
         */
        command?: CommandProjection;
      }
    | { kind: "subagent"; name: string; remoteUrl?: string };
}
```

#### `operation.finished`

```ts theme={null}
{
  type: "operation.finished";
  operationId: string;
  kind: "tool";
  output?: JsonValue;
  status: "completed" | "failed" | "rejected";
}
```

#### `operation.finished`

```ts theme={null}
{
  type: "operation.finished";
  operationId: string;
  kind: "subagent";
  output?: JsonValue;
  status: "completed" | "failed";
}
```

#### `skill.loaded`

```ts theme={null}
{ type: "skill.loaded"; skill: string; operationId?: string }
```

#### `input.requested`

```ts theme={null}
{ type: "input.requested"; request: InputRequest }
```

#### `thinking`

```ts theme={null}
{ type: "thinking"; text: string }
```

#### `context.injected`

```ts theme={null}
{ type: "context.injected"; text: string; source?: string }
```

#### `compaction`

```ts theme={null}
{ type: "compaction"; reason?: string }
```

#### `error`

```ts theme={null}
{ type: "error"; message: string }
```

## 事件总表

| 事件                                       | 说什么             | 消费它的断言 / API                                                      |
| ---------------------------------------- | --------------- | ----------------------------------------------------------------- |
| `message`                                | agent（或用户）说了一段话 | `turn.message`、`turn.event(eventMatch("message", ...))`、Judge 的材料 |
| `operation.started`（`kind: "tool"`）      | 发起一次工具调用        | `turn.calledTool(...)`、`turn.toolOrder(...)`、`turn.event(...)`    |
| `operation.finished`（`kind: "tool"`）     | 该次工具调用的结果       | 带状态条件的 `ToolMatch`                                                |
| `operation.started`（`kind: "subagent"`）  | 委派一个子 agent     | `turn.event(eventMatch("operation.started", ...))`                |
| `operation.finished`（`kind: "subagent"`） | 子 agent 返回      | `turn.event(eventMatch("operation.finished", ...))`               |
| `input.requested`                        | 停下来等人输入（HITL）   | `t.requireInputRequest()`、`turn.event(...)`                       |
| `thinking`                               | 思考文本            | `turn.event(eventMatch("thinking", ...))`、view 展示                 |
| `compaction`                             | 上下文被压缩          | `turn.event(eventMatch("compaction"))`                            |
| `error`                                  | 本轮出错            | `turn.event(eventMatch("error", ...))`、view 展示                    |

事件断言共用三个入口：`event(match)`、`notEvent(match)` 与 `eventOrder(matches)`；它们都接收 `eventMatch(...)`，不另造 selector object 或匿名事件 predicate。

## 逐事件说明

### `message` —— 说了什么

```ts theme={null}
{ type: "message", role: "assistant" | "user", text: string }
```

每段助手文本吐一条 `role: "assistant"` 的 `message`。**工具结果不是助手消息**——不要把工具输出包成 `message`，否则 `t.reply` 会读到错误内容。用户输入的 `message` 由 [NiceEval](https://niceeval.com/) 自动记录，adapter 不用吐。

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

```ts theme={null}
{ type: "operation.started", operationId: string,
  operation: { kind: "tool", name: string, input: JsonValue,
    tool?: ToolName, command?: CommandProjection } }
{ type: "operation.finished", operationId: string, kind: "tool",
  output?: JsonValue, status: "completed" | "failed" | "rejected" }
```

* 每个 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 status` 和 `pnpm test`。名称只能说明工具类别；需要辨认命令时，评估作者使用已归一的 command projection：

```ts theme={null}
import { commandMatch } from "niceeval/expect";

turn.calledTool(commandMatch("pnpm", { argsStart: ["test"] }));
```

`ToolMatch` 每次比较一条逻辑 occurrence。完整 selector、JSON、路径、输出与计数规则见 [Scoped assertions](https://github.com/NiceEval/NiceEval/blob/main/docs/feature/assertions/library/scoped-assertions.md)。

原生协议直接给出单一 invocation 的 structured argv 时，从 `niceeval/adapter` 调用公开构造器：

```ts theme={null}
import { commandProjection } from "niceeval/adapter";

events.push({
  type: "operation.started",
  operationId: call.id,
  operation: {
    kind: "tool",
    name: call.name,
    input: call.input,
    command: commandProjection({
      state: "available",
      executable: call.executable,
      args: call.args,
    }),
  },
});
```

`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` —— 委派了谁

```ts theme={null}
{ type: "operation.started", operationId: string,
  operation: { kind: "subagent", name: string, remoteUrl?: string } }
{ type: "operation.finished", operationId: string, kind: "subagent",
  output?: JsonValue, status: "completed" | "failed" }
```

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

### `input.requested` —— 停下等人（HITL）

```ts theme={null}
{ type: "input.requested", request: {
    id?: string,
    action?: string,        // 停在哪个动作上(如工具名)
    input?: JsonValue,      // 该动作的入参
    prompt?: string,        // 问人的问题
    options?: { id: string, label?: string }[],   // 可选项(approve / deny…)
} }
```

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

### `thinking` / `compaction` / `error`

```ts theme={null}
{ type: "thinking", text: string }
{ type: "compaction", reason?: string }   // 上下文压缩;adapter 的 parser 吐出即可断言,没有声明层
{ type: "error", message: string }
```

有就吐，没有不硬造。`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`，不会把“没观察到”冒充“没有发生”。手工映射同样要如实声明覆盖范围，见[能力位参考](/docs/zh/reference/capabilities)。

## 一个完整的映射示例

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

```ts theme={null}
import type { StreamEvent } from "niceeval";

function toStreamEvents(body: MyAgentResponse): StreamEvent[] {
  const events: StreamEvent[] = [];
  for (const step of body.steps) {
    if (step.type === "tool_call") {
      events.push({
        type: "operation.started",
        operationId: step.id,
        operation: { kind: "tool", name: step.tool, input: step.args },
      });
      events.push({
        type: "operation.finished",
        operationId: step.id,
        kind: "tool",
        output: step.result,
        status: step.error ? "failed" : "completed",
      });
    }
    if (step.type === "text") events.push({ type: "message", role: "assistant", text: step.text });
  }
  return events;
}
```

## 相关阅读

* [接入你的 agent](/docs/zh/tutorials/connect-your-agent) —— 从零跑通的教程。
* [能力位](/docs/zh/reference/capabilities) —— 声明"事件流是完整的"意味着什么。
* [编写评估用例](/docs/zh/tutorials/authoring) —— 消费这条流的断言全集。
