niceeval 0.6.1 → 0.6.2

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 (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -1,6 +1,8 @@
1
+ // cases: docs/engineering/unit-tests/eval/cases.md
1
2
  import { describe, expect, it } from "vitest";
2
3
  import { createEvalContext, type ContextState } from "./context.ts";
3
- import { includes } from "../expect/index.ts";
4
+ import { commandSucceeded, includes } from "../expect/index.ts";
5
+ import { completeCoverage, resolveAgentCoverage } from "../scoring/coverage.ts";
4
6
  import type { Agent, AgentContext, InputRequest, Sandbox, StreamEvent, Turn, TurnInput } from "../types.ts";
5
7
 
6
8
  // 计算工具 + 最终回复"1 + 1 = **2** 哦!😊"——复现截图里的场景:助手回复明明包含 "2",
@@ -10,6 +12,7 @@ function calculatorAgent(): Agent {
10
12
  name: "calculator",
11
13
  // 测试注入了真实的 fake sandbox,kind: "sandbox" 让 t.sandbox 过沙箱能力守卫。
12
14
  kind: "sandbox",
15
+ coverage: completeCoverage,
13
16
  async send(_input: TurnInput, ctx: AgentContext): Promise<Turn> {
14
17
  ctx.session.capture("sess-1");
15
18
  const events: StreamEvent[] = [
@@ -84,11 +87,12 @@ describe("createEvalContext / TestContext live state", () => {
84
87
  scripts: state.late.scripts,
85
88
  usage: { inputTokens: 0, outputTokens: 0 },
86
89
  status: "completed",
90
+ coverage: resolveAgentCoverage(completeCoverage),
87
91
  readFile: async () => undefined,
88
92
  });
89
- expect(result.passed).toBe(true);
90
- expect(result.score).toBe(1);
91
- expect(result.evidence).toBeUndefined();
93
+ expect(result.outcome).toBe("passed");
94
+ expect(result.outcome === "passed" ? result.score : -1).toBe(1);
95
+ expect(result.outcome === "passed" ? result.evidence : "?").toBeUndefined();
92
96
  });
93
97
 
94
98
  it("t.check(...) attaches the actually-checked value as evidence when it fails", async () => {
@@ -103,10 +107,41 @@ describe("createEvalContext / TestContext live state", () => {
103
107
  scripts: state.late.scripts,
104
108
  usage: { inputTokens: 0, outputTokens: 0 },
105
109
  status: "completed",
110
+ coverage: resolveAgentCoverage(completeCoverage),
106
111
  readFile: async () => undefined,
107
112
  });
108
- expect(result.passed).toBe(false);
109
- expect(result.evidence).toBe("1 + 1 = **2** 哦!😊");
113
+ expect(result.outcome).toBe("failed");
114
+ expect(result.outcome === "failed" ? result.received : undefined).toBe("1 + 1 = **2** 哦!😊");
115
+ });
116
+
117
+ it("t.check(CommandResult, …) 失败时 received 塌成退出码+输出尾部,evidence 是命令行", async () => {
118
+ const { context, state } = makeContext(calculatorAgent());
119
+ const commandResult = {
120
+ stdout: `\n> test\n> pytest tests/\n${"collecting …\n".repeat(200)}1 failed, 5 passed in 12.72s\n`,
121
+ stderr: "",
122
+ exitCode: 1,
123
+ command: "npm run test",
124
+ };
125
+ context.check(commandResult, commandSucceeded());
126
+
127
+ const [result] = await state.collector.finalize({
128
+ events: [],
129
+ facts: { toolCalls: [], subagentCalls: [], inputRequests: [], parked: false, messageCount: 0, compactions: 0 },
130
+ diff: state.late.diff,
131
+ scripts: state.late.scripts,
132
+ usage: { inputTokens: 0, outputTokens: 0 },
133
+ status: "completed",
134
+ coverage: resolveAgentCoverage(completeCoverage),
135
+ readFile: async () => undefined,
136
+ });
137
+ expect(result.outcome).toBe("failed");
138
+ if (result.outcome !== "failed") return;
139
+ const [firstLine] = result.received!.split("\n");
140
+ expect(firstLine).toMatch(/^exit 1 · "…/);
141
+ expect(firstLine).toContain("1 failed, 5 passed in 12.72s"); // 摘要面只保留的这一行自含失败计数
142
+ expect(firstLine!.length).toBeLessThan(200);
143
+ expect(result.received).toContain("output tail:"); // 更长尾部保留换行,attempt 首页展开
144
+ expect(result.evidence).toBe("npm run test");
110
145
  });
111
146
 
112
147
  it("t.events reflects the turn's events after send(), not an empty snapshot", async () => {
@@ -147,6 +182,7 @@ function scriptedAgent(turns: Turn[]): Agent & { received: TurnInput[] } {
147
182
  const agent: Agent = {
148
183
  name: "scripted",
149
184
  kind: "remote",
185
+ coverage: completeCoverage,
150
186
  async send(input: TurnInput) {
151
187
  received.push(input);
152
188
  const turn = turns[Math.min(i, turns.length - 1)] as Turn;
@@ -208,6 +244,8 @@ describe("t.respond() / t.respondAll(): structured InputResponse", () => {
208
244
  await context.send("apply two edits");
209
245
 
210
246
  await expect(context.respond("approve")).rejects.toThrow(/字符串回答无法对位|cannot be matched/);
247
+ // 报错但响应已发出同样违约:agent 只收到最初那一次 send,没有第二次输入。
248
+ expect(agent.received).toHaveLength(1);
211
249
  });
212
250
 
213
251
  it("object form { request, optionId } disambiguates when multiple requests are pending", async () => {
@@ -299,6 +337,7 @@ function baseScoringContext(state: ContextState) {
299
337
  scripts: state.late.scripts,
300
338
  usage: { inputTokens: 0, outputTokens: 0 },
301
339
  status: "completed" as const,
340
+ coverage: resolveAgentCoverage(completeCoverage),
302
341
  readFile: async () => undefined,
303
342
  };
304
343
  }
@@ -314,7 +353,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
314
353
 
315
354
  const [result] = await state.collector.finalize(baseScoringContext(state));
316
355
  expect(result.name).toBe("parked");
317
- expect(result.passed).toBe(true);
356
+ expect(result.outcome).toBe("passed");
318
357
  });
319
358
 
320
359
  it("turn.noFailedActions() looks only at this turn's own tool calls", async () => {
@@ -333,7 +372,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
333
372
 
334
373
  const [result] = await state.collector.finalize(baseScoringContext(state));
335
374
  expect(result.name).toBe("noFailedActions");
336
- expect(result.passed).toBe(false);
375
+ expect(result.outcome).toBe("failed");
337
376
  });
338
377
 
339
378
  it("turn.maxTokens()/turn.maxCost() read this turn's own Turn.usage, not the session total", async () => {
@@ -347,9 +386,9 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
347
386
 
348
387
  const [tokens, cost] = await state.collector.finalize(baseScoringContext(state));
349
388
  expect(tokens.name).toBe("maxTokens(1000)");
350
- expect(tokens.passed).toBe(true);
389
+ expect(tokens.outcome).toBe("passed");
351
390
  expect(cost.name).toBe("maxCost(0.01)");
352
- expect(cost.passed).toBe(false);
391
+ expect(cost.outcome).toBe("failed");
353
392
  });
354
393
 
355
394
  it("turn.loadedSkill() reads the skill.loaded event scoped to this turn", async () => {
@@ -364,7 +403,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
364
403
  turn.loadedSkill("pdf-export");
365
404
 
366
405
  const [result] = await state.collector.finalize(baseScoringContext(state));
367
- expect(result.passed).toBe(true);
406
+ expect(result.outcome).toBe("passed");
368
407
  });
369
408
 
370
409
  // Skill 加载是一等事件,不是「名字叫 load_skill 的工具调用」——adapter 负责归一
@@ -382,6 +421,6 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
382
421
  turn.loadedSkill("pdf-export");
383
422
 
384
423
  const [result] = await state.collector.finalize(baseScoringContext(state));
385
- expect(result.passed).toBe(false);
424
+ expect(result.outcome).toBe("failed");
386
425
  });
387
426
  });
@@ -4,13 +4,15 @@
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { basename, extname } from "node:path";
6
6
  import { SessionManager, RunSession, lastAssistantText } from "./session.ts";
7
- import { AssertionCollector, computePassed } from "../scoring/collector.ts";
7
+ import { AssertionCollector, computePassed, unavailable } from "../scoring/collector.ts";
8
+ import type { ResolvedCoverage } from "../scoring/coverage.ts";
8
9
  import { deepEqual, validateSchema } from "../scoring/match.ts";
9
10
  import type { Spec } from "../scoring/collector.ts";
10
11
  import * as Scoped from "../scoring/scoped.ts";
11
12
  import { buildJudge } from "../scoring/judge.ts";
12
13
  import { EvalSkipped, EvalRequirementFailed, TurnFailed } from "./control-flow.ts";
13
14
  import { deriveRunFacts } from "../o11y/derive.ts";
15
+ import { diffIsEmpty, diffMatches, emptyDiffData } from "../scoring/diff.ts";
14
16
  import { t } from "../i18n/index.ts";
15
17
  import { resolveLocalPath } from "../sandbox/paths.ts";
16
18
  import { brief } from "../util.ts";
@@ -73,6 +75,15 @@ export interface ContextDeps {
73
75
  otel?: import("../o11y/otlp/turn-otel.ts").AgentOtelChannel;
74
76
  /** Eval definition directory; used to resolve host-side relative fixture paths. */
75
77
  evalBaseDir?: string;
78
+ /** runner 绑定的作用域反馈(t.progress / t.diagnostic 与 adapter ctx 共用实现);
79
+ * 省略时(测试直调)progress 退回 log、diagnostic 静默丢弃。 */
80
+ feedback?: import("../types.ts").ScopedFeedback;
81
+ /** adapter send 在飞时的通知(errored 归因到嵌套的 `agent.run` 阶段用);透传给 SessionManager。 */
82
+ onSendActive?: (active: boolean) => void;
83
+ /** 变更分类账的 send 窗口钩子(仅沙箱型);透传给 SessionManager(见 SessionDeps.ledgerHooks)。 */
84
+ ledgerHooks?: import("./session.ts").SessionDeps["ledgerHooks"];
85
+ /** 每轮 send 的墙钟包络回报(runner 挂 turn 时间树节点);透传给 SessionManager。 */
86
+ onTurn?: import("./session.ts").SessionDeps["onTurn"];
76
87
  }
77
88
 
78
89
  /**
@@ -100,12 +111,16 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
100
111
  log: deps.log,
101
112
  telemetry: deps.telemetry,
102
113
  otel: deps.otel,
114
+ feedback: deps.feedback,
115
+ onSendActive: deps.onSendActive,
116
+ onTurn: deps.onTurn,
117
+ ledgerHooks: deps.ledgerHooks,
103
118
  });
104
119
  const collector = new AssertionCollector();
105
120
  const state: ContextState = {
106
121
  collector,
107
122
  manager,
108
- late: { diff: { generatedFiles: {}, deletedFiles: [] }, scripts: {} },
123
+ late: { diff: emptyDiffData(), scripts: {} },
109
124
  };
110
125
 
111
126
  async function resolveValue(value: unknown, sc: ScoringContext): Promise<unknown> {
@@ -113,8 +128,36 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
113
128
  return value;
114
129
  }
115
130
 
116
- /** 断言失败时给 view 看的「实际被检查了什么」,而不是重复 matcher 自己的名字。 */
131
+ /** CommandResult 形状的值(duck-type,不 import sandbox 域):received 按「退出码 + 输出尾部」投影。 */
132
+ function asCommandResult(value: unknown): { stdout: string; stderr: string; exitCode: number; command?: string } | undefined {
133
+ if (!value || typeof value !== "object") return undefined;
134
+ const v = value as { stdout?: unknown; stderr?: unknown; exitCode?: unknown };
135
+ return typeof v.stdout === "string" && typeof v.stderr === "string" && typeof v.exitCode === "number"
136
+ ? (value as { stdout: string; stderr: string; exitCode: number; command?: string })
137
+ : undefined;
138
+ }
139
+
140
+ /** 断言失败时给 view 看的「实际被检查了什么」,而不是重复 matcher 自己的名字。
141
+ * 按值的形状落成人可读事实(而不是留一坨 JSON 给渲染层解析):CommandResult 的第一行是
142
+ * `exit N · "…输出尾部摘要"`(stdout+stderr 合并折单行,信号常收在末尾——pytest / vitest
143
+ * 的 failed 计数都在最后几行;榜单与 --eval 标注这类单行面只保留这一行),随后附原样保留
144
+ * 换行的更长尾部——runner 不另存 eval 侧命令的输出,这条记录就是它唯一的家,attempt 首页
145
+ * 与 result.json 靠它给出「更进一步」;文件引用带 `// path` 头;其余走通用 JSON 预览。 */
117
146
  function previewCheckedValue(value: unknown): string {
147
+ const cmd = asCommandResult(value);
148
+ if (cmd) {
149
+ const combined = `${cmd.stdout}\n${cmd.stderr}`.trim();
150
+ const folded = combined.replace(/\s+/g, " ").trim();
151
+ const summary = folded.length > 160 ? `…${folded.slice(-159)}` : folded;
152
+ const headline = summary.length > 0 ? `exit ${cmd.exitCode} · "${summary}"` : `exit ${cmd.exitCode}`;
153
+ if (combined.length <= 160) return headline;
154
+ let tail = combined.slice(-3600);
155
+ if (combined.length > 3600) {
156
+ const firstBreak = tail.indexOf("\n");
157
+ if (firstBreak >= 0) tail = tail.slice(firstBreak + 1); // 不从半行开始
158
+ }
159
+ return `${headline}\noutput tail:\n${tail}`;
160
+ }
118
161
  if (value && typeof value === "object" && typeof (value as { path?: unknown }).path === "string") {
119
162
  const content = (value as { content?: unknown }).content;
120
163
  if (typeof content === "string") return brief(`// ${(value as { path: string }).path}\n${content}`, 4000);
@@ -122,13 +165,17 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
122
165
  return brief(value, 4000);
123
166
  }
124
167
 
168
+ /** 失败断言的 evidence:被检查值自带命令摘要(CommandResult.command)时就是「命令行本身」。 */
169
+ function checkedValueEvidence(value: unknown): string | undefined {
170
+ const command = asCommandResult(value)?.command;
171
+ return typeof command === "string" && command.length > 0 ? command : undefined;
172
+ }
173
+
174
+ // agent 归因 diff 的只读视图:get = 最后触及窗口的终态;matches 扫触及路径与各窗口内容。
125
175
  const diffView: DiffView = {
126
- get: (path) => state.late.diff.generatedFiles[path],
127
- isEmpty: () =>
128
- Object.keys(state.late.diff.generatedFiles).length === 0 &&
129
- state.late.diff.deletedFiles.length === 0,
130
- matches: (re) =>
131
- Object.entries(state.late.diff.generatedFiles).some(([p, c]) => re.test(p) || re.test(c)),
176
+ get: (path) => state.late.diff.get(path),
177
+ isEmpty: () => diffIsEmpty(state.late.diff),
178
+ matches: (re) => diffMatches(state.late.diff, re),
132
179
  };
133
180
 
134
181
  const sandboxAssertions = {
@@ -168,6 +215,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
168
215
  getEvents: () => readonly StreamEvent[],
169
216
  getStatus: () => "completed" | "failed" | "waiting",
170
217
  getUsage: () => Usage,
218
+ getCoverage: () => ResolvedCoverage,
171
219
  ) {
172
220
  return collector.record({
173
221
  ...spec,
@@ -179,6 +227,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
179
227
  facts: deriveRunFacts(events),
180
228
  status: getStatus(),
181
229
  usage: getUsage(),
230
+ coverage: getCoverage(),
182
231
  });
183
232
  },
184
233
  });
@@ -195,24 +244,34 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
195
244
  }
196
245
 
197
246
  function makeSessionHandle(session: RunSession): SessionHandle {
198
- const scoped = (spec: Spec) =>
199
- recordScoped(spec, () => session.events, () => session.lastStatus, () => session.usage);
247
+ // session 作用域 = 记录断言时快照( docs/feature/scoring/architecture/scopes.md):
248
+ // 之后该 session 再发生的轮次不改变这条断言的评估材料;只看最后一轮用 send() TurnHandle。
249
+ const scoped = (spec: Spec) => {
250
+ const events = session.events.slice();
251
+ const status = session.lastStatus;
252
+ const usage = { ...session.usage };
253
+ const coverage = session.coverage;
254
+ return recordScoped(spec, () => events, () => status, () => usage, () => coverage);
255
+ };
200
256
 
201
257
  const handle: SessionHandle = {
202
- send: async (text) => makeTurnHandle(await manager.send(session, text), collector, deps, text),
203
- sendFile: async (path, text) =>
204
- makeTurnHandle(await manager.send(session, text ?? "", [await readInputFile(path)]), collector, deps, text ?? ""),
258
+ send: async (input) => {
259
+ const text = typeof input === "string" ? input : input.text;
260
+ const files = typeof input === "string" ? undefined : input.files;
261
+ const turn = await manager.send(session, text, files);
262
+ return makeTurnHandle(turn, collector, deps, text, manager.resolveTurnCoverage(turn));
263
+ },
264
+ sendFile: async (path, text) => {
265
+ const turn = await manager.send(session, text ?? "", [await readInputFile(path)]);
266
+ return makeTurnHandle(turn, collector, deps, text ?? "", manager.resolveTurnCoverage(turn));
267
+ },
205
268
  requireInputRequest: (filter) => requireInputRequest(session, filter),
206
269
  respond: async (...answers) => {
207
270
  if (answers.length === 0) throw new Error(t("hitl.respondEmpty"));
208
271
  const built = buildRespondInput(session, answers);
209
272
  session.pendingInputRequests.length = 0;
210
- return makeTurnHandle(
211
- await manager.send(session, built.text, undefined, built.responses),
212
- collector,
213
- deps,
214
- built.text,
215
- );
273
+ const turn = await manager.send(session, built.text, undefined, built.responses);
274
+ return makeTurnHandle(turn, collector, deps, built.text, manager.resolveTurnCoverage(turn));
216
275
  },
217
276
  respondAll: async (optionId) => {
218
277
  if (session.pendingInputRequests.length === 0) {
@@ -226,7 +285,8 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
226
285
  }));
227
286
  session.pendingInputRequests.length = 0;
228
287
  const input = requests.map(() => optionId).join("\n");
229
- return makeTurnHandle(await manager.send(session, input, undefined, responses), collector, deps, input);
288
+ const turn = await manager.send(session, input, undefined, responses);
289
+ return makeTurnHandle(turn, collector, deps, input, manager.resolveTurnCoverage(turn));
230
290
  },
231
291
  get reply() {
232
292
  return session.lastMessage;
@@ -251,7 +311,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
251
311
  notEvent: (type) => scoped(Scoped.notEventOfType(type)),
252
312
  calledSubagent: (name, match) => scoped(Scoped.calledSubagent(name, match)),
253
313
  eventOrder: (types) => scoped(Scoped.eventOrder(types)),
254
- eventsSatisfy: (predicate, label) => scoped(Scoped.eventsSatisfy(predicate, label)),
314
+ eventsSatisfy: (label, predicate) => scoped(Scoped.eventsSatisfy(label, predicate)),
255
315
  maxTokens: (max) => scoped(Scoped.maxTokens(max)),
256
316
  maxCost: (usd) => scoped(Scoped.maxCost(usd)),
257
317
  get usage() {
@@ -266,6 +326,12 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
266
326
 
267
327
  const primary = makeSessionHandle(manager.primary);
268
328
 
329
+ // t 作用域 = 整个 attempt:全部 session(含 t.newSession() 开的)的全部轮次,finalize 时对
330
+ // 聚合结果求值(见 docs/feature/scoring/architecture/scopes.md)。newSession 的事件进入这里,
331
+ // 但不进入主 session 的即时 t.reply / t.events 读取视图;t.judge 默认材料仍是主 session 对话。
332
+ const aggregateScoped = (spec: Spec) =>
333
+ recordScoped(spec, () => manager.allEvents, () => manager.lastStatus, () => manager.usage, () => manager.coverage);
334
+
269
335
  // 沙箱能力守卫:非沙箱型 agent(kind !== "sandbox")把文件系统类动作替换成「一调用就报清晰错误」。
270
336
  // 其余能力(多轮对话、工具断言……)不再问卷式声明——没接 ctx.session 续接存取器的 agent
271
337
  // 每轮各是新对话,没吐 action.* 事件的 agent 上正断言自然不命中,负断言按事件完整性证明
@@ -288,6 +354,12 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
288
354
  model: deps.model,
289
355
  reasoningEffort: deps.reasoningEffort,
290
356
  flags: deps.flags,
357
+ // 作用域反馈:scope 固定为 eval.run(runner 按当前阶段归因,eval 不能冒充其它阶段)。
358
+ progress: (u: import("../types.ts").ProgressUpdate) =>
359
+ deps.feedback
360
+ ? deps.feedback.progress(u)
361
+ : deps.log(u.current !== undefined && u.total !== undefined ? `${u.message} (${u.current}/${u.total})` : u.message),
362
+ diagnostic: (d: import("../types.ts").DiagnosticInput) => deps.feedback?.diagnostic(d),
291
363
  log: deps.log,
292
364
  skip: (reason: string) => {
293
365
  if (reason.trim().length === 0) throw new Error(t("context.skipEmpty"));
@@ -297,16 +369,22 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
297
369
 
298
370
  check: (value: unknown, assertion: ValueAssertion) => {
299
371
  // evaluate 读 spec 自己的 severity/threshold(而不是捕获记录时的快照):
300
- // 句柄的 .gate()/.atLeast() 会事后改写 spec,evidence 判定必须和 finalize 同一口径。
372
+ // 句柄的 .gate()/.atLeast() 会事后改写 spec,received 判定必须和 finalize 同一口径。
301
373
  const spec: Spec = {
302
374
  name: assertion.name,
303
375
  severity: assertion.severity,
304
376
  threshold: assertion.threshold,
377
+ ...(assertion.isOptional ? { optional: true as const } : {}),
305
378
  evaluate: async (sc) => {
306
379
  const resolved = await resolveValue(value, sc);
307
380
  const score = await assertion.score(resolved);
308
381
  if (computePassed(spec.severity, spec.threshold, score)) return score;
309
- return { score, evidence: previewCheckedValue(resolved) };
382
+ return {
383
+ score,
384
+ expected: assertion.expected,
385
+ received: previewCheckedValue(resolved),
386
+ ...(checkedValueEvidence(resolved) !== undefined ? { evidence: checkedValueEvidence(resolved) } : {}),
387
+ };
310
388
  },
311
389
  };
312
390
  return collector.record(spec);
@@ -321,13 +399,43 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
321
399
  name: assertion.name,
322
400
  severity: "gate",
323
401
  threshold: assertion.threshold,
324
- evaluate: () => score,
402
+ evaluate: () =>
403
+ passed
404
+ ? score
405
+ : {
406
+ score,
407
+ expected: assertion.expected,
408
+ received: previewCheckedValue(v),
409
+ ...(checkedValueEvidence(v) !== undefined ? { evidence: checkedValueEvidence(v) } : {}),
410
+ },
325
411
  });
326
412
  if (!passed) throw new EvalRequirementFailed(assertion.name);
327
413
  return value;
328
414
  },
329
415
 
330
416
  sandbox: sandboxHandle,
417
+
418
+ // 作用域断言(t 级:聚合全部 session)。这些描述符盖过 primary 的同名方法——
419
+ // t.send/t.reply/t.events 仍是主 session 的即时视图,断言聚合与读取视图是两回事。
420
+ succeeded: () => aggregateScoped(Scoped.succeeded()),
421
+ parked: () => aggregateScoped(Scoped.parked()),
422
+ messageIncludes: (token: string | RegExp) => aggregateScoped(Scoped.messageIncludes(token)),
423
+ calledTool: (name: string, match?: import("../types.ts").ToolMatch) => aggregateScoped(Scoped.calledTool(name, match)),
424
+ notCalledTool: (name: string, match?: import("../types.ts").ToolMatch) => aggregateScoped(Scoped.notCalledTool(name, match)),
425
+ toolOrder: (names: string[]) => aggregateScoped(Scoped.toolOrder(names)),
426
+ usedNoTools: () => aggregateScoped(Scoped.usedNoTools()),
427
+ maxToolCalls: (max: number) => aggregateScoped(Scoped.maxToolCalls(max)),
428
+ loadedSkill: (skill: string) => aggregateScoped(Scoped.loadedSkill(skill)),
429
+ noFailedActions: () => aggregateScoped(Scoped.noFailedActions()),
430
+ event: (type: StreamEvent["type"], opts?: { count?: number }) => aggregateScoped(Scoped.eventOfType(type, opts)),
431
+ notEvent: (type: StreamEvent["type"]) => aggregateScoped(Scoped.notEventOfType(type)),
432
+ calledSubagent: (name: string, match?: import("../types.ts").SubagentMatch) =>
433
+ aggregateScoped(Scoped.calledSubagent(name, match)),
434
+ eventOrder: (types: StreamEvent["type"][]) => aggregateScoped(Scoped.eventOrder(types)),
435
+ eventsSatisfy: (label: string, predicate: (events: readonly StreamEvent[]) => boolean) =>
436
+ aggregateScoped(Scoped.eventsSatisfy(label, predicate)),
437
+ maxTokens: (max: number) => aggregateScoped(Scoped.maxTokens(max)),
438
+ maxCost: (usd: number) => aggregateScoped(Scoped.maxCost(usd)),
331
439
  };
332
440
  const context = Object.defineProperties(
333
441
  {},
@@ -364,7 +472,13 @@ function mimeTypeFor(path: string): string {
364
472
  }
365
473
  }
366
474
 
367
- function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: ContextDeps, input: string): TurnHandle {
475
+ function makeTurnHandle(
476
+ turn: Turn,
477
+ collector: AssertionCollector,
478
+ deps: ContextDeps,
479
+ input: string,
480
+ coverage: ResolvedCoverage,
481
+ ): TurnHandle {
368
482
  const message = lastAssistantText(turn.events) ?? "";
369
483
  const facts = deriveRunFacts(turn.events);
370
484
  const usage = turn.usage ?? { inputTokens: 0, outputTokens: 0 };
@@ -379,6 +493,7 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
379
493
  facts,
380
494
  status: turn.status,
381
495
  usage,
496
+ coverage,
382
497
  }),
383
498
  });
384
499
 
@@ -404,13 +519,28 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
404
519
  collector.record({
405
520
  name: "outputEquals",
406
521
  severity: "gate",
407
- evaluate: () => (deepEqual(turn.data, value) ? 1 : 0),
522
+ evaluate: () => {
523
+ if (deepEqual(turn.data, value)) return 1;
524
+ // 正断言:data 通道非 complete 且这一轮根本没给 data,「没采到」不能算成「没输出」。
525
+ if (turn.data === undefined && coverage.data.status !== "complete") {
526
+ const c = coverage.data;
527
+ return unavailable(`coverage:data=${c.status}${c.reason ? ` (${c.reason})` : ""}`);
528
+ }
529
+ return { score: 0, expected: brief(value, 800), received: brief(turn.data, 800) };
530
+ },
408
531
  }),
409
532
  outputMatches: (schema) =>
410
533
  collector.record({
411
534
  name: "outputMatches",
412
535
  severity: "gate",
413
- evaluate: async () => ((await validateSchema(turn.data, schema)) ? 1 : 0),
536
+ evaluate: async () => {
537
+ if (await validateSchema(turn.data, schema)) return 1;
538
+ if (turn.data === undefined && coverage.data.status !== "complete") {
539
+ const c = coverage.data;
540
+ return unavailable(`coverage:data=${c.status}${c.reason ? ` (${c.reason})` : ""}`);
541
+ }
542
+ return { score: 0, received: brief(turn.data, 800) };
543
+ },
414
544
  }),
415
545
  messageIncludes: (token) => scoped(Scoped.messageIncludes(token)),
416
546
  succeeded: () => scoped(Scoped.succeeded()),
@@ -426,7 +556,7 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
426
556
  notEvent: (type) => scoped(Scoped.notEventOfType(type)),
427
557
  calledSubagent: (name, match) => scoped(Scoped.calledSubagent(name, match)),
428
558
  eventOrder: (types) => scoped(Scoped.eventOrder(types)),
429
- eventsSatisfy: (predicate, label) => scoped(Scoped.eventsSatisfy(predicate, label)),
559
+ eventsSatisfy: (label, predicate) => scoped(Scoped.eventsSatisfy(label, predicate)),
430
560
  maxTokens: (max) => scoped(Scoped.maxTokens(max)),
431
561
  maxCost: (usd) => scoped(Scoped.maxCost(usd)),
432
562
  judge: buildJudge({
@@ -526,7 +656,9 @@ function inputRequestMatches(request: InputRequest, filter?: InputRequestFilter)
526
656
  if (filter.display !== undefined && !stringMatches(request.display ?? "", filter.display)) return false;
527
657
  if (filter.action !== undefined && !stringMatches(request.action ?? "", filter.action)) return false;
528
658
  if (filter.optionIds !== undefined) {
659
+ // 「恰好提供这组选项」:集合完全一致(顺序无关),不是子集包含——写少一个选项不算命中。
529
660
  const optionIds = new Set((request.options ?? []).map((o) => o.id));
661
+ if (optionIds.size !== filter.optionIds.length) return false;
530
662
  if (!filter.optionIds.every((id) => optionIds.has(id))) return false;
531
663
  }
532
664
  if (filter.input !== undefined && !partialObjectMatches(request.input, filter.input)) return false;
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/eval/cases.md
1
2
  import { describe, expect, it } from "vitest";
2
3
 
3
4
  import { createAgentSession, SessionManager } from "./session.ts";