niceeval 0.4.3 → 0.4.5

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 (179) 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 +1 -1
  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 +40 -10
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +20 -6
  123. package/src/i18n/zh-CN.ts +20 -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/attempt.ts +2 -1
  128. package/src/runner/reporters/artifacts.ts +11 -3
  129. package/src/runner/reporters/live.ts +45 -8
  130. package/src/runner/run.test.ts +50 -0
  131. package/src/runner/run.ts +135 -41
  132. package/src/runner/types.ts +55 -2
  133. package/src/sandbox/types.ts +18 -0
  134. package/src/scoring/types.ts +5 -0
  135. package/src/shared/types.ts +3 -0
  136. package/src/util.test.ts +26 -1
  137. package/src/util.ts +16 -0
  138. package/src/view/app/App.tsx +2 -2
  139. package/src/view/app/components/AttemptModal.tsx +2 -0
  140. package/src/view/app/components/CopyControls.tsx +87 -28
  141. package/src/view/app/i18n.ts +3 -3
  142. package/src/view/client-dist/app.css +1 -1
  143. package/src/view/client-dist/app.js +18 -18
  144. package/docs/README.md +0 -120
  145. package/docs/adapters/README.md +0 -60
  146. package/docs/adapters/authoring.md +0 -179
  147. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  148. package/docs/adapters/collection.md +0 -128
  149. package/docs/adapters/contract.md +0 -264
  150. package/docs/adapters/reference/agent-eval.md +0 -215
  151. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  152. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  153. package/docs/adapters/reference/eve-protocol.md +0 -127
  154. package/docs/adapters/reference/otel-genai.md +0 -107
  155. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  156. package/docs/adapters/targets.md +0 -99
  157. package/docs/architecture.md +0 -140
  158. package/docs/assertions.md +0 -387
  159. package/docs/capabilities-by-construction.md +0 -48
  160. package/docs/cli.md +0 -173
  161. package/docs/concepts.md +0 -106
  162. package/docs/e2e-ci.md +0 -332
  163. package/docs/eval-authoring.md +0 -319
  164. package/docs/experiments.md +0 -157
  165. package/docs/getting-started.md +0 -224
  166. package/docs/multi-agent.md +0 -145
  167. package/docs/observability.md +0 -337
  168. package/docs/origin-integration.md +0 -195
  169. package/docs/references.md +0 -35
  170. package/docs/reports.md +0 -551
  171. package/docs/results-format.md +0 -228
  172. package/docs/results-lib.md +0 -191
  173. package/docs/runner.md +0 -104
  174. package/docs/sandbox.md +0 -276
  175. package/docs/scoring.md +0 -119
  176. package/docs/source-map.md +0 -126
  177. package/docs/tier-sync.md +0 -193
  178. package/docs/view.md +0 -194
  179. package/docs/vision.md +0 -88
@@ -0,0 +1,252 @@
1
+ ---
2
+ title: "标准事件流参考"
3
+ sidebarTitle: "事件流"
4
+ description: "StreamEvent 的九种事件:每种的字段、什么时候吐、哪些断言消费它。adapter 的核心工作就是产出这条流。"
5
+ ---
6
+
7
+ adapter 的 `send` 返回一个 `Turn`,其中 `events: StreamEvent[]` 是**断言的唯一数据源**:`t.calledTool()`、`t.reply`、`toolOrder`、`noFailedActions`……全部从这条流上读。把你的 agent"这一轮做了什么"翻成这条流,整套断言就都能用。
8
+
9
+ ## Turn:send 的返回值
10
+
11
+ ```ts
12
+ interface Turn {
13
+ events: StreamEvent[]; // 本轮事件,按真实发生顺序
14
+ data?: unknown; // 结构化输出 → outputEquals / outputMatches
15
+ status: "completed" | "failed" | "waiting"; // waiting = 停下等人(HITL)
16
+ usage?: Usage; // → maxTokens / maxCost / 成本报表,完整字段见下方
17
+ }
18
+ ```
19
+
20
+ `data` 的语义是"本轮的结构化产物":**应用的回答本身是结构化对象**(抽取、分类、表单填充)时才填,`outputEquals` / `outputMatches` 读它。只回文本的应用不填——不要把原始响应 body 或 `message` 文本复制进去凑数,也不要反过来把结构化输出序列化塞进 `events`。`usage` 拿得到就带,拿不到就不填——**别编数字**。
21
+
22
+ `usage` 的完整字段(`Usage` 类型):
23
+
24
+ {/* GENERATED:BEGIN usage-fields */}
25
+
26
+ #### `inputTokens`
27
+
28
+ ```ts
29
+ inputTokens: number;
30
+ ```
31
+
32
+ 输入(prompt)token 数,不含缓存命中部分。
33
+
34
+ #### `outputTokens`
35
+
36
+ ```ts
37
+ outputTokens: number;
38
+ ```
39
+
40
+ 输出(completion)token 数。
41
+
42
+ #### `cacheReadTokens`
43
+
44
+ ```ts
45
+ cacheReadTokens?: number;
46
+ ```
47
+
48
+ 命中 prompt 缓存、按缓存价读取的 token 数(省略表示该 agent 不上报此项)。
49
+
50
+ #### `cacheWriteTokens`
51
+
52
+ ```ts
53
+ cacheWriteTokens?: number;
54
+ ```
55
+
56
+ 写入 prompt 缓存的 token 数(省略表示该 agent 不上报此项)。
57
+
58
+ #### `requests`
59
+
60
+ ```ts
61
+ requests?: number;
62
+ ```
63
+
64
+ 本次运行触发的模型请求次数(多轮/重试可能大于 1)。
65
+
66
+ #### `costUSD`
67
+
68
+ ```ts
69
+ costUSD?: number;
70
+ ```
71
+
72
+ 网关/adapter 实测的真实美元成本(只能由 `Turn.usage.costUSD` 显式带回,从不从
73
+ token 用量或 OTel span 反推得到)。存在时优先于按价格表(`defineConfig({ pricing })`)
74
+ 估算的成本——见 `estimateCost` 的 `usage.costUSD ?? estimateCost(...)` 兜底顺序。
75
+
76
+ {/* GENERATED:END usage-fields */}
77
+
78
+ ## StreamEvent 变体一览
79
+
80
+ `StreamEvent` 的九种变体,逐字段列出(消费它们的断言 / 使用细节见下面的「事件总表」和「逐事件说明」):
81
+
82
+ {/* GENERATED:BEGIN stream-events */}
83
+
84
+ #### `message`
85
+
86
+ ```ts
87
+ { type: "message"; role: "assistant" | "user"; text: string; loc?: SourceLoc }
88
+ ```
89
+
90
+ #### `action.called`
91
+
92
+ ```ts
93
+ { type: "action.called"; callId: string; name: string; input: JsonValue; tool?: ToolName }
94
+ ```
95
+
96
+ #### `action.result`
97
+
98
+ ```ts
99
+ {
100
+ type: "action.result";
101
+ callId: string;
102
+ output?: JsonValue;
103
+ status: "completed" | "failed" | "rejected";
104
+ }
105
+ ```
106
+
107
+ #### `subagent.called`
108
+
109
+ ```ts
110
+ { type: "subagent.called"; callId: string; name: string; remoteUrl?: string }
111
+ ```
112
+
113
+ #### `subagent.completed`
114
+
115
+ ```ts
116
+ { type: "subagent.completed"; callId: string; output?: JsonValue; status: "completed" | "failed" }
117
+ ```
118
+
119
+ #### `input.requested`
120
+
121
+ ```ts
122
+ { type: "input.requested"; request: InputRequest }
123
+ ```
124
+
125
+ #### `thinking`
126
+
127
+ ```ts
128
+ { type: "thinking"; text: string }
129
+ ```
130
+
131
+ #### `compaction`
132
+
133
+ ```ts
134
+ { type: "compaction"; reason?: string }
135
+ ```
136
+
137
+ #### `error`
138
+
139
+ ```ts
140
+ { type: "error"; message: string }
141
+ ```
142
+
143
+ {/* GENERATED:END stream-events */}
144
+
145
+ ## 事件总表
146
+
147
+ | 事件 | 说什么 | 消费它的断言 / API |
148
+ |---|---|---|
149
+ | `message` | agent(或用户)说了一段话 | `t.reply`、`messageIncludes`、judge 的材料 |
150
+ | `action.called` | 发起一次工具调用 | `calledTool`、`toolOrder`、`maxToolCalls`、`notCalledTool` |
151
+ | `action.result` | 该次调用的结果 | `calledTool` 的 `status` 匹配、`noFailedActions` |
152
+ | `subagent.called` | 委派一个子 agent | `calledSubagent` |
153
+ | `subagent.completed` | 子 agent 返回 | `calledSubagent` 的 `status`、`noFailedActions` |
154
+ | `input.requested` | 停下来等人输入(HITL) | `t.parked()`、`t.requireInputRequest()` |
155
+ | `thinking` | 思考文本 | `event("thinking")`、view 展示 |
156
+ | `compaction` | 上下文被压缩 | `event("compaction")`(adapter 的 parser 吐出这条事件即可断言,不需要任何声明) |
157
+ | `error` | 本轮出错 | `event("error")`、view 展示 |
158
+
159
+ 任何事件都能被通用断言消费:`event(type)` / `notEvent(type)` / `eventOrder(types)` / `eventsSatisfy(label, predicate)`。
160
+
161
+ ## 逐事件说明
162
+
163
+ ### `message` —— 说了什么
164
+
165
+ ```ts
166
+ { type: "message", role: "assistant" | "user", text: string }
167
+ ```
168
+
169
+ 每段助手文本吐一条 `role: "assistant"` 的 `message`。**工具结果不是助手消息**——不要把工具输出包成 `message`,否则 `t.reply` 会读到错误内容。用户输入的 `message` 由 [NiceEval](https://niceeval.com/) 自动记录,adapter 不用吐。
170
+
171
+ ### `action.called` / `action.result` —— 调了什么工具、结果如何
172
+
173
+ ```ts
174
+ { type: "action.called", callId: string, name: string, input: JsonValue }
175
+ { type: "action.result", callId: string, output?: JsonValue,
176
+ status: "completed" | "failed" | "rejected" }
177
+ ```
178
+
179
+ - 每个 `action.called` 配一个**同 `callId`** 的 `action.result`——并发调用靠它不错配。你的 agent 返回里有显式 id(AI SDK 的 `toolCallId`、Anthropic 的 `tool_use.id`)就直接用;实在没有再按顺序合成。
180
+ - `status` 如实填:工具执行失败是 `"failed"`(`noFailedActions()` 会响);**人否决是 `"rejected"`**(`noFailedActions()` 依然通过,`calledTool(..., { status: "rejected" })` 可精确断言)。两回事,别混。
181
+ - `name` 用工具的原始名字。
182
+
183
+ ### `subagent.called` / `subagent.completed` —— 委派了谁
184
+
185
+ ```ts
186
+ { type: "subagent.called", callId: string, name: string, remoteUrl?: string }
187
+ { type: "subagent.completed", callId: string, output?: JsonValue,
188
+ status: "completed" | "failed" }
189
+ ```
190
+
191
+ 被测系统把任务委派给子 agent(等它返回)时吐这一对,`callId` 配对规则同上。喂 `calledSubagent("researcher")` 这类断言。
192
+
193
+ ### `input.requested` —— 停下等人(HITL)
194
+
195
+ ```ts
196
+ { type: "input.requested", request: {
197
+ id?: string,
198
+ action?: string, // 停在哪个动作上(如工具名)
199
+ input?: JsonValue, // 该动作的入参
200
+ prompt?: string, // 问人的问题
201
+ options?: { id: string, label?: string }[], // 可选项(approve / deny…)
202
+ } }
203
+ ```
204
+
205
+ agent 停轮等人时,每个待回答的问题吐一条,同时整轮 `status` 返回 `"waiting"`。`t.requireInputRequest(filter)` 的 filter 逐字段匹配这个 `request`——**能填的字段尽量填**,否则 eval 侧筛选不到。接法见[接入教程的 HITL 部分](/zh/guides/connect-your-agent)。
206
+
207
+ ### `thinking` / `compaction` / `error`
208
+
209
+ ```ts
210
+ { type: "thinking", text: string }
211
+ { type: "compaction", reason?: string } // 上下文压缩;adapter 的 parser 吐出即可断言,没有声明层
212
+ { type: "error", message: string }
213
+ ```
214
+
215
+ 有就吐,没有不硬造。`compaction` 主要来自 coding agent CLI(上下文满了自动压缩)。
216
+
217
+ ## 映射的三条纪律
218
+
219
+ 1. **时序即事实**:事件按真实发生顺序排。`toolOrder` / `eventOrder` 靠子序匹配,顺序错了断言就失真。
220
+ 2. **`callId` 配对**:每个 `called` 都要有同 id 的 `result`,缺一半会让 `calledTool(..., { status })` 匹配不上。
221
+ 3. **完整性没有声明层,由来源证明**:用官方转换器(`fromClaudeSdkMessages`、`fromAiSdk` 等)时,返回值本身保证**所有**工具调用都在流里;手工映射时完整与否完全取决于你的映射代码写没写全。只吐一部分时,`notCalledTool` / `maxToolCalls` 这类负断言会静默通过——没有报错,比失败更难发现。手工映射前确认自己的代码覆盖了全部工具调用,见[能力位参考](/zh/reference/capabilities)。
222
+
223
+ ## 一个完整的映射示例
224
+
225
+ agent 返回里带步骤记录时,映射就是一段小循环:
226
+
227
+ ```ts
228
+ import type { StreamEvent } from "niceeval";
229
+
230
+ function toStreamEvents(body: MyBotResponse): StreamEvent[] {
231
+ const events: StreamEvent[] = [];
232
+ for (const step of body.steps) {
233
+ if (step.type === "tool_call") {
234
+ events.push({ type: "action.called", callId: step.id, name: step.tool, input: step.args });
235
+ events.push({
236
+ type: "action.result",
237
+ callId: step.id,
238
+ output: step.result,
239
+ status: step.error ? "failed" : "completed",
240
+ });
241
+ }
242
+ if (step.type === "text") events.push({ type: "message", role: "assistant", text: step.text });
243
+ }
244
+ return events;
245
+ }
246
+ ```
247
+
248
+ ## 相关阅读
249
+
250
+ - [接入你的 agent](/zh/guides/connect-your-agent) —— 从零跑通的教程。
251
+ - [能力位](/zh/reference/capabilities) —— 声明"事件流是完整的"意味着什么。
252
+ - [编写 eval](/zh/guides/authoring) —— 消费这条流的断言全集。
@@ -0,0 +1,208 @@
1
+ ---
2
+ title: "niceeval/expect matchers 与自定义断言参考"
3
+ sidebarTitle: "断言 Matchers"
4
+ description: "niceeval/expect 参考:includes、equals、matches、similarity、satisfies。可链式调用 .gate() 或 .atLeast(0.7),也可以用 makeAssertion 构建自定义 matcher。"
5
+ ---
6
+
7
+ `niceeval/expect` 提供一组可组合 matcher,传给 `t.check()` 或 `t.require()`。matcher 会返回一个 `Assertion`,并带有默认严重性:`gate` 或 `soft`。
8
+
9
+ ```ts
10
+ import { includes, equals, matches, similarity, satisfies } from "niceeval/expect";
11
+ ```
12
+
13
+ ## 使用方式
14
+
15
+ ```ts
16
+ t.check(t.reply, includes("confirmed"));
17
+ t.check(turn.data, equals({ intent: "refund" }));
18
+ t.require(turn.status, equals("completed"));
19
+ ```
20
+
21
+ | 方法 | 失败时 | 适合 |
22
+ |---|---|---|
23
+ | `t.check` | 记录结果,继续执行 | 大多数断言 |
24
+ | `t.require` | 立即抛出,中断 test | 后续执行没有意义的前置条件 |
25
+
26
+ ## Matchers
27
+
28
+ 下面每个 matcher 的签名和说明从 `niceeval/expect` 的源码生成,和当前实现保持同步。
29
+
30
+ {/* GENERATED:BEGIN expect-matchers */}
31
+
32
+ #### `includes`
33
+
34
+ ```ts
35
+ export function includes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion { ... }
36
+ ```
37
+
38
+ String(value) 含子串 / 命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。
39
+
40
+ #### `excludes`
41
+
42
+ ```ts
43
+ export function excludes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion { ... }
44
+ ```
45
+
46
+ includes 的取反:不含子串 / 不命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。
47
+
48
+ #### `equals`
49
+
50
+ ```ts
51
+ export function equals(expected: unknown): ValueAssertion { ... }
52
+ ```
53
+
54
+ 深相等则 1,否则 0。默认硬门槛。
55
+
56
+ #### `matches`
57
+
58
+ ```ts
59
+ export function matches(schema: unknown): ValueAssertion { ... }
60
+ ```
61
+
62
+ 用 schema 校验 value——不是正则匹配,是 Standard Schema / zod 风格的结构校验。
63
+ 优先 Standard Schema(schema['~standard'].validate),否则退化到 zod 风格的
64
+ .safeParse / .parse。校验通过 1,否则 0;任何异常 → 0。默认硬门槛。
65
+
66
+ #### `similarity`
67
+
68
+ ```ts
69
+ export function similarity(expected: string): ValueAssertion { ... }
70
+ ```
71
+
72
+ 纯字符串编辑距离,不是语义相似度——归一化 Levenshtein 距离 [0,1](1 - 编辑距离 / 较长串长度),
73
+ 不理解含义,同义改写 / 语序调整会被判低分。默认软分,阈值 0.6。
74
+
75
+ #### `satisfies`
76
+
77
+ ```ts
78
+ export function satisfies(predicate: (v: unknown) => boolean, label?: string): ValueAssertion { ... }
79
+ ```
80
+
81
+ 谓词为真则 1,否则 0。默认硬门槛;name 带上 label 便于报告辨认。
82
+
83
+ #### `isDefined`
84
+
85
+ ```ts
86
+ export function isDefined(label?: string): ValueAssertion { ... }
87
+ ```
88
+
89
+ value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。默认硬门槛。
90
+
91
+ #### `isTrue`
92
+
93
+ ```ts
94
+ export function isTrue(label?: string): ValueAssertion { ... }
95
+ ```
96
+
97
+ value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用)。默认硬门槛。
98
+
99
+ #### `commandSucceeded`
100
+
101
+ ```ts
102
+ export function commandSucceeded(): ValueAssertion { ... }
103
+ ```
104
+
105
+ CommandResult.exitCode === 0 则 1,否则 0。默认硬门槛。
106
+
107
+ #### `isFalse`
108
+
109
+ ```ts
110
+ export function isFalse(label?: string): ValueAssertion { ... }
111
+ ```
112
+
113
+ value === false 则 1,否则 0。带 label 的布尔断言。默认硬门槛。
114
+
115
+ #### `makeAssertion`
116
+
117
+ ```ts
118
+ export function makeAssertion(spec: {
119
+ name: string;
120
+ severity?: Severity;
121
+ threshold?: number;
122
+ score: (value: unknown) => number | Promise<number>;
123
+ }): ValueAssertion { ... }
124
+ ```
125
+
126
+ 自定义断言工厂:直接给名字 / 严重级 / 阈值 / score,一次调用即返回可用的 ValueAssertion——
127
+ 不像 gate()/atLeast() 那样需要二段链式调用来定级。severity 省略默认 gate。
128
+
129
+ {/* GENERATED:END expect-matchers */}
130
+
131
+ ### 常见用法示例
132
+
133
+ ```ts
134
+ t.check(t.reply, includes("Paris"));
135
+ t.check(t.reply, includes(/order #\d+/i));
136
+ t.check(turn.data, equals({ intent: "refund" }));
137
+ t.check(t.reply, matches(/#[A-Z0-9]{8}/));
138
+ t.check(t.reply, similarity("The answer explains the refund window").atLeast(0.8));
139
+ t.check(turn.data, satisfies((value) => Array.isArray(value)));
140
+ ```
141
+
142
+ ## gate 和 soft
143
+
144
+ ```ts
145
+ t.check(t.reply, includes("required").gate());
146
+ t.check(t.reply, includes("nice to have").atLeast(0.7));
147
+ ```
148
+
149
+ 每个 matcher 返回的 `Assertion`(即 `ValueAssertion`)都带有这些成员;`.gate()` 把它变成硬门槛,`.atLeast()` 变成软阈值:
150
+
151
+ {/* GENERATED:BEGIN value-assertion */}
152
+
153
+ #### `name`
154
+
155
+ ```ts
156
+ readonly name: string;
157
+ ```
158
+
159
+ #### `severity`
160
+
161
+ ```ts
162
+ readonly severity: Severity;
163
+ ```
164
+
165
+ #### `threshold`
166
+
167
+ ```ts
168
+ readonly threshold?: number;
169
+ ```
170
+
171
+ #### `score`
172
+
173
+ ```ts
174
+ score(value: unknown): number | Promise<number>;
175
+ ```
176
+
177
+ #### `gate`
178
+
179
+ ```ts
180
+ gate(threshold?: number): ValueAssertion;
181
+ ```
182
+
183
+ 转成硬门槛断言:未达阈值(省略 threshold 则按 score &gt; 0 判定)整条 eval 判为 failed。返回新实例,不改原对象。
184
+
185
+ #### `atLeast`
186
+
187
+ ```ts
188
+ atLeast(threshold: number): ValueAssertion;
189
+ ```
190
+
191
+ 转成软阈值断言:未达 threshold 时该条记为 failed,但默认不拖累整条 eval 的 outcome;
192
+ `--strict` 运行下,软阈值失败也会把整条 eval 的 outcome 计为 failed。返回新实例,不改原对象。
193
+
194
+ {/* GENERATED:END value-assertion */}
195
+
196
+ ## 自定义 matcher
197
+
198
+ ```ts
199
+ import { makeAssertion } from "niceeval/expect";
200
+
201
+ const validEmail = makeAssertion("valid email", (value) => {
202
+ return typeof value === "string" && value.includes("@") ? 1 : 0;
203
+ });
204
+
205
+ t.check(t.reply, validEmail());
206
+ ```
207
+
208
+ 自定义 matcher 应返回 0 到 1 之间的分数,并给出清晰名称。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niceeval",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,7 +61,7 @@
61
61
  "files": [
62
62
  "src",
63
63
  "bin",
64
- "docs"
64
+ "docs-site"
65
65
  ],
66
66
  "dependencies": {
67
67
  "autoevals": "0.0.132",
@@ -153,6 +153,7 @@
153
153
  "test:watch": "vitest",
154
154
  "niceeval": "node bin/niceeval.js",
155
155
  "gen:diff-code": "tsx scripts/gen-diff-code.ts",
156
+ "docs:reference": "tsx scripts/generate-reference.ts",
156
157
  "tiers:sync": "node scripts/sync-tiers.mjs sync",
157
158
  "tiers:check": "node scripts/sync-tiers.mjs check",
158
159
  "view:build": "vite build --config src/view/app/vite.config.ts",
package/src/agents/bub.ts CHANGED
@@ -42,7 +42,7 @@ const BUB = "$(command -v bub || echo $HOME/.local/bin/bub)";
42
42
  // 可用 NICEEVAL_BUB_OVERRIDE / NICEEVAL_BUB_OTEL_PLUGIN 覆盖,不必改源码。
43
43
  const BUB_OVERRIDE =
44
44
  getEnv("NICEEVAL_BUB_OVERRIDE") ??
45
- "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/streaming-usage-include-usage";
45
+ "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/tape-assistant-text-with-tool-calls";
46
46
  const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
47
47
  const OTEL_PLUGIN =
48
48
  getEnv("NICEEVAL_BUB_OTEL_PLUGIN") ??
@@ -24,7 +24,7 @@ export interface ClaudeCodeConfig {
24
24
  */
25
25
  maxTurns?: number;
26
26
  /**
27
- * 额外 MCP server(每个沙箱 setup 时写进 ~/.claude/claude.json)。
27
+ * 额外 MCP server(每个沙箱 setup 时写进用户级 ~/.claude.json)。
28
28
  * 示例:{ name: "browser", command: "npx", args: ["-y", "@anthropic/mcp-browser"] }
29
29
  */
30
30
  mcpServers?: McpServer[];
@@ -32,8 +32,6 @@ export interface ClaudeCodeConfig {
32
32
  * 额外安装的 skill,格式为 GitHub `"org/repo"`(如 `"Effect-TS/skills"`)。
33
33
  * setup 阶段在沙箱里执行 `npx skills add <org/repo>`;
34
34
  * 结果写进沙箱工作区的 skills-lock.json,claude CLI 启动时自动读取。
35
- *
36
- * @example skills: ["Effect-TS/skills"]
37
35
  */
38
36
  skills?: string[];
39
37
  }
@@ -74,14 +72,19 @@ export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
74
72
  ...(s.env && { env: s.env }),
75
73
  };
76
74
  }
77
- await shared.writeFile(sb, "~/.claude/claude.json", JSON.stringify({ mcpServers: servers }, null, 2));
75
+ // 用户级 MCP 配置在 ~/.claude.json(顶层 mcpServers 字段),不是 ~/.claude/claude.json
76
+ // ——后者 claude CLI 根本不读,MCP 静默挂不上(本机 `claude mcp list` 可核对)。
77
+ await shared.writeFile(sb, "~/.claude.json", JSON.stringify({ mcpServers: servers }, null, 2));
78
78
  }
79
79
 
80
80
  if (config?.skills?.length) {
81
81
  for (const source of config.skills) {
82
82
  // source = "Effect-TS/skills"(GitHub org/repo)
83
83
  // `npx skills add` 拉 repo、读 manifest、写 skills-lock.json,claude CLI 自动读取。
84
- await sb.runShell(`npx skills add ${source}`);
84
+ // -y 跳过确认;-a claude-code 显式指定目标 CLI——不加这两个 flag 时命令会打印一个
85
+ // "选择安装到哪些 agent" 的交互式多选框等 stdin,headless 沙箱里会一直卡到超时
86
+ // (无 tty,选择框永远等不到输入)。
87
+ await sb.runShell(`npx skills add ${shared.shellQuote(source)} -y -a claude-code`);
85
88
  }
86
89
  }
87
90
  },
@@ -25,8 +25,6 @@ export interface CodexConfig {
25
25
  /**
26
26
  * 额外安装的 skill,格式为 GitHub `"org/repo"`(如 `"Effect-TS/skills"`)。
27
27
  * setup 阶段执行 `npx skills add <org/repo>`,结果写进 skills-lock.json。
28
- *
29
- * @example skills: ["Effect-TS/skills"]
30
28
  */
31
29
  skills?: string[];
32
30
  }
@@ -69,10 +67,12 @@ export function codexAgent(config?: CodexConfig): Agent {
69
67
  if (config?.mcpServers?.length) {
70
68
  const mcpToml = config.mcpServers
71
69
  .map((s) => {
72
- const lines: string[] = [`[mcp_server.${s.name}]`, `command = "${s.command}"`];
70
+ // 注意是复数 mcp_servers:单数 [mcp_server.x] 会被 codex 静默忽略,
71
+ // MCP 压根挂不上(实测 codex-cli 0.142.x,`codex mcp list` 可核对)。
72
+ const lines: string[] = [`[mcp_servers.${s.name}]`, `command = "${s.command}"`];
73
73
  if (s.args?.length) lines.push(`args = [${s.args.map((a) => `"${a}"`).join(", ")}]`);
74
74
  if (s.env && Object.keys(s.env).length) {
75
- lines.push(`[mcp_server.${s.name}.env]`);
75
+ lines.push(`[mcp_servers.${s.name}.env]`);
76
76
  for (const [k, v] of Object.entries(s.env)) lines.push(`${k} = "${v}"`);
77
77
  }
78
78
  return lines.join("\n");
@@ -83,7 +83,11 @@ export function codexAgent(config?: CodexConfig): Agent {
83
83
 
84
84
  if (config?.skills?.length) {
85
85
  for (const source of config.skills) {
86
- await sb.runShell(`npx skills add ${source}`);
86
+ // claude-code adapter:-y -a codex 避免无 tty 环境下卡在交互式 agent 选择框。
87
+ // codex 没有 claude-code 那种原生 Skill 工具,装进的是 skills 包的"通用"目录
88
+ // (`.agents/skills/<name>`)——codex CLI 本身不会主动去读它,要在 prompt 里
89
+ // 显式提示"检查仓库里有没有 skill/guide 文件",agent 才会用 shell 命令读进上下文。
90
+ await sb.runShell(`npx skills add ${shared.shellQuote(source)} -y -a codex`);
87
91
  }
88
92
  }
89
93
  },
@@ -107,7 +107,7 @@ describe("fromCodexThreadEvents", () => {
107
107
  it("command_execution:started 发 called,completed 只补 result(按 exit_code 判状态)", () => {
108
108
  const s = fromCodexThreadEvents();
109
109
  expect(s.add({ type: "item.started", item: { type: "command_execution", id: "c1", command: "ls" } })).toEqual([
110
- { type: "action.called", callId: "c1", name: "command_execution", input: { command: "ls" } },
110
+ { type: "action.called", callId: "c1", name: "command_execution", input: { command: "ls" }, tool: "shell" },
111
111
  ]);
112
112
  expect(
113
113
  s.add({ type: "item.completed", item: { type: "command_execution", id: "c1", command: "ls", exit_code: 0, aggregated_output: "a.txt" } }),
@@ -117,7 +117,7 @@ describe("fromCodexThreadEvents", () => {
117
117
  it("只有 completed 的工具项也成对;失败 exit_code → failed", () => {
118
118
  const s = fromCodexThreadEvents();
119
119
  expect(s.add({ type: "item.completed", item: { type: "command_execution", id: "c2", command: "false", exit_code: 1 } })).toEqual([
120
- { type: "action.called", callId: "c2", name: "command_execution", input: { command: "false" } },
120
+ { type: "action.called", callId: "c2", name: "command_execution", input: { command: "false" }, tool: "shell" },
121
121
  { type: "action.result", callId: "c2", output: { output: null, exit_code: 1 }, status: "failed" },
122
122
  ]);
123
123
  });
@@ -125,7 +125,7 @@ describe("fromCodexThreadEvents", () => {
125
125
  it("mcp_tool_call 带 server 前缀;turn.completed 聚合 usage", () => {
126
126
  const s = fromCodexThreadEvents();
127
127
  expect(s.add({ type: "item.completed", item: { type: "mcp_tool_call", id: "m1", server: "kb", tool: "search", arguments: { q: "x" } } })).toEqual([
128
- { type: "action.called", callId: "m1", name: "kb.search", input: { q: "x" } },
128
+ { type: "action.called", callId: "m1", name: "kb.search", input: { q: "x" }, tool: "web_search" },
129
129
  { type: "action.result", callId: "m1", output: null, status: "completed" },
130
130
  ]);
131
131
  expect(s.usage).toBeUndefined();
@@ -138,7 +138,7 @@ describe("fromCodexThreadEvents", () => {
138
138
  expect(
139
139
  s.add({ type: "item.completed", item: { type: "file_change", id: "p1", changes: [{ path: "a.ts", kind: "update" }] } }),
140
140
  ).toEqual([
141
- { type: "action.called", callId: "p1#0", name: "file_change", input: { path: "a.ts", kind: "update" } },
141
+ { type: "action.called", callId: "p1#0", name: "file_change", input: { path: "a.ts", kind: "update" }, tool: "file_edit" },
142
142
  { type: "action.result", callId: "p1#0", output: { path: "a.ts", kind: "update" }, status: "completed" },
143
143
  ]);
144
144
  });
@@ -20,7 +20,9 @@
20
20
  // return { status: stream.failed ? "failed" : "completed", events, usage: stream.usage };
21
21
  // ```
22
22
 
23
- import type { JsonValue, StreamEvent, Usage } from "../types.ts";
23
+ import type { JsonValue, StreamEvent, ToolName, Usage } from "../types.ts";
24
+ import { normalizeToolName } from "../o11y/tool-names.ts";
25
+ import { CODEX_TOOL_ALIASES } from "../o11y/parsers/codex.ts";
24
26
 
25
27
  // ───────────────────────── 通用 SSE 读帧器 ─────────────────────────
26
28
 
@@ -306,7 +308,8 @@ export interface CodexThreadStream {
306
308
  /**
307
309
  * 逐帧喂 `ThreadEvent`,返回标准事件:消息类(agent_message → message、reasoning → thinking、
308
310
  * error item / turn.failed → error)+ 工具项(command_execution / mcp_tool_call / web_search /
309
- * file_change → action.called + action.result)。usage `turn.completed` 聚合,经 `usage` 读。
311
+ * file_change → action.called + action.result,附规范工具名 `tool`,如 command_execution
312
+ * `"shell"`,供 `calledTool("shell")` 这类跨 agent 断言命中)。usage 从 `turn.completed` 聚合,经 `usage` 读。
310
313
  * 断言依据全部来自这条流;codex CLI 的原生 OTLP span 只用于瀑布图(spanMapper: mapCodexSpans)。
311
314
  */
312
315
  add(event: CodexThreadEventLike): StreamEvent[];
@@ -333,19 +336,21 @@ export function fromCodexThreadEvents(): CodexThreadStream {
333
336
  };
334
337
 
335
338
  // 工具项 → action.called(+completed 时的 action.result)。与 o11y/parsers/codex.ts 的
336
- // transcript 映射保持同一套语义(字段名以 codex exec --json 的 ThreadItem 为准)
339
+ // transcript 映射保持同一套语义(字段名以 codex exec --json 的 ThreadItem 为准),
340
+ // 包括规范工具名 tool:少了它 derive 只能落 name="unknown",calledTool("shell") 这类
341
+ // 跨 agent 规范名断言在 SDK 流路径上会静默失配(2026-07-09 CI 实红)。
337
342
  const handleToolItem = (item: Record<string, unknown>, isCompleted: boolean): StreamEvent[] => {
338
343
  const events: StreamEvent[] = [];
339
- const emitCall = (callId: string, name: string, input: JsonValue): void => {
344
+ const emitCall = (callId: string, name: string, input: JsonValue, tool: ToolName): void => {
340
345
  if (startedCallIds.has(callId)) return;
341
346
  startedCallIds.add(callId);
342
- events.push({ type: "action.called", callId, name, input });
347
+ events.push({ type: "action.called", callId, name, input, tool });
343
348
  };
344
349
 
345
350
  switch (item.type) {
346
351
  case "command_execution": {
347
352
  const callId = callIdOf(item, "cmd");
348
- emitCall(callId, "command_execution", { command: item.command ?? null } as JsonValue);
353
+ emitCall(callId, "command_execution", { command: item.command ?? null } as JsonValue, "shell");
349
354
  if (isCompleted) {
350
355
  const exit = item.exit_code;
351
356
  const success = exit === 0 || (exit == null && item.status !== "failed" && item.status !== "error");
@@ -362,7 +367,7 @@ export function fromCodexThreadEvents(): CodexThreadStream {
362
367
  const callId = callIdOf(item, "mcp");
363
368
  const tool = String(item.tool ?? "unknown");
364
369
  const name = typeof item.server === "string" ? `${item.server}.${tool}` : tool;
365
- emitCall(callId, name, (item.arguments ?? null) as JsonValue);
370
+ emitCall(callId, name, (item.arguments ?? null) as JsonValue, normalizeToolName(tool, CODEX_TOOL_ALIASES));
366
371
  if (isCompleted) {
367
372
  const success = !item.error && item.status !== "failed";
368
373
  events.push({ type: "action.result", callId, output: (item.result ?? null) as JsonValue, status: success ? "completed" : "failed" });
@@ -371,7 +376,7 @@ export function fromCodexThreadEvents(): CodexThreadStream {
371
376
  }
372
377
  case "web_search": {
373
378
  const callId = callIdOf(item, "web");
374
- emitCall(callId, "web_search", { query: item.query ?? null } as JsonValue);
379
+ emitCall(callId, "web_search", { query: item.query ?? null } as JsonValue, "web_search");
375
380
  if (isCompleted) events.push({ type: "action.result", callId, output: null, status: "completed" });
376
381
  return events;
377
382
  }
@@ -383,7 +388,7 @@ export function fromCodexThreadEvents(): CodexThreadStream {
383
388
  changes.forEach((ch, i) => {
384
389
  const callId = `${baseId}#${i}`;
385
390
  const change = isRecord(ch) ? { path: ch.path ?? null, kind: ch.kind ?? null } : {};
386
- events.push({ type: "action.called", callId, name: "file_change", input: change as JsonValue });
391
+ events.push({ type: "action.called", callId, name: "file_change", input: change as JsonValue, tool: "file_edit" });
387
392
  events.push({ type: "action.result", callId, output: change as JsonValue, status: "completed" });
388
393
  });
389
394
  return events;