niceeval 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +15 -5
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +36 -7
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +19 -6
  123. package/src/i18n/zh-CN.ts +19 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/reporters/artifacts.ts +11 -3
  128. package/src/runner/reporters/live.ts +45 -8
  129. package/src/runner/run.ts +71 -21
  130. package/src/runner/types.ts +45 -2
  131. package/src/sandbox/types.ts +18 -0
  132. package/src/scoring/types.ts +5 -0
  133. package/src/shared/types.ts +3 -0
  134. package/src/util.test.ts +26 -1
  135. package/src/util.ts +16 -0
  136. package/src/view/app/App.tsx +2 -2
  137. package/src/view/app/components/AttemptModal.tsx +2 -0
  138. package/src/view/app/components/CopyControls.tsx +87 -28
  139. package/src/view/app/i18n.ts +3 -3
  140. package/src/view/client-dist/app.css +1 -1
  141. package/src/view/client-dist/app.js +18 -18
  142. package/docs/README.md +0 -120
  143. package/docs/adapters/README.md +0 -60
  144. package/docs/adapters/authoring.md +0 -179
  145. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  146. package/docs/adapters/collection.md +0 -128
  147. package/docs/adapters/contract.md +0 -264
  148. package/docs/adapters/reference/agent-eval.md +0 -215
  149. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  150. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  151. package/docs/adapters/reference/eve-protocol.md +0 -127
  152. package/docs/adapters/reference/otel-genai.md +0 -107
  153. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  154. package/docs/adapters/targets.md +0 -99
  155. package/docs/architecture.md +0 -140
  156. package/docs/assertions.md +0 -387
  157. package/docs/capabilities-by-construction.md +0 -48
  158. package/docs/cli.md +0 -173
  159. package/docs/concepts.md +0 -106
  160. package/docs/e2e-ci.md +0 -332
  161. package/docs/eval-authoring.md +0 -319
  162. package/docs/experiments.md +0 -157
  163. package/docs/getting-started.md +0 -224
  164. package/docs/multi-agent.md +0 -145
  165. package/docs/observability.md +0 -337
  166. package/docs/origin-integration.md +0 -195
  167. package/docs/references.md +0 -35
  168. package/docs/reports.md +0 -551
  169. package/docs/results-format.md +0 -228
  170. package/docs/results-lib.md +0 -191
  171. package/docs/runner.md +0 -104
  172. package/docs/sandbox.md +0 -276
  173. package/docs/scoring.md +0 -119
  174. package/docs/source-map.md +0 -126
  175. package/docs/tier-sync.md +0 -193
  176. package/docs/view.md +0 -194
  177. package/docs/vision.md +0 -88
@@ -0,0 +1,286 @@
1
+ ---
2
+ title: "Adapter:把 Agent 接入 NiceEval"
3
+ sidebarTitle: "Adapter"
4
+ description: "Adapter 是你写的适配器。本文讲清楚 send 函数传入什么返回什么、被测系统配置怎么传、三个接入 Tier,以及 t 上的能力从哪来。"
5
+ ---
6
+
7
+ 在 [NiceEval](https://niceeval.com/) 里,`Adapter` 是连接运行器和被测系统的适配层。Adapter 需要实现 `send` 函数:把 `t.send()` 等 eval 侧动作发给你的应用,再把应用返回翻译成 [NiceEval](https://niceeval.com/) 的标准 `Turn`。
8
+
9
+ 后续阅读[写 send](/zh/guides/write-send)
10
+
11
+ ## 适配器
12
+ 如果被测系统使用标准的 OpenAI Chat Completions 或 Responses 协议,可以直接用官方适配器。自己实现的前后端协议(HTTP、gRPC、WebSocket 都行)则写一个 Adapter:它知道怎么鉴权、怎么调用你的应用、怎么把返回翻译成标准事件流。
13
+
14
+ 一次 `t.send()` 的完整往返,穿过这条边界两次:
15
+
16
+ ![一次 t.send 的完整往返:eval 调用 t.send,运行器组装 TurnInput 与 ctx,adapter 调用你的应用并返回标准事件流 Turn。](/images/agent-turn-roundtrip-zh.svg)
17
+
18
+ 去程:eval 里的驱动动词(`t.send` / `t.sendFile` / `t.respond`)组装 `TurnInput` 和本会话线的 `ctx`,调一次 Adapter 的 `send`。
19
+
20
+ 回程:Adapter 把应用的原始返回翻译成 `Turn`——其中 `events` 是一个按发生顺序排列的事件对象数组(下文有实际值)
21
+
22
+ 被测系统的 URL、鉴权、协议细节通过 Adapter 工厂的配置传入。`defineExperiment` 里放的是已经配置好的 agent 实例;Adapter 在 `send` 闭包里消费这些配置,再把每轮动态值(如 `ctx.model`、`ctx.flags`、`ctx.telemetry`)随请求转发给应用:
23
+
24
+ ```ts
25
+ // agents/web-agent.ts
26
+ import { defineAgent } from "niceeval/adapter";
27
+ import type { Agent } from "niceeval/adapter";
28
+
29
+ interface WebAgentOptions {
30
+ baseUrl: string;
31
+ apiKey?: string;
32
+ }
33
+
34
+ export function webAgent(options: WebAgentOptions): Agent {
35
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
36
+
37
+ return defineAgent({
38
+ name: "web-agent",
39
+ async send(input, ctx) {
40
+ const response = await fetch(`${baseUrl}/api/turn`, {
41
+ method: "POST",
42
+ headers: {
43
+ "content-type": "application/json",
44
+ ...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
45
+ ...ctx.telemetry?.headers,
46
+ },
47
+ body: JSON.stringify({
48
+ message: input.text,
49
+ files: input.files,
50
+ model: ctx.model,
51
+ flags: ctx.flags,
52
+ }),
53
+ signal: ctx.signal,
54
+ });
55
+
56
+ if (!response.ok) {
57
+ return {
58
+ status: "failed",
59
+ events: [{ type: "error", message: `HTTP ${response.status}` }],
60
+ };
61
+ }
62
+
63
+ const body = await response.json();
64
+ return {
65
+ status: "completed",
66
+ events: body.events,
67
+ data: body.data,
68
+ usage: body.usage,
69
+ };
70
+ },
71
+ });
72
+ }
73
+
74
+ // experiments/staging.ts
75
+ import { defineExperiment } from "niceeval";
76
+ import { webAgent } from "../agents/web-agent.ts";
77
+
78
+ export default defineExperiment({
79
+ agent: webAgent({
80
+ baseUrl: "https://staging.example.com",
81
+ apiKey: process.env.STAGING_AGENT_API_KEY,
82
+ }),
83
+ model: "gpt-5.4",
84
+ flags: { promptVariant: "concise" },
85
+ });
86
+ ```
87
+
88
+ ## 接入 Tier
89
+
90
+ 按「Adapter 接到哪里、额外拿到什么观测数据」,接入分三档:**Tier 1 只接 send**(应用代码一行不改,全套断言在这一档就齐了)、**Tier 2 send + OTel**(应用把 span 发给 [NiceEval](https://niceeval.com/),换 `niceeval view` 的调用瀑布图)、**Tier 3 侵入改造 + experiment flags**(feature A/B)。每档投入什么、买到什么、什么时候升级,见 [Tier](/zh/concepts/tier)。
91
+
92
+ eval 侧的驱动 API——`t.send()`、`t.sendFile()`、`t.newSession()`、HITL 的 `t.respond()` / `t.respondAll()`
93
+ 统一都是调用 Adapter 的 `send`。
94
+
95
+ 怎么收敛、send 里怎么接,见[写 send](/zh/guides/write-send)。
96
+
97
+ ## send 函数
98
+
99
+ 不管 Adapter 连的是 HTTP 服务还是沙箱里的 CLI,暴露给运行器的接口完全一致:
100
+
101
+ ```ts
102
+ interface Agent {
103
+ readonly name: string;
104
+ send(input: TurnInput, ctx: AgentContext): Promise<Turn>;
105
+ // 可选:setup / teardown(每沙箱一次的生命周期);
106
+ // 观测相关的可选成员不参与 send,见「OTel 接入」
107
+ }
108
+ ```
109
+
110
+ `send` 是唯一要实现的函数。它的签名里只有三个类型,分开看。
111
+
112
+ ### 传入:`TurnInput`
113
+
114
+ ```ts
115
+ interface TurnInput {
116
+ readonly text: string; // 每轮必有:发送的文本;回答轮 = 回答文本(多条以换行连接)
117
+ readonly files?: readonly InputFile[]; // 仅 t.sendFile 轮:附带的文件(base64)
118
+ readonly responses?: readonly InputResponse[]; // 仅回答轮(t.respond / t.respondAll):逐请求的结构化回答
119
+ readonly outputSchema?: JsonSchema; // 仅本轮声明了 output 时:结构化输出要求,已降为 JSON Schema
120
+ }
121
+
122
+ interface InputFile {
123
+ readonly filename?: string; // 可选,供 adapter / 模型参考
124
+ readonly mimeType: string; // 如 image/png
125
+ readonly dataBase64: string; // base64 内容,JSON 友好,可直接放进请求体
126
+ }
127
+
128
+ interface InputResponse {
129
+ readonly requestId: string; // 必有:对应哪条 input.requested 请求,多个请求并停时靠它对位
130
+ readonly optionId?: string; // 与 text 二选一:回答命中了请求 options 里的某个 id(approve / deny…)
131
+ readonly text?: string; // 与 optionId 二选一:自由文本回答(请求没有选项,或回答不是任何选项)
132
+ }
133
+ ```
134
+
135
+ eval 侧不管调的是 `t.send()`、`t.sendFile()` 还是 `t.respond()`,到 Adapter 时都是一次普通的 `send`,差别全在字段上:
136
+
137
+ 如果你的应用接口不收文件就忽略 `files`。
138
+
139
+ | eval 侧 | `text` | `files` | `responses` | `outputSchema` |
140
+ |---|---|---|---|---|
141
+ | `t.send(text)` | 发送文本 | — | — | 声明了 `output` 才有 |
142
+ | `t.sendFile(path, text?)` | 附言(可为空串) | 有 | — | 同上 |
143
+ | `t.respond(...)` / `t.respondAll(...)` | 回答文本 | — | 逐请求一条 | 同上 |
144
+
145
+ `files` 接口不收文件就忽略;
146
+ `outputSchema` 应用接口收 schema 就转发(Chat Completions 的 `response_format`、Responses 的 `text.format`),不收也没关系——校验反正发生在运行器(见下面 `Turn.data` 的规则)。
147
+
148
+ #### 不同回答的入参
149
+
150
+ HITL 回答轮里,人的裁决以结构化形式随 `input.responses` 到达——Adapter 不需要解析 `text` 去猜哪句回答对应哪个请求、算不算批准。每条回答必带 `requestId`;`optionId` 和 `text` 二选一:回答命中请求 `options` 里的某个 id 就是 `optionId`(eval 侧已校验过存在,不会有打错的字静默传进来),否则整句作为自由文本落在 `text`。四种典型形态:
151
+
152
+ ```ts
153
+ // ① 单个待处理请求,回答命中选项(approve / deny 同理,只是 optionId 不同)
154
+ await t.respond("approve");
155
+ // → { text: "approve",
156
+ // responses: [{ requestId: "req_1", optionId: "approve" }] }
157
+
158
+ // ② 多个请求并停,用对象形式显式对位
159
+ await t.respond({ request, optionId: "deny" });
160
+ // → { text: "deny",
161
+ // responses: [{ requestId: request.id, optionId: "deny" }] }
162
+
163
+ // ③ 回答不是任何选项 → 自由文本(等补充信息的请求就是这种)
164
+ await t.respond("收件人改成 ceo@corp.com");
165
+ // → { text: "收件人改成 ceo@corp.com",
166
+ // responses: [{ requestId: "req_1", text: "收件人改成 ceo@corp.com" }] }
167
+
168
+ // ④ respondAll:每条待处理请求一条回答,同一个 optionId
169
+ await t.respondAll("approve");
170
+ // → { text: "approve\napprove",
171
+ // responses: [{ requestId: "req_1", optionId: "approve" },
172
+ // { requestId: "req_2", optionId: "approve" }] }
173
+ ```
174
+
175
+ Adapter 侧的对应义务:按 `requestId` 把裁决交回应用(不要按顺序猜);被人拒绝的调用把 `action.result` 的 `status` 置 `"rejected"` 而不是 `"failed"`——拒绝是人的决定、不是工具故障,这样 `noFailedActions()` 不误伤,`calledTool(..., { status: "rejected" })` 可精确断言。
176
+
177
+ ### 上下文:`AgentContext`
178
+
179
+ ```ts
180
+ interface AgentContext {
181
+ // 每轮都在的三个
182
+ readonly signal: AbortSignal; // 运行器的超时与取消:透传给你发出的每个请求
183
+ readonly session: AgentSession; // 本条会话线的状态槽
184
+ log(msg: string): void; // 写进本轮日志,niceeval view 里可见
185
+
186
+ // 按接入档位出现的三个(见「接入分三个 Tier」)
187
+ readonly model?: string; // Tier 1 的模型对比:experiment.model 透传;应用接口收模型选择就转发
188
+ readonly reasoningEffort?: string; // 推理努力程度:experiment.reasoningEffort 透传,归属与 model 一致
189
+ readonly telemetry?: Telemetry; // Tier 2:本轮的 W3C trace context(headers,每轮一个新 traceparent)
190
+ readonly flags: Readonly<Record<string, unknown>>; // Tier 3 的 feature A/B:experiment.flags 透传;没配时是 {}
191
+
192
+ readonly sandbox?: Sandbox; // 仅 defineSandboxAgent 构造的 Agent 会拿到
193
+ }
194
+
195
+ interface AgentSession {
196
+ // 会话续接:服务端记历史(接口收会话 id 的形状)
197
+ readonly id?: string; // 本线记过的会话 id;新会话线是 undefined
198
+ capture(id: string | undefined): void; // 记回传的 id;只在还没记过时落地
199
+
200
+ // 会话续接:客户端带全量历史(Chat Completions 形)
201
+ history<TMsg>(): { get(): TMsg[]; commit(messages: TMsg[]): void };
202
+
203
+ // HITL:停轮现场的存取(take 取到即清除,一次消费)
204
+ hold<T>(state: T): void;
205
+ take<T>(): T | undefined;
206
+
207
+ // 逃生舱:自由状态槽,起始 {},框架从不写入
208
+ readonly state: Record<string, unknown>;
209
+ }
210
+ ```
211
+
212
+ `ctx` 里没有要你检查的标志位。三个档位字段都是"透传"语义:`model`、`flags` 由 experiment 声明、运行器原样递过来,Adapter 只负责随请求转发给应用,不解释它们的含义;`telemetry` 仅在配置了 OTel 接入时出现,`send` 里只需要把 `headers` spread 进请求头——接收端点在 `defineConfig` 里固定、由应用启动时指向,不从这里传,见[OTel 接入](/zh/guides/connect-otel)。
213
+
214
+ `session` 是本条会话线的自有状态,[NiceEval](https://niceeval.com/) 对它只承诺一件事:**同一条会话线的每次 `send` 拿到同一个 `ctx.session`,新会话线(eval 的第一轮,或 `t.newSession()` 之后)拿到一个全新的。** 会话续接(`id`/`capture`、`history`)和 HITL 停轮现场(`hold`/`take`)的存取器都在它上面,"第一轮"就是新会话线的自然形态——`id` 是 `undefined`、`history.get()` 是空数组,没有要判断的分支;`state` 是这些存取器之外的逃生舱,框架从不往里写数据。
215
+
216
+ ### 返回:`Turn`
217
+
218
+ ```ts
219
+ interface Turn {
220
+ readonly events: StreamEvent[]; // ★ 标准事件流——每个 Adapter 的核心产物
221
+ readonly data?: JsonValue; // 本轮结构化产物;仅当 input.outputSchema 出现时填,运行器按声明校验
222
+ readonly status: "completed" | "failed" | "waiting"; // waiting = 停轮等人(HITL)
223
+ readonly usage?: Usage;
224
+ }
225
+ ```
226
+
227
+ `events` 就是一个**普通的 JS 数组**:这一轮里发生的每件事一个对象,按真实发生顺序排列。比如"北京今天多少度?"这一轮,agent 先查天气再回答,`events` 长这样:
228
+
229
+ ```ts
230
+ [
231
+ { type: "action.called", callId: "c1", name: "get_weather", input: { city: "北京" } },
232
+ { type: "action.result", callId: "c1", output: { temp: 21 }, status: "completed" },
233
+ { type: "message", role: "assistant", text: "北京今天 21°C。" },
234
+ ]
235
+ ```
236
+
237
+ 对象统共九种类型(`message`、`action.*`、`input.requested`……完整清单见[事件流参考](/zh/reference/events))。断言读的就是这个数组:`t.calledTool("get_weather")` 数 `action.called`,`t.reply` 取最后一条 assistant `message`。注意 `send` 每次只返回**本轮**的数组——跨轮拼成整条会话线是运行器的事,见下一节。多数情况下你也不手写这些对象:官方转换器的返回值就是一个填好 `events`、`usage`、`status` 的完整 `Turn`,怎么选转换器见[写 send](/zh/guides/write-send)。
238
+
239
+ `data` 不是"随便放点什么"的口袋,它只有一条规则:**要什么,由 eval 在 send 上用 schema 声明;声明了才有 data,拿到手就是声明的类型。**
240
+
241
+ ```ts
242
+ // eval 侧:output 声明既是给应用的要求,也是 turn.data 的类型来源
243
+ const turn = await t.send("这张发票总额多少?", {
244
+ output: z.object({ amount: z.number(), currency: z.string() }),
245
+ });
246
+ turn.data.amount; // 类型是 number——不是 unknown,不用手动收窄
247
+ ```
248
+
249
+ 声明降为 JSON Schema 后经 `input.outputSchema` 到达 Adapter;Adapter 拿到应用的返回后把结构化产物放进 `data`(比如 `JSON.parse(reply.content)`)。**校验是运行器强制的**:`data` 不匹配声明,本轮直接 `failed` 并报出差异——不存在"塞了个错的对象、断言静默通过"这条路。反过来,没声明 `output` 的轮次没有 `data`:只回文本的应用永远不碰这个字段,不要把原始响应 body 复制进去凑数。
250
+
251
+ ### send 返回之后:Turn 的四个字段各去哪
252
+
253
+ `send` 一 return,Adapter 的工作就结束了——剩下全是运行器的事。四个字段各有明确去向:
254
+
255
+ | Turn 字段 | 运行器拿它做什么 | eval 作者在哪感知到 |
256
+ |---|---|---|
257
+ | `events` | 追加进本会话线的事件流,`deriveRunFacts` 折叠成事实(`toolCalls`、`parked`、`messageCount`…);最后一条 assistant `message` 同时更新 `t.reply` | 全部作用域断言:`t.calledTool()`、`t.messageIncludes()`…;`t.events` 可直接查 |
258
+ | `status` | 记录本轮终态;`"waiting"` 时把本轮的 `input.requested` 收进待答列表 | `t.succeeded()` / `t.parked()`;`"waiting"` 之后用 `t.respond()` 接 |
259
+ | `usage` | 逐轮累加到会话线与整次运行 | `maxTokens` / `maxCost` 评分器、报告里的用量 |
260
+ | `data` | 按 send 上声明的 `output` schema 校验——不匹配本轮直接 `failed`;通过才保存 | `turn.data`(按声明强类型)、`outputEquals` |
261
+
262
+ `await t.send()` 拿到的句柄就是对这一轮 Turn 的视角:turn 级断言(`draft.parked()` 这类)只看这一轮的 `events`,`t` 级断言看整条会话线累计的。所以 Adapter 不需要"配合"任何断言——把这四个字段填对,断言、评分、报告全部在下游自动发生。
263
+
264
+ ## 能力从哪来:构造证明,不是问卷
265
+
266
+
267
+ | `t` 上解锁什么 | 证据是什么 | 你写什么 |
268
+ |---|---|---|
269
+ | `t.send`、`t.sendFile`、`t.check`、`t.judge`… | 任何 Agent 都有 | 无 |
270
+ | 多次 `t.send()`、`t.reply`、`t.newSession()` | `send` 里接了 `ctx.session` 的续接存取器(`history()` 或 `id` + `capture()`)——没接则每轮各是新对话 | 无——存取器就在 `ctx` 上 |
271
+ | `t.respond()` / `t.parked()` 等 HITL | send 返回过 `"waiting"` + `input.requested` 事件 | 无——做到了就是有 |
272
+ | `t.calledTool()` / `t.toolOrder()` 等正断言 | events 里有 `action.*` | 无——有事件就能断 |
273
+ | `t.notCalledTool()` / `t.usedNoTools()` 等负断言**可信** | 事件来自带完整性证明的官方转换器 | 无——证明随转换器返回值走 |
274
+ | `t.sandbox`、`t.sandbox.fileChanged()` 等 | `defineSandboxAgent` 构造 | 无 |
275
+ | `EvalResult.trace`、`niceeval view` 瀑布图 | `tracing` 块存在(span 只进瀑布图,不喂断言) | 无——块本来就要写 |
276
+
277
+ 负断言那行是唯一分档次的:`notCalledTool` 断的是"没发生",前提是"事件全量"——这只有来源本身有完整性契约时才成立(SDK 原生事件流透传、AI SDK 的 `result.steps`、Responses 的 `output`)。手写映射没有证明,负断言会在运行时提示不可信,而不是静默假通过。所以也不存在"声明了做不到"的失真:可信度跟着事件的来源走,不跟作者的自觉走。逐能力的精确义务见[能力参考](/zh/reference/capabilities)。
278
+
279
+
280
+ ## 相关阅读
281
+
282
+ - [写 send](/zh/guides/write-send) — 实操教程:从发一条消息到完整接入,七步递进,每步解锁一组断言。
283
+ - [接入你的 Agent](/zh/guides/connect-your-agent) — 接入全景:最小接入、参数通道与增量地图。
284
+ - [Drive](/zh/concepts/drive) — eval 侧视角:`t.send()`、`t.newSession()` 与 HITL 怎么用。
285
+ - [Assert](/zh/concepts/assert) — 标准事件流驱动的完整断言词汇。
286
+ - [架构概览](/zh/concepts/overview) — 四层架构和边界。
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: "NiceEval 断言:值、作用域事实、测试与成本"
3
+ sidebarTitle: "断言"
4
+ description: "NiceEval 的断言词汇——值断言、作用域断言、test-as-scoring 和效率断言——以及 gate / soft 严重级和判决 outcome 的规则。"
5
+ ---
6
+
7
+ 断言把 agent 在一次 eval 里做的一切——每条消息、每次工具调用、每处文件改动、每一分 token——折叠成一个可解释的结果。[NiceEval](https://niceeval.com/) 提供四种互补的断言机制:有的立即检查一个值,有的在整轮跑完后评估整次运行,有的在沙箱里跑测试,有的衡量效率。四种都产出同一种 `Assertion` 类型,都进同一套判决规则。第五种机制——让语言模型评判开放式质量——见 [Judge](/zh/concepts/judge)。
8
+
9
+ ## 四种断言机制
10
+
11
+ <CardGroup cols={2}>
12
+ <Card title="1. 值断言" icon="equals">
13
+ `t.check(value, matcher)` 和 `t.require(value, matcher)` 立即针对 `niceeval/expect` 的匹配器评估一个具体值,适合能就地验证的事实。
14
+ </Card>
15
+ <Card title="2. 作用域断言" icon="crosshairs">
16
+ `t.succeeded()`、`t.calledTool()`、`t.messageIncludes()` 等在 `test(t)` 里注册,但在函数返回**之后**才对完整轮次数据统一评估,适合整次运行的事实。
17
+ </Card>
18
+ <Card title="3. Test-as-scoring" icon="flask">
19
+ 在 sandbox eval 里,从 `test(t)` 跑项目测试、构建脚本或临时探针命令——适合代码任务,文件内容和构建结果就是事实标准。
20
+ </Card>
21
+ <Card title="4. 效率断言" icon="gauge">
22
+ `t.maxTokens()` 和 `t.maxCost()` 把 token 用量和估算成本变成可评分的维度。答对了但烧掉十倍 token 的 agent,不该跟省着用的拿一样的分。
23
+ </Card>
24
+ </CardGroup>
25
+
26
+ ## gate 与 soft 严重级
27
+
28
+ 每条断言都带一个**严重级**,决定它如何影响最终判决,只有两档:
29
+
30
+ <Tabs>
31
+ <Tab title="gate">
32
+ gate 是硬性要求。一旦失败,整个 eval 立刻判为 `failed`——不管其它断言表现如何。适合必须为真的事实:“调用了正确的工具”“输出解析为合法 JSON”“没有 shell 命令报错”。
33
+
34
+ `niceeval/expect` 里大多数匹配器(`includes`、`equals`、`matches`、`satisfies`)默认 gate;`t.succeeded()`、`t.calledTool()` 这类作用域断言也默认 gate。
35
+ </Tab>
36
+ <Tab title="soft">
37
+ soft 是带数值阈值的质量分。分数低于阈值时,eval 变成 `passed` 而不是 `failed`——是质量回归的信号,但不算硬性破坏。soft 失败只在 `--strict` 下才算数。
38
+
39
+ 适合“好不好”而不是“对不对”的连续判断:相似度打分、LLM-as-judge 的事实性评分、想跟踪但不想拦 CI 的成本预算。
40
+
41
+ 产出连续分数的匹配器(`similarity`)和所有 judge 调用默认走 soft。
42
+ </Tab>
43
+ </Tabs>
44
+
45
+ 在任意匹配器或断言上用链式方法覆盖默认严重级:
46
+
47
+ ```ts
48
+ t.check(t.reply, includes("confirmed")); // 默认 gate
49
+ t.check(t.reply, similarity(expected).gate()); // 提升为 gate
50
+ t.maxTokens(80_000).atLeast(0.7); // 降为 soft 并带阈值
51
+ ```
52
+
53
+ ## 判决规则
54
+
55
+ 所有断言收齐后,运行器按这个顺序折叠成一个结果:
56
+
57
+ ```
58
+ 执行出错(超时 / 异常 / 作者错误) → failed
59
+ 显式调用了 t.skip(reason) → skipped
60
+ 任一 gate 断言失败 → failed
61
+ 所有 gate 都过,但至少一个 soft 低于阈值 → passed(--strict 下标红)
62
+ 否则 → passed
63
+ ```
64
+
65
+ <CardGroup cols={2}>
66
+ <Card title="passed" icon="circle-check" color="#22c55e">
67
+ 没有错误,所有 gate 断言通过,所有 soft 断言都达到阈值。
68
+ </Card>
69
+ <Card title="failed" icon="circle-xmark" color="#ef4444">
70
+ 执行出错,或至少一个 gate 断言没通过。硬失败。
71
+ </Card>
72
+ <Card title="passed(计分)" icon="chart-bar" color="#f59e0b">
73
+ 所有 gate 都过,但至少一个 soft 低于阈值。质量回归——默认不报红,`--strict` 下才报红。
74
+ </Card>
75
+ <Card title="skipped" icon="forward" color="#6b7280">
76
+ 调用了 `t.skip("reason")`。完全排除在通过率统计之外。
77
+ </Card>
78
+ </CardGroup>
79
+
80
+ 多次运行(`runs > 1`)时,单个 eval 的汇总变成**通过率**(产出 `passed` 的运行占比)和平均耗时,而不是单一 outcome。
81
+
82
+ ## 1. 值断言 —— `niceeval/expect` 匹配器
83
+
84
+ `t.check(value, assertion)` 立即评估断言并记录结果。`t.require(value, assertion)` 做同样的事,但断言失败时**立即抛出**,中止 `test` 函数剩余部分——适合前置条件:一个必要事实不成立就没必要继续跑。
85
+
86
+ `niceeval/expect` 提供的匹配器:
87
+
88
+ ```ts
89
+ import {
90
+ includes, // 子串或正则匹配 (默认: gate)
91
+ equals, // 深度相等 (默认: gate)
92
+ matches, // Standard Schema(Zod 等)校验 (默认: gate)
93
+ similarity, // 归一化 Levenshtein 0–1 (默认: soft)
94
+ satisfies, // 自定义谓词 + 标签 (默认: gate)
95
+ } from "niceeval/expect";
96
+ ```
97
+
98
+ 用法示例:
99
+
100
+ ```ts
101
+ // 检查回复包含某个字符串
102
+ t.check(t.reply, includes("订单已确认"));
103
+
104
+ // 结构化输出的深度相等
105
+ t.check(turn.data, equals({ status: "refund", amount: 42 }));
106
+
107
+ // 用 Zod schema 校验结构化输出
108
+ t.check(turn.data, matches(z.object({ intent: z.enum(["refund", "ship"]) })));
109
+
110
+ // 带显式阈值的相似度
111
+ t.check(t.reply, similarity("期望答案").atLeast(0.8));
112
+
113
+ // 自定义谓词
114
+ t.check(turn.data, satisfies((d) => d.total > 0, "total is positive"));
115
+ ```
116
+
117
+ 匹配器是纯函数——`(value) => number`——所以你可以自己写一个,不需要任何特殊注册就能传给 `t.check`。
118
+
119
+ ## 2. 作用域断言
120
+
121
+ 作用域断言在 `test(t)` 里注册,但在函数返回**之后**才对累积完的完整轮次数据评估。它们读的是 `t.send()` 产出的标准事件流(见 [Drive](/zh/concepts/drive))及其派生事实——所以只要你的 adapter 产出正确的事件,这些断言对任何 agent 都一样好用。
122
+
123
+ <Warning>
124
+ 作用域断言只有在 agent 声明了对应能力时才出现在 `t` 上。agent 没声明 `toolObservability: true` 时调用 `t.calledTool()` 是编译期报错。
125
+ </Warning>
126
+
127
+ ### 运行 / session 维度
128
+
129
+ ```ts
130
+ t.succeeded(); // 运行完成,没有失败的 action,没有未解决的 HITL
131
+ t.parked(); // 干净地停在 HITL 的 input.requested 事件上
132
+ t.messageIncludes("此致"); // 拼接全部 message 事件后包含这个字符串/正则
133
+ ```
134
+
135
+ ### 工具 / action 维度
136
+
137
+ ```ts
138
+ t.calledTool("bash", { input: { command: /^pwd/ }, count: 1 });
139
+ t.notCalledTool("shell", { input: { command: /npm i/ } });
140
+ t.toolOrder(["read_file", "write_file"]); // 工具调用的相对顺序
141
+ t.usedNoTools();
142
+ t.maxToolCalls(5);
143
+ t.loadedSkill("memory-v2"); // calledTool("load_skill", ...) 的语法糖
144
+ t.calledSubagent("researcher", { remoteUrl: /api\.example/ });
145
+ t.noFailedActions(); // 没有工具、子 agent 或 skill 状态是 "failed"
146
+ ```
147
+
148
+ `calledTool` / `notCalledTool` 的 `input` 参数支持一套小型匹配语言:普通对象做深度部分匹配,`RegExp` 匹配序列化后的输入,谓词函数拿到原始 input 值。
149
+
150
+ ### 事件流维度(低层逃生舱)
151
+
152
+ ```ts
153
+ t.event("input.requested", { count: 1 });
154
+ t.notEvent("error");
155
+ t.eventOrder(["action.called", "subagent.called"]);
156
+ t.eventsSatisfy("read before write", (events) => /* 自定义谓词 */ true);
157
+ ```
158
+
159
+ 以上所有作用域断言都是这几条低层事件流查询的语法糖。找不到合适的高层断言时,可以降到 `eventsSatisfy`,对原始 `StreamEvent[]` 写任意谓词。
160
+
161
+ ### 结构化输出(挂在 `turn` 上,不是 `t`)
162
+
163
+ ```ts
164
+ const turn = await t.send("把结果按 JSON 返回");
165
+ turn.outputEquals({ status: "ok" }); // turn.data 的深度相等
166
+ turn.outputMatches(z.object({ status: z.string() })); // Standard Schema 校验
167
+ ```
168
+
169
+ ### 工作区维度(仅 sandbox agent)
170
+
171
+ ```ts
172
+ t.sandbox.fileChanged("src/Button.tsx");
173
+ t.sandbox.fileDeleted("src/old.ts");
174
+ t.sandbox.diff.isEmpty(); // 这一轮没有仓库文件被改动
175
+ t.sandbox.notInDiff(/sk-[A-Za-z0-9]/); // diff 里不含密钥 / 内联样式
176
+ t.check(await t.sandbox.runCommand("npm", ["test"]), commandSucceeded()); // npm test 退出码 0
177
+ t.check(await t.sandbox.runCommand("npm", ["run", "build"]), commandSucceeded()); // npm run build 退出码 0
178
+ t.sandbox.noFailedShellCommands();
179
+ ```
180
+
181
+ `t.sandbox.diff` 是可查询对象:`t.sandbox.diff.get("src/Button.tsx")` 返回文件改动后的内容;`t.sandbox.diff.isEmpty()` 检查有没有文件变化;`t.sandbox.diff.matches(re)` 和 `t.sandbox.notInDiff(re)` 对完整 diff 文本跑正则。
182
+
183
+ 作用域断言到处遵守同一条规则:**接收者决定作用域,不是断言名字决定作用域。** `t.*` 聚合这次 eval run 的全部轮次(含 `t.newSession()` 开的额外 session);`session.*`(`t.newSession()` 的返回值)只看这一条 session;`turn.*`(`t.send()` 的返回值)只看这一轮自己。同一套词汇,不同接收者——各接收者是什么见 [Drive](/zh/concepts/drive)。
184
+
185
+ ## 3. Test-as-scoring(沙箱型 eval)
186
+
187
+ 沙箱型代码 eval 里,在 `test(t)` 内跑验证命令,把结果记成断言:
188
+
189
+ ```ts
190
+ import { commandSucceeded, includes } from "niceeval/expect";
191
+
192
+ const testResult = await t.sandbox.runCommand("npm", ["test"]);
193
+ t.check(testResult, commandSucceeded());
194
+
195
+ const src = await t.sandbox.readSourceFiles({ extensions: ["ts"] });
196
+ t.check(src.text(), includes(/z\.object\s*\(/));
197
+ ```
198
+
199
+ 也可以通过标准事件流断言行为:`t.calledTool(...)`、`t.sandbox.noFailedShellCommands()`、`t.eventsSatisfy(...)`,以及 `t.sandbox.fileChanged(...)` 这类 diff 断言。
200
+
201
+ ## 4. 效率 / 成本断言
202
+
203
+ token 用量是一等评分维度。答对了但花费远超预期的 agent,不该跟省着用的拿一样的分。
204
+
205
+ ```ts
206
+ t.maxTokens(50_000); // 整次运行的硬 token 上限(默认 gate)
207
+ t.maxCost(0.5); // 估算成本上限,美元(需要在配置里提供价格表)
208
+ t.maxTokens(80_000).atLeast(0.7); // soft 变体——照样跟踪,只有 --strict 下才报红
209
+ t.check(t.usage.outputTokens, satisfies((n) => n < 10_000, "not verbose"));
210
+ ```
211
+
212
+ `t.usage` 在 `test(t)` 里随处可用,暴露 `{ inputTokens, outputTokens, cacheReadTokens?, … }`。沙箱型 agent 的 token 数由 adapter 从 transcript 里抠出;远程 agent 直接在 `Turn.usage` 里返回。
213
+
214
+ ## 自定义评分器
215
+
216
+ 值断言就是一个函数 `(value) => number | Promise<number>`,用 `makeAssertion` 自己写:
217
+
218
+ ```ts
219
+ import { makeAssertion } from "niceeval/expect";
220
+ import type { Assertion } from "niceeval/expect";
221
+
222
+ function jsonValid(): Assertion {
223
+ return makeAssertion({
224
+ name: "jsonValid",
225
+ severity: "gate",
226
+ score: (value) => {
227
+ try { JSON.parse(String(value)); return 1; }
228
+ catch { return 0; }
229
+ },
230
+ });
231
+ }
232
+
233
+ t.check(t.reply, jsonValid());
234
+ ```
235
+
236
+ 自定义匹配器和内置的一样支持链式方法:`.gate()`、`.atLeast(0.7)`。
237
+
238
+ ## 相关阅读
239
+
240
+ - [Drive](/zh/concepts/drive) — `t.send()`、`t.newSession()` 和 HITL:这些断言读的 Turn 数据是怎么产出的。
241
+ - [Judge](/zh/concepts/judge) — 第五种评分机制,评无法写成固定规则的开放式质量。
242
+ - [写 send](/zh/guides/write-send) — 标准事件流如何产出,作用域断言依赖它什么。
243
+ - [Evals](/zh/concepts/evals) — 断言如何折进 eval 生命周期和 outcome 类型。
@@ -0,0 +1,118 @@
1
+ ---
2
+ title: "NiceEval 里怎么驱动 agent:send、session 与 HITL"
3
+ sidebarTitle: "驱动"
4
+ description: "t.send() 和它返回的 Turn、t.sendFile()、多轮对话、t.newSession(),以及 respond() 处理的人工介入(HITL)。"
5
+ ---
6
+
7
+ 在断言或 judge 之前,先要让 agent 动起来。**Drive** 是 `test(t)` 里发送输入、拿到结果的那部分——`t.send()`、`t.sendFile()`、`t.newSession()`,以及 HITL 的 `t.respond()` / `t.respondAll()`。驱动产出的都是一个 **Turn**,[NiceEval](https://niceeval.com/) 里的所有断言和 judge 都从 Turn 的数据上读,具体看 [Assert](/zh/concepts/assert) 和 [Judge](/zh/concepts/judge)。
8
+
9
+ ## `t.send()` 和它返回的 `Turn`
10
+
11
+ `t.send(input)` 是唯一的动词。它底层调用 Adapter 的 `send(input, ctx)`,把返回值归一成一个 `Turn`:
12
+
13
+ ```ts
14
+ const turn = await t.send("布鲁克林今天天气怎么样?");
15
+
16
+ turn.message; // 最后一条 assistant 消息
17
+ turn.data; // 结构化产物:仅当这次 send 声明了 output 时存在,类型随声明(见下)
18
+ turn.status; // "completed" | "failed" | "waiting"
19
+ turn.events; // 这一轮的 StreamEvent[]
20
+ turn.usage; // { inputTokens, outputTokens, ... },adapter 报了才有
21
+ turn.succeeded(); // 作用域断言:这一轮没失败、也没卡在未回答的 HITL 上
22
+ ```
23
+
24
+ `t.reply` 是主 session 最后一条 assistant 消息的简写,等价于最近一次 `t.send()` 返回的 `turn.message`;`t.events` 是主 session 目前累积的完整事件流。
25
+
26
+ 要结构化输出时,在 send 上声明 `output`(Standard Schema,zod 即可)。声明既是给应用的要求,也是 `turn.data` 的类型来源:
27
+
28
+ ```ts
29
+ const turn = await t.send("这张发票总额多少?", {
30
+ output: z.object({ amount: z.number(), currency: z.string() }),
31
+ });
32
+ t.check(turn.data.amount, equals(3200)); // turn.data 是 { amount: number; currency: string }
33
+ ```
34
+
35
+ adapter 返回后运行器立刻按声明校验:`data` 不匹配,本轮直接 `failed` 并报出差异。没声明 `output` 的轮次没有 `turn.data`——它不是"agent 顺手返回点什么"的口袋,而是你声明了才存在、存在就强类型的字段。
36
+
37
+ <Tip>
38
+ `turn.succeeded()` 只看这一轮:检查这一轮的 `status`,以及是否卡在未回答的 HITL 请求上。后面几轮依赖前一轮成功时,紧跟着这一轮调它——在一个已经失败的轮次上继续挂断言,通常只会产出一堆混乱的连带失败。
39
+ </Tip>
40
+
41
+ ## 带文件的一轮 —— `t.sendFile()`
42
+
43
+ `t.sendFile(path, text?)` 读一个本地文件(相对 eval 所在目录),按扩展名推断 MIME 类型,把它作为 data URL 附加到这一轮的输入里:
44
+
45
+ ```ts
46
+ const turn = await t.sendFile("fixtures/invoice.png", "这张发票的总额是多少?");
47
+ t.check(turn.message, includes("$"));
48
+ ```
49
+
50
+ ## 多轮对话
51
+
52
+ 每次 `await t.send(...)` 都是同一个对话上的新一轮。把每轮的返回赋给局部变量,除了 run 级的 `t` 断言外还能单独断言这一轮:
53
+
54
+ ```ts
55
+ const draft = await t.send("帮我拟一封跟进邮件。");
56
+ draft.succeeded();
57
+ t.check(draft.message, includes("此致"));
58
+ draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);
59
+
60
+ await t.send("好,发出去。");
61
+ t.calledTool("send_email");
62
+ ```
63
+
64
+ 多轮 `t.send()` 能不能真的续上上文,取决于 Adapter 的 `send` 是否接了 `ctx.session` 的会话续接存取器(`history()` 或 `id` + `capture()`)——没接时每轮各是一场新对话。怎么接见 [Adapter](/zh/concepts/adapter) 与[写 send](/zh/guides/write-send)。
65
+
66
+ ## 独立会话 —— `t.newSession()`
67
+
68
+ `t.newSession()` 开一条与主会话并行、互不干扰的第二条对话线。它返回的 session handle 带同一套 drive API(`send` / `sendFile` / `respond` / `respondAll`)和同一套作用域断言词汇,但只看这条 session 自己的事件:
69
+
70
+ ```ts
71
+ await t.send("我叫小明。");
72
+ await t.send("我叫什么?");
73
+ t.check(t.reply, includes("小明")); // 主 session 记住了
74
+
75
+ const fresh = t.newSession();
76
+ await fresh.send("我叫什么?");
77
+ t.check(fresh.reply, satisfies((r) => !r.includes("小明"), "没有记忆泄漏"));
78
+ ```
79
+
80
+ <Warning>
81
+ 常见错误是想当然认为 `t.newSession()` 天然保证隔离。运行器保证的只是新会话线的状态是空的——真正对应用开一条新对话是 adapter 的事。一个不看自己的会话状态、永远续接同一个底层上下文的 adapter,会让 `t.newSession()` 悄悄共享状态,且不会报错。写 adapter 时,先用上面这条 eval 验一下隔离,再信任它。
82
+ </Warning>
83
+
84
+ ## 人工介入(HITL)
85
+
86
+ 有些 agent 会在一轮执行中间停下来,等审批或缺失信息,而不是直接跑完。这时这一轮以 `status: "waiting"` 结束,并带一条或多条 `input.requested` 事件说明在等什么。完整的心智模型(握手时序、Adapter 侧义务、rejected 语义)见 [HITL](/zh/concepts/hitl),这里讲 eval 侧怎么用。
87
+
88
+ ```ts
89
+ const draft = await t.send("拟一封跟进邮件,但先别发,等我确认。");
90
+ draft.parked(); // t.parked() 断言 status === "waiting"
91
+
92
+ const request = t.requireInputRequest({
93
+ prompt: /是否发送/,
94
+ optionIds: ["approve", "reject"],
95
+ });
96
+
97
+ await t.respond({ request, optionId: "approve" });
98
+ t.calledTool("send_email");
99
+ ```
100
+
101
+ `t.requireInputRequest(filter)` 把一个待处理的 HITL 请求变成可检查、可回应的具体值——如果匹配到 0 个或超过 1 个待处理请求就会抛出,所以尽量把能填的 filter 字段都填上(`id` / `prompt` / `display` / `action` / `optionIds` / `input`)来消歧。`t.respond(...)` 回答它并发出下一轮;每个参数是一条回答:字符串按待处理请求的顺序对位,`{ request, optionId }` 对象形式显式指名(多个请求并停时用它)。在底层它只是又一次普通的 `send`:回答文本进 `input.text`,同时以结构化形式逐条进 `input.responses`——命中请求选项的回答带 `{ requestId, optionId }`,自由文本回答带 `{ requestId, text }`,adapter 不用解析文本,按 `requestId` 就能对上"哪个回答给哪个请求"。每种回答到 adapter 长什么样,见[不同回答的入参](/zh/concepts/adapter#不同回答的入参)。
102
+
103
+ 如果当前轮有多个同类待处理请求、都该给同一个答案(比如逐个批准一批文件改动),用 `t.respondAll(optionId)` 一次性处理,不用挨个解。`optionId` 会先对每条待处理请求校验——不在请求的 `options` 里就直接抛错,打错的字不会被静默当成别的答案发出去:
104
+
105
+ ```ts
106
+ await t.send("把这批改动逐项提交审批。");
107
+ t.requireInputRequest({ display: /审批/ });
108
+
109
+ await t.respondAll("approve");
110
+ t.succeeded();
111
+ ```
112
+
113
+ ## 相关阅读
114
+
115
+ - [HITL](/zh/concepts/hitl) — 停轮等人的完整概念:握手时序与两侧义务。
116
+ - [Assert](/zh/concepts/assert) — 从 `Turn.events` 和 `Turn.data` 上读的断言词汇。
117
+ - [Judge](/zh/concepts/judge) — `t.judge` / `session.judge` / `turn.judge` 各自默认评什么材料。
118
+ - [Adapter](/zh/concepts/adapter) — 能力从哪来:什么解锁 `t.newSession()`、HITL 和工具相关断言。