> ## 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.

# 内置 Agent 能力参考

> NiceEval 内置的 claude-code、codex、bub 适配器分别做到了哪些能力，对应哪些断言，以及已知限制。

内置的三个 sandbox agent（`claude-code`、`codex`、`bub`）做到的能力不完全相同——能力没有声明这一层，全由构造方式和实际行为证明，见[能力位参考](/docs/zh/reference/capabilities)。这篇按能力盘点三个内置 agent 各自做到哪，以及两个接 AI SDK 应用的内置件：无侵入 HTTP adapter `uiMessageStreamAgent`（含 HITL）和结果转换器 `turnFromAiSdk`。

## 能力速览

| 能力         | 对应的断言 / API                                                                                                                                                               |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 收发消息（基础契约） | `t.send()`（可多次调用）、`t.reply`、`turn.outputEquals` / `outputMatches`、按状态判断的 `t.succeeded()`                                                                                  |
| 事件流完整性     | `calledTool` / `toolOrder` / `usedNoTools` / `maxToolCalls` / `messageIncludes` / `noFailedActions` / `event` / `eventOrder` 等整套作用域断言。**负断言（`notCalledTool` 等）有完整事件流才可信** |
| 会话续接       | 跨轮记忆断言、`t.newSession()` 会话隔离                                                                                                                                              |
| HITL（人工介入） | `t.parked()`、`t.requireInputRequest()`、`t.respond()` / `t.respondAll()`                                                                                                   |
| `tracing`  | trace decoding、`niceeval view` 的调用瀑布图                                                                                                                                     |

## 三个内置 agent 分别做到哪

| Agent         | 收发 | 事件流 | 会话续接                        | HITL | tracing                           | 备注                                                       |
| ------------- | -- | --- | --------------------------- | ---- | --------------------------------- | -------------------------------------------------------- |
| `claude-code` | ✅  | ✅   | ✅（`claude --resume <id>`）   | ❌    | ✅（`http/protobuf` → OTLP，beta 开关） | 内置 parser 自动吐 `compaction` 事件，`t.event("compaction")` 可用 |
| `codex`       | ✅  | ✅   | ✅（`codex exec resume <id>`） | ❌    | ✅（`http/json` → OTLP）             | 内置 parser 自动吐 `compaction` 事件                            |
| `bub`         | ✅  | ✅   | ✅（`--session-id` + tape 续接） | ❌    | ✅（`http/protobuf` → OTLP）         | 内置 parser 自动吐 `compaction` 事件                            |

三者都用 `defineSandboxAgent` 构造（`Agent.kind` 恒为 `"sandbox"`），所以 `t.sandbox.fileChanged()` / diff 断言、文件 IO、命令执行在三个 agent 上都能用，不受这张表限制。

<Warning>
  三个内置 sandbox agent 都**不支持 HITL**：`send` 只返回 `"completed"` / `"failed"`，从不返回 `"waiting"`，也不吐 `input.requested` 事件。

  需要 `t.respond()` / `t.requireInputRequest()` 时：被测对象是 AI SDK `useChat` 后端的话，用下文的内置 `uiMessageStreamAgent`（v7 tool approval 原生映射成 HITL）。其它被测对象自己写 adapter：实现 `waiting` 状态 + `input.requested` 事件 + resume 交回，[`examples/zh/tier1/pi-sdk`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/pi-sdk) / [`examples/zh/tier1/claude-sdk`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/claude-sdk) 都有手写 HITL 的现成参考。
</Warning>

## 逐 agent 细节

### claude-code

* 连接方式：Sandbox 里 spawn `claude --print --dangerously-skip-permissions`，读回 `~/.claude/projects/**/*.jsonl` 最新一份 transcript。
* 会话续接：`ctx.session.id` 有值时追加 `--resume <id>`；transcript 解出的会话 id 经 `ctx.session.capture()` 写回。
* 鉴权：`ANTHROPIC_API_KEY`，可选 `ANTHROPIC_BASE_URL`；配置项见下方 `ClaudeCodeConfig`。
* `tracing` 通过 claude CLI 原生 OTLP trace spans（beta）配置，协议为 `http/protobuf`。在 env 中设置 `CLAUDE_CODE_ENABLE_TELEMETRY` 与 `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA`（须显式开启的 beta 开关），即可把 endpoint 交给 CLI。trace decoding 显示 interaction / llm\_request / tool 层级的瀑布图。

### codex

* 连接方式：Sandbox 里跑 `codex exec --json`（续接时是 `codex exec resume <id> --json`），stdout JSONL 当 transcript。命令带 `--dangerously-bypass-approvals-and-sandbox` 与 `--dangerously-bypass-hook-trust`：Sandbox 里没人能回答 codex 的交互确认，插件或 `postSetup` 装好的 hook 因此不需要交互授信就能生效。
* 鉴权：`CODEX_API_KEY`（不是 `OPENAI_API_KEY`），可选 `CODEX_BASE_URL` 接 OpenAI 兼容代理。配置项见下方 `CodexConfig`。
* `tracing` 通过 `~/.codex/config.toml` 的 `[otel.trace_exporter.otlp-http]` 段配置，协议 `http/json`。

### bub

* 连接方式：Sandbox 里跑 `bub run` + `--session-id`，从 `~/.bub/tapes/<hash>.jsonl` 读 tape 当 transcript。
* 鉴权：`BUB_API_KEY` + `BUB_API_BASE`（OpenAI 兼容代理）。配置项见下方 `BubConfig`。
* `tracing` 通过环境变量注入（`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` 等），协议 `http/protobuf`。
* 安装走 `uv tool install`（非 npm 包），首次安装带 checkpoint 缓存以加速后续 Sandbox。

## 三个内置 sandbox agent 的配置项

### `ClaudeCodeConfig`

#### `apiKey`

```ts theme={null}
apiKey?: string;
```

Anthropic API key。省略时读 ANTHROPIC\_API\_KEY env。

#### `baseUrl`

```ts theme={null}
baseUrl?: string;
```

自定义 API base URL(代理 / 内网端点)。省略时读 ANTHROPIC\_BASE\_URL env;
两者都没有则用 Anthropic 官方端点(claude CLI 默认行为)。

#### `env`

```ts theme={null}
env?: Readonly<globalThis.Record<string, string>>;
```

Extra process environment for Claude Code. Values stay private runtime data and do not enter carry identity.
Mirror behavior-changing non-secret values into Experiment flags or the owning Plugin identity.

#### `maxTurns`

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

最多跑几个 tool-use 轮次(→ `--max-turns`)。
控制评估用例成本上限;省略时用 CLI 原生默认(无限制)。

#### `mcpServers`

```ts theme={null}
mcpServers?: readonly McpServer[];
```

额外 MCP server(每个 Sandbox setup 时写进用户级 \~/.claude.json)。
stdio 形态写 command(可带 args / env);Streamable HTTP 形态写 url(可带 headers,
逐字进请求头),落成 \{ "type": "http", "url": …, "headers": … } 条目。

#### `skills`

```ts theme={null}
skills?: readonly SkillSpec[];
```

装进 Sandbox 的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
落在 project 级 `.claude/skills/<name>/`,claude CLI 原生发现。

#### `plugins`

```ts theme={null}
plugins?: readonly ClaudeCodePluginSpec[];
```

Claude Code 原生 Plugin(先连 Marketplace,再从中装指定 Plugin)。

#### `settingsFile`

```ts theme={null}
settingsFile?: string;
```

一份完整的 Claude Code `settings.json`(官方格式)在本地项目里的路径 —— 相对运行
niceeval 的项目根(含 `niceeval.config.ts` 的目录)解析,不是 Sandbox 内路径;只接受
项目根内的相对路径,包含 `..` 的路径、绝对路径、`~` 路径和解析后逃出项目根的符号链接
都在 setup 阶段报错。原始字节原样上传为 Sandbox 里原本为空的用户级 `~/.claude/settings.json`
(不继承宿主机配置、不拼接、不重新序列化);保留键 `model` 与 `env` 出现在文件里
setup 报错。manifest 只记项目相对路径与字节 SHA-256,不落正文。

#### `postSetup`

```ts theme={null}
postSetup?: readonly SandboxCommand[];
```

安装后按数组顺序运行的用户 Hook(复用 SandboxCommand 的窄上下文):在写 settings、挂 MCP、
装 Skills / Plugin、写 manifest 全部完成后执行,适合跑插件自带的 setup 脚本这类
「安装产物就位后才能跑」的过程动作。抛错按基础设施错误计(attempt errored)。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

#### `preTeardown`

```ts theme={null}
preTeardown?: readonly SandboxCommand[];
```

与 `postSetup` 成对的收尾 Hook:按 `postSetup` 的逆序语义,在 agent 自己的 teardown 步骤
之前执行(LIFO 镜像 —— `postSetup` 跑在 agent 安装之后,`preTeardown` 就跑在 agent 收尾
之前),当且仅当 `postSetup` 的时点走到过才触发。抛错按基础设施错误计,由 teardown 段
按 teardown-failed 诊断收束。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

### `CodexConfig`

#### `apiKey`

```ts theme={null}
apiKey?: string;
```

代理 / OpenAI API key。省略时读 CODEX\_API\_KEY env。

#### `baseUrl`

```ts theme={null}
baseUrl?: string;
```

OpenAI 兼容代理 base URL(如 `https://s2a.example.com/v1`)。省略时读 CODEX\_BASE\_URL env。

#### `env`

```ts theme={null}
env?: Readonly<globalThis.Record<string, string>>;
```

注入每次 Codex CLI 进程的额外环境变量。首轮 `codex exec` 与续轮 `codex exec resume`
使用同一份声明；Codex 启动的 lifecycle Hook、MCP 动态 header 与命令子进程都会继承。
值只经 Sandbox command options 传入，不拼进 shell 文本或写入 setup manifest，并全部按
潜在敏感值从 timing / execution / error 证据中脱敏。`CODEX_API_KEY` 仍由 `apiKey` 或
宿主同名环境变量提供，Adapter 的鉴权值会覆盖这里的同名键。
env value 不进入 carry 身份。会改变被测行为的非敏感值必须同时声明在 Experiment flags
或所属 Plugin identity；只轮换凭据不会让旧结果失效。
`PATH` 是 Sandbox 受管变量，不接受经这里声明——出现即在 factory 构造时报错，改用
Sandbox factory 的 `pathPrepend`（见 docs/feature/sandbox/library.md）。

#### `mcpServers`

```ts theme={null}
mcpServers?: readonly McpServer[];
```

额外 MCP server(每个 Sandbox setup 时追加进 \~/.codex/config.toml)。
stdio 形态(command/args/env)写 \[mcp\_servers.\<name>] 的 command 行;
Streamable HTTP 形态(url/headers)写 url 行,headers 进 \[mcp\_servers.\<name>.http\_headers] 子表。

#### `skills`

```ts theme={null}
skills?: readonly SkillSpec[];
```

装进 Sandbox 的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
落在 `.agents/skills/<name>/`,并写一段发现指引进 AGENTS.md —— codex 没有 Claude Code 那种
原生 Skill 工具,只把文件装进去它不会自己去读(见 memory/codex-no-native-skill-tool.md)。

#### `plugins`

```ts theme={null}
plugins?: readonly CodexPluginSpec[];
```

Codex 原生 Plugin(先连 Marketplace,再从中装指定 Plugin)。

#### `configFile`

```ts theme={null}
configFile?: string;
```

一份完整的 Codex `config.toml`(官方 TOML 格式)在本地项目里的路径 —— 相对运行
niceeval 的项目根(含 `niceeval.config.ts` 的目录)解析,不是 Sandbox 内路径;只接受
项目根内的相对路径,包含 `..` 的路径、绝对路径、`~` 路径和解析后逃出项目根的符号链接
都在 setup 阶段报错。原始字节原样并入 Sandbox 里原本为空的用户级 `~/.codex/config.toml`
(不继承宿主机配置、不解析后重写);保留键 `model`、`model_provider`、`model_providers`、
`model_reasoning_effort`、`mcp_servers`、`otel` 出现在文件里 setup 报错。manifest 只记
项目相对路径与字节 SHA-256,不落正文。

#### `postSetup`

```ts theme={null}
postSetup?: readonly SandboxCommand[];
```

安装后按数组顺序运行的用户 Hook(复用 SandboxCommand 的窄上下文):在写主配置、挂 MCP、
装 Skills / Plugin、写 manifest 全部完成后执行,适合跑插件自带的 setup 脚本这类
「安装产物就位后才能跑」的过程动作。抛错按基础设施错误计(attempt errored)。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

#### `preTeardown`

```ts theme={null}
preTeardown?: readonly SandboxCommand[];
```

与 `postSetup` 成对的收尾 Hook:按 `postSetup` 的逆序语义,在 agent 自己的 teardown 步骤
之前执行(LIFO 镜像 —— `postSetup` 跑在 agent 安装之后,`preTeardown` 就跑在 agent 收尾
之前),当且仅当 `postSetup` 的时点走到过才触发。抛错按基础设施错误计,由 teardown 段
按 teardown-failed 诊断收束。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

### `BubConfig`

#### `apiKey`

```ts theme={null}
apiKey?: string;
```

OpenAI 兼容代理的 API key。省略时读 BUB\_API\_KEY env。

#### `apiBase`

```ts theme={null}
apiBase?: string;
```

OpenAI 兼容代理的 base URL。省略时读 BUB\_API\_BASE env。

#### `skills`

```ts theme={null}
skills?: SkillSpec[];
```

装进 Sandbox 的 Skill(本地目录/文件,或 repo + 可钉 ref + 可选启用集)。
落在 `.agents/skills/<name>/`,并写一段发现指引进 AGENTS.md(bub 没有原生 Skill 加载机制)。

#### `version`

```ts theme={null}
version?: string;
```

装哪一版 Bub(PyPI 版本号,如 `"0.4.0"`)。省略时用 NiceEval 钉的默认版本;
永远是确定版本,不装 latest —— 被测对象的版本要能从实验配置读出来。

#### `otelPlugin`

```ts theme={null}
otelPlugin?: string;
```

OTel tape store 插件的 git 依赖(时间轨的来源)。省略时用 NiceEval 钉的默认 pin。

插件与 Bub 的 tape 协议同代:默认 pin 从 `bub.tape` 取类型,要求 Bub ≥ 0.3.10;更早的
插件 commit 按 republic 的类型校验,配 Bub ≤ 0.3.9。配错代不会安装失败,而是 span 全被拒、
时间轨静默为空 —— 所以往回钉 `version` 时必须同批钉配套的插件 commit。

#### `pythonPlugins`

```ts theme={null}
pythonPlugins?: PythonPluginSpec[];
```

额外装进 bub tool 环境的 Python Package,每个 Sandbox setup 时进 `uv tool install … --with <pkg>`。
规范化后的 package 列表进安装 checkpoint key:plugin 集合不同的两个 agent 变体不会复用同一个
安装 checkpoint(否则第二个变体会静默拿到第一个变体的环境)。

#### `postSetup`

```ts theme={null}
postSetup?: SandboxCommand[];
```

安装后按数组顺序运行的用户 Hook(复用 SandboxCommand 的窄上下文):在装 bub、装 Skills /
Python package、写 manifest 全部完成后执行。抛错按基础设施错误计(attempt errored)。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

#### `preTeardown`

```ts theme={null}
preTeardown?: SandboxCommand[];
```

与 `postSetup` 成对的收尾 Hook:按 `postSetup` 的逆序语义,在 agent 自己的 teardown 步骤
之前执行(LIFO 镜像 —— `postSetup` 跑在 agent 安装之后,`preTeardown` 就跑在 agent 收尾
之前),当且仅当 `postSetup` 的时点走到过才触发。抛错按基础设施错误计,由 teardown 段
按 teardown-failed 诊断收束。
见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。

## `uiMessageStreamAgent`：接 AI SDK 应用的内建无侵入 adapter（含 HITL）

`uiMessageStreamAgent`（`niceeval/adapter` 导出）对着 [UI Message Stream 协议](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol)（AI SDK `useChat` 后端的标准 SSE）的 HTTP 端点无侵入收发——只 `fetch`，不 import 应用代码，应用在哪部署都能接：

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

export default uiMessageStreamAgent({
  name: "my-assistant",
  url: "https://my-app.example.com/api/chat",
  body: (ctx) => ({ model: ctx.model }),   // 应用支持请求级选模型时,模型对比零改动
});
```

它替你做好的能力：

* **收发 + 事件流**：SSE 帧经 `ai` 包官方的框架无关 reducer `readUIMessageStream`（`useChat` 内部同款）归约，工具调用/结果/消息文本从消息 parts 直构——**不要求应用接 OTel**。
* **会话续接**：协议是服务端零状态、「客户端带全量历史」——工厂用 Adapter 私有的 typed slot 存整份 `UIMessage[]`，每轮原样重放。新会话线（`t.newSession()` 之后）还没有写过这个 slot。
* **HITL**：AI SDK v7 tool approval（工具带 `needsApproval: true`）原生映射——part 停在 `approval-requested` 时该 Turn 的 `status: "waiting"` + `input.requested`。`t.respond("approve" / "deny")` 翻译成 `approval-responded` 原地改写该 part、原样重发 `messages` 触发服务端续跑（和真实前端 `addToolApprovalResponse()` + `sendMessage()` 的协议行为一致）。它没有单独的 approve 端点。
* **拒绝**：调用以 `rejected` 落进事件流，且默认带「不要重试」的 reason（`denyReason` 可覆盖）——不带的话模型经常原样重发同一个调用。
* **usage / 瀑布图**：UI Message Stream 协议帧里没有 usage，所以 `t.maxTokens` 这类用量断言在这个内置件上默认没有数据（应用把 usage 放进 message metadata 属于应用自己的协议扩展）。瀑布图是另一回事：应用有 OTel 埋点（如官方 `@ai-sdk/otel`）时，按 [OTel 接入](/docs/zh/tutorials/connect-otel)把 span 发给 [NiceEval](https://niceeval.com/)，`niceeval view` 就有完整瀑布图——span 只进瀑布图，不喂断言。

需要在评估用例项目里安装 `ai`（可选 peer 依赖，协议 reducer 来自它）。完整可跑示例：[`examples/zh/tier1/ai-sdk-v7`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/ai-sdk-v7)。

`uiMessageStreamAgent(options)` 完整参数（`UiMessageStreamAgentOptions`）：

#### `name`

```ts theme={null}
name?: string;
```

agent 名(报告 / 结果聚合的身份)。默认 "ui-message-stream"。

#### `url`

```ts theme={null}
url: string | ((ctx: AgentContext) => string | Promise<string>);
```

被测应用的 chat 端点(完整 URL,应用在哪部署就指哪);函数形式每轮解析。

#### `headers`

```ts theme={null}
headers?: globalThis.Record<string, string> | ((ctx: AgentContext) => globalThis.Record<string, string>);
```

附加请求头(鉴权等);`ctx.telemetry.headers`(traceparent)总会自动并入。

#### `body`

```ts theme={null}
body?: (ctx: AgentContext) => globalThis.Record<string, JsonValue | undefined>;
```

除 `messages` 外并入请求体的字段,如 `(ctx) => ({ model: ctx.model })`(undefined 字段序列化时自动丢弃)。

#### `projectToolCommand`

```ts theme={null}
projectToolCommand?: (tool: Readonly<{ name: string; input: JsonValue }>) => CommandProjection | undefined;
```

由 endpoint owner 逐笔把逻辑工具调用分类为 command 或 not-command。
未知调用返回 undefined 并保持 actions coverage partial；NiceEval 不按名称或 input 猜测。

#### `denyReason`

```ts theme={null}
denyReason?: string;
```

拒绝审批时随 `approval-responded` 带出的理由。应用/SDK 会把它作为模型看到的工具结果
文本 —— 写清楚「不要重试」能明显降低模型原样重发同一调用的概率(实测)。

#### `settleMs`

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

流结束后再等这么久才返回(毫秒),给应用侧的观测导出(如 BatchSpanProcessor)留时间。

#### `tracing`

```ts theme={null}
tracing?: AgentTracing;
```

应用有 OTel 时的端点投递方式(拿瀑布图);事件流不依赖它。

#### `spanMapper`

```ts theme={null}
spanMapper?: SpanMapper;
```

应用有 OTel 时的 span 归一函数(拿瀑布图);事件流不依赖它。

## OpenAI 兼容响应转换器：`turnFromChatCompletion` / `turnFromResponses`

`turnFromChatCompletion(res)` / `turnFromResponses(res)`（`niceeval/adapter` 导出）把 OpenAI 的两种响应形状：Chat Completions 与 Responses。它会把整段响应零映射转成 `Turn`：不限于 OpenAI 官方，任何声明兼容这两种协议形状的服务都能用。

```ts theme={null}
import { completeEvidenceCoverage, defineAgent, turnFromChatCompletion } from "niceeval/adapter";

export default defineAgent({
  name: "my-openai-compat-agent",
  evidenceCoverage: completeEvidenceCoverage,
  async send(input) {
    const res = await client.chat.completions.create({ messages: [...], tools });
    return turnFromChatCompletion(res);
  },
});
```

两种形状对负断言（`notCalledTool` 等）的可信度不同：Chat Completions 不承诺「响应 = 完整过程」（应用可能在服务端跑完工具循环，只把最终答案给你），负断言只能当「没看到」，不能当「确实没发生」。Responses 的协议契约里 `output` 数组记录了模型这一轮决定做的全部事（包括每个 `function_call`），负断言可信。两个转换器产出的 `Turn` 形状本身相同，这条差异只体现在你对负断言的解读上。

## SDK 事件流转换器：`createClaudeSdkEventStream` / `createPiAgentEventStream` / `createCodexThreadEventStream`

各 Agent SDK 的流式协议由 SDK 定义，不是某个应用的私有格式。`niceeval/adapter` 已经提供原生帧到标准事件的转换器。自己编写非侵入式 Adapter 时，只需填写应用接口和审批接口的地址。`driveFrameStream`（见下一节）会逐帧读取结果，不需要再写 `for` 循环：

```ts theme={null}
import { sseJsonFrames, createClaudeSdkEventStream, driveFrameStream } from "niceeval/adapter";

const frames = sseJsonFrames<SDKMessage>(res.body);   // 标准 SSE → 逐帧 JSON
const stream = createClaudeSdkEventStream();               // SDKMessage → 标准事件
return driveFrameStream(frames, stream, ctx);         // 逐帧喂 stream.add()，汇总成 Turn
```

* **`createClaudeSdkEventStream`**（`SDKMessage`）：`assistant` 的 text/tool\_use 块、`user` 的 tool\_result 块、`system`/`permission_denied`（→ `rejected`）、`result` 的 usage。`markRejected()` 登记被拒调用。
* **`createPiAgentEventStream`**（pi-agent-core `AgentEvent`）：`message_end` 的文本/thinking/usage、`tool_execution_start/end` 的工具对。
* **`createCodexThreadEventStream`**（Codex SDK `ThreadEvent`）：`agent_message`/`reasoning` 的消息类。工具项包括：`command_execution` / `mcp_tool_call` / `file_change` / `web_search` → 配对的 tool `operation.started` + `operation.finished`。`turn.completed` 的 usage、错误帧。瀑布图要 codex CLI 原生 OTLP（config.toml 的 `[otel]` 块）把 span 发给 [NiceEval](https://niceeval.com/)，配官方 `mapCodexSpans` 归一。

可运行的参考实现位于 [`examples/zh/tier1`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1)。其中 Claude SDK、pi SDK 和 Codex SDK 的 Adapter 都由转换器、`driveFrameStream` 和应用接口配置组成。

## 通用「拼装方式」件：`driveFrameStream` / `deltaStream` + `ctx.session`

一个手写 send 里真正互不相干的只有三段：transport（怎么发）、reduce（原始数据 → 事件，上一节的转换器管这个）、编排（会话续接 + HITL 暂停恢复）。第三段和任何具体协议无关，纯粹是控制流模式。服务端历史用 `ctx.session.id` / `capture`，客户端历史和 HITL 停轮现场用 `createSessionSlot<T>()` 声明的 Adapter 私有 slot，经 `ctx.session.get` / `set` / `take` 存取（`AgentSession`，见 [Adapter 概念](/docs/zh/explanation/adapter#上下文：agentcontext)）。剩下真正跨协议复用的只有两个官方件：

| 件                                                  | 解决什么                                                                                                                                                    |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `driveFrameStream(cursor, reducer, ctx, onFrame?)` | 逐帧喂 reducer、处理传输帧、检测 HITL 暂停信号——循环本身收成一个函数。暂停时 `cursor` 不关闭，`onFrame` 里用 `ctx.session.set(slot, cursor)` 存住它，回答轮 `ctx.session.take(slot)` 取回接着读，不重新发起请求 |
| `deltaStream(spec)`                                | 逐 token / 逐参数增量的通用累加器——原始 OpenAI/Anthropic 流式 API、自己手写的 token-by-token 后端这类"没有整段落地帧"的协议用它。声明"这一帧对应哪个操作"（文本增量/工具参数增量/收尾），拼接时机它自己管                        |

```ts theme={null}
const pendingSlot = createSessionSlot<Pending>("my-adapter/pending");

// send 一进来先看有没有挂起的现场（HITL 回答轮）
const pending = ctx.session.take(pendingSlot);
if (pending) {
  // ……按 input.responses 对位取裁决、把审批交回应用……
  return driveFrameStream(pending.cursor, pending.stream, ctx, onFrame);
}

// 正常轮：会话续接走 ctx.session.id / capture，暂停时把 cursor 存进 typed slot
return driveFrameStream(cursor, stream, ctx, (frame, derived) => {
  ctx.session.capture(stream.sessionId);
  const gated = derived.find((event) =>
    event.type === "operation.started" &&
    event.operation.kind === "tool" &&
    event.operation.name === GATED_TOOL_NAME
  );
  if (gated?.type === "operation.started") {
    ctx.session.set(pendingSlot, { cursor, stream, toolUseId: gated.operationId });
    return { pause: { id: gated.operationId, action: GATED_TOOL_NAME, options: [{ id: "approve" }, { id: "deny" }] } };
  }
});
```

三个 Tier 1 示例（claude-sdk、pi-sdk 用 `driveFrameStream` + typed slot，codex-sdk 只用 `driveFrameStream`）已经用这套件重写。Adapter 只剩 transport 与 `onFrame` 里"这一帧要不要额外处理"的判断，没有手写循环和模块级 Map。

## `turnFromAiSdk`：AI SDK 结果 → 事件流转换器

`turnFromAiSdk`（`niceeval/adapter` 导出）在自己写的 adapter 里用（比如 HTTP web agent 的服务端直构，见 `examples/zh/ai-sdk/`）。它把 AI SDK 的 `generateText` / `streamText` 结果映射成 `{ events, usage, status }`。`toolCallId` 精确配对、时序保真、`tool-error` 映射成失败的 tool `operation.finished`、usage 聚合（v4 / v5 / v7 字段漂移都兜住）——直接铺进 `Turn`：

```ts theme={null}
import { completeEvidenceCoverage, defineAgent, turnFromAiSdk } from "niceeval/adapter";

export default defineAgent({
  name: "my-ai-sdk-agent",
  evidenceCoverage: completeEvidenceCoverage,
  async send(input) {
    const result = await generateText({ model, tools, prompt: input.text });
    return { ...turnFromAiSdk(result), data: result.text };
  },
});
```

`status` 由转换器给：有待人批准的 tool approval 时是 `"waiting"`（并附 `input.requested` 事件），否则 `"completed"`。会话续接（多轮 resume）、HITL 的裁决交回、tracing 取决于你自己的 `send` 怎么写。

## 怎么选

* 被测对象是 AI SDK 应用（`useChat` 后端）：内置 `uiMessageStreamAgent` 无侵入接入，零映射（含 HITL）。
* 被测对象是其它 agent 系统（HTTP / gRPC 服务）：无侵入接入，手写事件映射（官方转换器能覆盖大半）——[`examples/zh/tier1`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1) 下五个示例全是这条路。应用已埋 OTel 的话顺手接上，多一张瀑布图。
* 需要调用瀑布图（`niceeval view` 的 trace）：`claude-code` / `codex` / `bub` 三个都自发 OTLP（claude-code 走 CLI 的 beta 遥测，span 只有结构与计时）。自己写的 adapter 声明 `tracing` 即可（见 OTel 指南）。
* 需要人工审批 / 多步确认（HITL）：`uiMessageStreamAgent` 原生支持（AI SDK v7 tool approval）。Sandbox 型三个都不支持，其它被测对象自己写 Adapter，拿 `driveFrameStream` + typed slot 拼（Tier 1 示例里 pi-sdk / claude-sdk 有现成写法）。
* 想跑一个 coding agent 改代码、看 diff、判断工具调用：`claude-code` / `codex` / `bub`（收发 + 事件流 + 会话续接 + workspace + sandbox）。
* 后端协议是自己发明的、没有官方 SDK 转换器可用时，别从零手写 send。挑一种 reduce 形状（整段落地写小映射，逐 token 增量用 `deltaStream`），服务端会话续接用 `ctx.session.id` / `capture`，客户端历史或 HITL 现场用 typed slot，拼起来就是 send。

## 相关阅读

* [接入你的 agent](/docs/zh/tutorials/connect-your-agent) — 自己写 adapter 时每个能力怎么做、对应什么断言。
* [Sandbox Agent](/docs/zh/tutorials/sandbox-agent) — 怎么运行内置 sandbox agent，以及怎么写自己的。
* [Adapter 参考](/docs/zh/reference/define-agent) — `defineAgent` / `defineSandboxAgent` 完整参数。
* [OTel 接入](/docs/zh/tutorials/connect-otel) — 把应用的 span 发给 [NiceEval](https://niceeval.com/)，换 `niceeval view` 的调用瀑布图。
