niceeval 0.6.0 → 0.6.1

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 (87) hide show
  1. package/dist/agents/types.d.ts +5 -1
  2. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  3. package/dist/report/built-ins/experiment-comparison.js +13 -0
  4. package/dist/report/built-ins/index.d.ts +1 -1
  5. package/dist/report/built-ins/index.js +1 -1
  6. package/dist/report/components.d.ts +3 -1
  7. package/dist/report/components.js +1 -1
  8. package/dist/report/compute.js +15 -33
  9. package/dist/report/format.d.ts +0 -6
  10. package/dist/report/format.js +1 -18
  11. package/dist/report/index.d.ts +2 -2
  12. package/dist/report/index.js +1 -1
  13. package/dist/report/locale.d.ts +13 -2
  14. package/dist/report/locale.js +26 -3
  15. package/dist/report/metrics.js +1 -1
  16. package/dist/report/react/AttemptList.d.ts +2 -2
  17. package/dist/report/react/AttemptList.js +3 -4
  18. package/dist/report/react/ExperimentList.d.ts +3 -2
  19. package/dist/report/react/ExperimentList.js +59 -7
  20. package/dist/report/react/MetricScatter.js +1 -5
  21. package/dist/report/react/fixtures.js +9 -4
  22. package/dist/report/react/format.d.ts +1 -1
  23. package/dist/report/react/format.js +1 -1
  24. package/dist/report/react/index.d.ts +1 -1
  25. package/dist/report/text/faces.js +71 -33
  26. package/dist/report/types.d.ts +3 -15
  27. package/docs-site/zh/concepts/adapter.mdx +2 -2
  28. package/docs-site/zh/concepts/assert.mdx +11 -10
  29. package/docs-site/zh/concepts/evals.mdx +7 -6
  30. package/docs-site/zh/concepts/overview.mdx +1 -1
  31. package/docs-site/zh/guides/agent-feedback-loop.mdx +9 -7
  32. package/docs-site/zh/guides/fixtures.mdx +3 -1
  33. package/docs-site/zh/guides/official-adapters.mdx +1 -1
  34. package/docs-site/zh/guides/publish-report.mdx +1 -1
  35. package/docs-site/zh/guides/report-components.mdx +13 -13
  36. package/docs-site/zh/guides/results-data.mdx +28 -2
  37. package/docs-site/zh/guides/sandbox-agent.mdx +1 -0
  38. package/docs-site/zh/guides/sandbox-providers.mdx +1 -1
  39. package/docs-site/zh/guides/viewing-results.mdx +6 -5
  40. package/docs-site/zh/guides/write-send.mdx +2 -1
  41. package/docs-site/zh/reference/cli.mdx +3 -3
  42. package/docs-site/zh/reference/define-agent.mdx +3 -1
  43. package/docs-site/zh/reference/events.mdx +2 -2
  44. package/docs-site/zh/reference/expect.mdx +10 -5
  45. package/package.json +1 -1
  46. package/src/agents/types.ts +5 -1
  47. package/src/i18n/en.ts +1 -2
  48. package/src/i18n/zh-CN.ts +1 -1
  49. package/src/report/built-in-user-parity.test.tsx +44 -47
  50. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  51. package/src/report/built-ins/index.ts +1 -1
  52. package/src/report/components.tsx +3 -1
  53. package/src/report/compute.ts +12 -36
  54. package/src/report/dual-render.test.tsx +33 -29
  55. package/src/report/format.ts +1 -16
  56. package/src/report/index.ts +1 -2
  57. package/src/report/locale.ts +26 -3
  58. package/src/report/metrics.ts +1 -1
  59. package/src/report/react/AttemptList.tsx +4 -6
  60. package/src/report/react/ExperimentList.tsx +167 -55
  61. package/src/report/react/MetricScatter.tsx +1 -11
  62. package/src/report/react/enhance.js +39 -0
  63. package/src/report/react/fixtures.ts +10 -4
  64. package/src/report/react/format.ts +1 -1
  65. package/src/report/react/index.tsx +0 -1
  66. package/src/report/react/render.test.tsx +24 -8
  67. package/src/report/react/styles.css +40 -13
  68. package/src/report/report.test.ts +2 -7
  69. package/src/report/text/faces.ts +76 -36
  70. package/src/report/types.ts +3 -15
  71. package/src/results/host-equivalence.test.ts +3 -3
  72. package/src/show/index.ts +8 -15
  73. package/src/show/render.ts +7 -204
  74. package/src/show/show.test.ts +22 -23
  75. package/src/view/data.test.ts +1 -1
  76. package/src/view/data.ts +6 -6
  77. package/src/view/index.ts +1 -1
  78. package/src/view/shared/types.ts +1 -1
  79. package/src/view/view-report.test.ts +5 -5
  80. package/dist/o11y/execution-tree.d.ts +0 -103
  81. package/dist/o11y/otlp/select.d.ts +0 -22
  82. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
  83. package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
  84. package/dist/results/annotated-source.d.ts +0 -61
  85. package/dist/results/attempt-evidence.d.ts +0 -69
  86. package/dist/results/attempt-source.d.ts +0 -15
  87. package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
@@ -4,7 +4,7 @@
4
4
  // 零 react、零 IO、纯同步 —— 这是 text 宿主不需要 react-dom 的那一半。
5
5
  // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典,
6
6
  // 默认 en 与历史输出逐字一致;数据(display、键、warnings message)不本地化。
7
- import { attemptItemReason, capabilityBadge, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
7
+ import { attemptItemReason, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
8
8
  import { countText, localeText, resolveMetricLabel } from "../locale.js";
9
9
  import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.js";
10
10
  import { renderTableText } from "./table.js";
@@ -290,10 +290,6 @@ export function scatterText(data, ctx) {
290
290
  if (drawable.length === 0) {
291
291
  return [localeText(locale, "scatter.noData", axes), ...footnotes].join("\n");
292
292
  }
293
- // 恰好 1 个可画点:成本 × 通过率的比较至少要两个实验,单点不成图。
294
- if (drawable.length === 1) {
295
- return [localeText(locale, "scatter.needTwo", axes), ...footnotes].join("\n");
296
- }
297
293
  // 点太密排不下时降级为坐标表,不硬挤
298
294
  if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
299
295
  const table = renderCoordinateTable(drawable.map((r) => ({ key: r.key, x: r.x.display, y: r.y.display })), { key: data.points, x: axisLabel(data.x, locale), y: axisLabel(data.y, locale) });
@@ -400,41 +396,84 @@ export function deltaText(data, ctx) {
400
396
  }
401
397
  // ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
402
398
  //
403
- // 三面共用的紧凑标记:`locator✓[E,X,⏱]`(判定符紧跟 locator,证据能力方括号紧跟判定符,
404
- // 中间不留空格)——docs-site/zh/guides/report-components.mdx「终端输出形成反馈闭环」定的形态。
399
+ // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
405
400
  // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
406
401
  // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
407
402
  // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
408
403
  function locatorBadge(item) {
409
- return `${item.locator}${verdictMark(item.verdict)}${capabilityBadge(item.capabilities)}`;
404
+ return `${item.locator}${verdictMark(item.verdict)}`;
410
405
  }
411
406
  // ── ExperimentList ──
412
- function experimentListEvalLine(row) {
413
- const badges = row.attempts.map(locatorBadge).join(" ");
414
- const trailer = row.verdict === "passed"
415
- ? [formatDurationMs(row.duration.value ?? 0), row.cost.value === null ? undefined : formatUSD(row.cost.value)]
416
- .filter((s) => s !== undefined)
417
- .join(" · ")
418
- : (row.reason ?? "");
419
- return ` ${verdictMark(row.verdict)} ${row.evalId} ${badges} ${trailer}`;
407
+ function experimentSummaryTable(items, ctx) {
408
+ const locale = ctx.locale;
409
+ const compact = ctx.width < 100;
410
+ const columns = [
411
+ { key: "experiment", header: compact && locale === "en" ? "Exp." : localeText(locale, "experimentList.experiment") },
412
+ { key: "model", header: localeText(locale, "table.model") },
413
+ { key: "agent", header: localeText(locale, "table.agent") },
414
+ { key: "duration", header: compact && locale === "en" ? "Avg" : localeText(locale, "experimentList.avgDuration"), align: "right" },
415
+ { key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
416
+ { key: "result", header: localeText(locale, "experimentList.result") },
417
+ { key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
418
+ { key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
419
+ ];
420
+ const rows = items.map((item) => ({
421
+ key: item.experimentId,
422
+ cells: {
423
+ experiment: item.experimentId,
424
+ model: item.model ?? localeText(locale, "experimentList.defaultModel"),
425
+ agent: item.agent,
426
+ duration: cellText(item.duration),
427
+ passRate: cellText(item.passRate),
428
+ result: verdictTallyText(item.verdicts, locale),
429
+ tokens: cellText(item.tokens),
430
+ cost: cellText(item.cost),
431
+ },
432
+ }));
433
+ const metadata = items.flatMap((item) => wrapDisplay(`${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
434
+ return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
420
435
  }
421
- export function experimentListText(items, ctx) {
436
+ function experimentDetailTable(item, ctx) {
422
437
  const locale = ctx.locale;
423
- if (items.length === 0)
424
- return localeText(locale, "attemptList.empty");
425
- const blocks = items.map((item) => {
426
- const identity = item.model ? `${item.experimentId} · ${item.agent} · ${item.model}` : `${item.experimentId} · ${item.agent}`;
427
- const summary = [
428
- `${localeText(locale, "overview.passRate")} ${cellText(item.passRate)}`,
429
- verdictTallyText(item.verdicts, locale),
430
- localeText(locale, "overview.attemptsCount", { n: item.attempts }),
431
- formatDurationMs(item.duration.value ?? 0),
432
- item.cost.value === null ? missingText(locale) : formatUSD(item.cost.value),
433
- ].join(" · ");
434
- const evalLines = item.evalRows.map(experimentListEvalLine);
435
- return [identity, ` ${summary}`, ...evalLines].join("\n");
438
+ const columns = [
439
+ { key: "status", header: localeText(locale, "experimentList.status") },
440
+ { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
441
+ { key: "result", header: localeText(locale, "experimentList.result") },
442
+ { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
443
+ { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
444
+ ];
445
+ const rows = item.evalRows.flatMap((row) => {
446
+ const parent = {
447
+ key: row.evalId,
448
+ cells: {
449
+ status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
450
+ entity: row.evalId,
451
+ result: "",
452
+ duration: "",
453
+ cost: "",
454
+ },
455
+ };
456
+ const attempts = row.attempts.map((attempt, index) => ({
457
+ key: attempt.locator,
458
+ cells: {
459
+ status: ` ${verdictMark(attempt.verdict)}`,
460
+ entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
461
+ result: attemptItemReason(attempt) ?? MISSING_MARK,
462
+ duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
463
+ cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
464
+ },
465
+ }));
466
+ return [parent, ...attempts];
436
467
  });
437
- return blocks.join("\n\n");
468
+ const flags = item.flags && Object.keys(item.flags).length > 0
469
+ ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
470
+ : undefined;
471
+ return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
472
+ }
473
+ export function experimentListText(items, ctx) {
474
+ if (items.length === 0)
475
+ return localeText(ctx.locale, "attemptList.empty");
476
+ return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
438
477
  }
439
478
  // ── EvalList ──
440
479
  function evalListAttemptLine(item) {
@@ -459,7 +498,7 @@ export function evalListText(items, ctx) {
459
498
  return blocks.join("\n\n");
460
499
  }
461
500
  // ── AttemptList ──
462
- /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本 + 证据能力,
501
+ /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
463
502
  * 然后逐条断言(gate 与 soft 都列,与 web 面的 AttemptRow 同一份材料)。 */
464
503
  function attemptListItemText(item, ctx, locale) {
465
504
  const head = [
@@ -468,7 +507,6 @@ function attemptListItemText(item, ctx, locale) {
468
507
  item.experimentId,
469
508
  formatDurationMs(item.durationMs),
470
509
  ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
471
- ...(capabilityBadge(item.capabilities) ? [capabilityBadge(item.capabilities)] : []),
472
510
  ].join(" · ");
473
511
  const lines = [head];
474
512
  if (item.error) {
@@ -1,10 +1,9 @@
1
1
  import type { AttemptHandle, SelectionWarning } from "../results/types.ts";
2
2
  import type { AttemptLocator } from "../results/locator.ts";
3
- import type { AttemptEvidenceCapabilities } from "../results/attempt-evidence.ts";
4
3
  import type { AssertionResult, Verdict } from "../types.ts";
5
4
  import type { LocalizedLabel, ReportLocale } from "./locale.ts";
6
5
  export type { SelectionWarning };
7
- export type { AttemptLocator, AttemptEvidenceCapabilities };
6
+ export type { AttemptLocator };
8
7
  export type { LocalizedLabel, ReportLocale };
9
8
  /** 两级聚合里单级的折叠方式。 */
10
9
  export type Aggregator = "mean" | "sum" | "min" | "max" | ((values: number[]) => number);
@@ -325,10 +324,8 @@ export interface DeltaData<K extends string = string> {
325
324
  }[];
326
325
  }
327
326
  /**
328
- * `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、error、耗时、成本、locator,外加
329
- * 证据能力标记(有没有保存的 Eval 源码 / 执行事件 / OTel 计时 / diff——与 `AttemptEvidence.capabilities`
330
- * 同一个类型,不重新发明四个布尔位的含义)。`ExperimentList` / `EvalList` 的下钻数组复用同一个
331
- * 类型,不是各自的精简版。
327
+ * `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、error、耗时、成本和 locator
328
+ * `ExperimentList` / `EvalList` 的下钻数组复用同一个类型,不是各自的精简版。
332
329
  */
333
330
  export interface AttemptListItem {
334
331
  evalId: string;
@@ -341,15 +338,6 @@ export interface AttemptListItem {
341
338
  durationMs: number;
342
339
  costUSD?: number;
343
340
  locator: AttemptLocator;
344
- /**
345
- * 证据能力标记:`eval`(运行时 Eval 源码已保存)、`execution`(执行事件非空)、
346
- * `timing`(执行事件 + 这次运行接入过 OTel)、`diff`(工作区有文件改动)。逐位定义与
347
- * `AttemptEvidence.capabilities` 完全一致——这里复用同一个类型,不重复声明四个布尔位的门槛。
348
- * 计算函数从 `EvalResult` 上已有的 `hasEvents` / `hasSources` / `hasTrace` 摘要位与一次
349
- * `attempt.diff()` 懒加载算出,不对每个 item 调用完整的 `loadAttemptEvidence`
350
- * (那还会额外装配 Eval 源码标注与 ExecutionTree,这里只要四个布尔位)。
351
- */
352
- capabilities: AttemptEvidenceCapabilities;
353
341
  }
354
342
  /**
355
343
  * `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)、失败原因摘要
@@ -194,7 +194,7 @@ interface AgentContext {
194
194
  readonly flags: Readonly<Record<string, unknown>>; // Tier 3 的 feature A/B:experiment.flags 透传;没配时是 {}
195
195
  readonly experimentId?: string; // 路径推导的实验 id,与结果里的 experimentId 同源;不经 experiment 跑时是 undefined
196
196
 
197
- readonly sandbox?: Sandbox; // defineSandboxAgent 构造的 Agent 会拿到
197
+ readonly sandbox: Sandbox; // sandbox Agent 也会拿到一个 Sandbox(remote agent 是无操作的 stub)
198
198
  }
199
199
 
200
200
  interface AgentSession {
@@ -239,7 +239,7 @@ interface Turn {
239
239
  ]
240
240
  ```
241
241
 
242
- 对象统共九种类型(`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)。
242
+ 对象统共十种类型(`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)。
243
243
 
244
244
  `data` 不是"随便放点什么"的口袋,它只有一条规则:**要什么,由 eval 在 send 上用 schema 声明;声明了才有 data,拿到手就是声明的类型。**
245
245
 
@@ -52,25 +52,26 @@ t.maxTokens(80_000).atLeast(0.7); // 降为 soft 并带阈值
52
52
 
53
53
  ## 判定规则
54
54
 
55
- 所有断言收齐后,运行器按这个顺序折叠成一个结果:
55
+ 所有断言收齐后,运行器按固定优先级取第一个成立项,折叠成一个结果:
56
56
 
57
57
  ```
58
- 执行出错(超时 / 异常 / 作者错误) failed
59
- 显式调用了 t.skip(reason) skipped
60
- 任一 gate 断言失败 failed
61
- 所有 gate 都过,但至少一个 soft 低于阈值 → passed(--strict 下标红)
62
- 否则 → passed
58
+ 执行异常、超时或作者错误 errored
59
+ 任一 gate 断言失败,或 --strict 下 soft 低于阈值 failed
60
+ 显式调用了 t.skip(reason) skipped
61
+ 否则 → passed
63
62
  ```
64
63
 
64
+ `errored` 压过一切,因为执行证据已经不可信;`failed` 压过 `skipped`,避免 `t.skip()` 掩盖此前记录的硬失败。
65
+
65
66
  <CardGroup cols={2}>
66
67
  <Card title="passed" icon="circle-check" color="#22c55e">
67
- 没有错误,所有 gate 断言通过,所有 soft 断言都达到阈值。
68
+ 没有错误,所有 gate 断言通过,所有 soft 断言都达到阈值(或未开 `--strict`)。
68
69
  </Card>
69
70
  <Card title="failed" icon="circle-xmark" color="#ef4444">
70
- 执行出错,或至少一个 gate 断言没通过。硬失败。
71
+ 至少一个 gate 断言没通过,或 `--strict` 下有 soft 断言低于阈值。硬失败。
71
72
  </Card>
72
- <Card title="passed(计分)" icon="chart-bar" color="#f59e0b">
73
- 所有 gate 都过,但至少一个 soft 低于阈值。质量回归——默认不报红,`--strict` 下才报红。
73
+ <Card title="errored" icon="triangle-exclamation" color="#f59e0b">
74
+ 执行异常、超时或作者错误,本次执行无法形成可信结论——不伪装成断言失败。
74
75
  </Card>
75
76
  <Card title="skipped" icon="forward" color="#6b7280">
76
77
  调用了 `t.skip("reason")`。完全排除在通过率统计之外。
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "NiceEval 中的 eval:生命周期、verdict 与文件"
3
3
  sidebarTitle: "评估"
4
- description: "eval 是一个测试用例:描述、agent 引用和 test 函数。了解 eval 如何被发现、调度、评分和报告。"
4
+ description: "eval 是一个测试用例:描述和 test 函数,agent-neutral。了解 eval 如何被发现、调度、评分和报告。"
5
5
  ---
6
6
 
7
7
  一个 eval 是一个可运行的测试用例。它通常由一个 `*.eval.ts` 文件导出,通过 `defineEval` 声明。
@@ -28,9 +28,10 @@ export default defineEval({
28
28
  | 字段 | 说明 |
29
29
  |---|---|
30
30
  | `description` | 给人看的描述,出现在报告里 |
31
- | `agent` | 使用哪个 Adapter,可由配置或 CLI 覆盖 |
32
31
  | `test(t)` | 交互和断言逻辑 |
33
32
 
33
+ eval 本身不声明用哪个 Agent——它默认保持 agent-neutral,同一个 eval 可以在不同 experiment 下跑不同 Agent。选哪个 Agent 是 experiment 的字段,不由 CLI 临时覆盖。
34
+
34
35
  <Note>
35
36
  不要手写 `id` 或 `name`。[NiceEval](https://niceeval.com/) 从文件路径推导 ID。
36
37
  </Note>
@@ -73,13 +74,13 @@ npx niceeval exp local weather/brooklyn
73
74
 
74
75
  <CardGroup cols={2}>
75
76
  <Card title="passed" icon="circle-check" color="#22c55e">
76
- 所有 gate 断言通过,并且没有未满足的硬失败。
77
+ 所有 gate 断言通过(`--strict` 下 soft 断言也都达到阈值),并且没有执行错误。
77
78
  </Card>
78
79
  <Card title="failed" icon="circle-xmark" color="#ef4444">
79
- 至少一个 gate 断言失败,或运行本身失败。
80
+ 至少一个 gate 断言失败,或 `--strict` 下有 soft 断言低于阈值。
80
81
  </Card>
81
- <Card title="passed" icon="chart-bar" color="#f59e0b">
82
- 没有 gate 失败,但 soft 分数需要保留为分值。
82
+ <Card title="errored" icon="triangle-exclamation" color="#f59e0b">
83
+ 执行异常、超时或作者错误,本次执行无法形成可信结论。
83
84
  </Card>
84
85
  <Card title="skipped" icon="forward" color="#6b7280">
85
86
  eval 主动跳过,通常通过 `t.skip(reason)`。
@@ -67,7 +67,7 @@ Subject under test / Sandbox backend
67
67
 
68
68
  <AccordionGroup>
69
69
  <Accordion title="Eval">
70
- 一个测试用例:描述、agent 引用和 `test(t)` 函数。
70
+ 一个测试用例:描述和 `test(t)` 函数,agent-neutral——用哪个 Agent 由 experiment 决定。
71
71
  </Accordion>
72
72
  <Accordion title="Agent">
73
73
  [NiceEval](https://niceeval.com/) 通过名字调用的一条连接,负责返回标准 `Turn`。
@@ -49,7 +49,7 @@ AI 通常按任务选择这些入口:
49
49
  npx niceeval show @1k2m9qtr
50
50
  ```
51
51
 
52
- 第一条命令显示当前各实验的通过率、成本、耗时,以及每个 Eval 的紧凑 Attempt locator。`[E]` / `[X]` / `[⏱]` / `[D]` 分别表示运行时 Eval 源码、执行步骤、OTel 时间和 diff 可用。第二条命令直接打开选中的 Attempt。
52
+ 第一条命令显示当前各实验的通过率、成本、耗时,以及每个 Eval 的紧凑 Attempt locator——locator 本身就是证据入口,不在列表里编码证据可用性。第二条命令直接打开选中的 Attempt,页面末尾的 `available:` 只列出这个 Attempt 实际可用的证据命令。
53
53
  </Step>
54
54
  <Step title="按问题读取证据">
55
55
  ```bash
@@ -68,7 +68,7 @@ AI 通常按任务选择这些入口:
68
68
  eval source: evals/weather/brooklyn.eval.ts · sha256:8d47…
69
69
 
70
70
  8 export default defineEval({
71
- 9 id: "weather/brooklyn",
71
+ 9 description: "Brooklyn weather query",
72
72
  10 async test(t) {
73
73
  11 const turn = await t.send("布鲁克林今天天气怎么样?");
74
74
  12 ✗ turn.calledTool("get_weather");
@@ -88,7 +88,8 @@ AI 通常按任务选择这些入口:
88
88
  received: 1
89
89
  source: evals/memory/swelancer-manager-proposals.eval.ts:40:11
90
90
 
91
- execution [X,⏱]
91
+ execution: 3 events · 0 skill loads · 0 tool calls · 1 AI messages
92
+ timing: OTel spans recorded for this attempt — see --execution for per-step timing.
92
93
  agent run ▕████████████████████▏ 41.2s
93
94
  ├─ inference ▕█████░░░░░░░░░░░░░░░▏ 10.1s · "布鲁克林今天大约 24°C,晴。"
94
95
  ├─ inference ▕░░░░░█████████░░░░░░▏ 18.3s
@@ -98,9 +99,10 @@ AI 通常按任务选择这些入口:
98
99
  changes · diff unavailable
99
100
  reason: this Attempt did not produce workspace file changes
100
101
 
101
- evidence: Eval source [E] · execution [X] · OTel timing [⏱]
102
102
  full eval source: …/weather/brooklyn/a2/eval-source.ts
103
- next: niceeval show @1k2m9qtr [--eval|--execution|--diff]
103
+ available:
104
+ niceeval show @1k2m9qtr --eval
105
+ niceeval show @1k2m9qtr --execution
104
106
  ```
105
107
 
106
108
  `--execution` 把 AI 消息、Skill load、工具调用和工具结果排成一棵执行树。它只展示 Agent 可理解的事件;没有关联到这些事件的 SDK / runtime span 不逐行输出,只报告省略数量并保留 `trace.json` 路径。下面的 Attempt 有 OTel,所以能关联的节点同时带相对时间与耗时:
@@ -155,7 +157,7 @@ AI 通常按任务选择这些入口:
155
157
  full events: …/weather/brooklyn/a2/events.json
156
158
  ```
157
159
 
158
- diff 是被测 Agent 在 Sandbox 工作区造成的文件变化,不是 Eval 源码的新旧差异。它只会出现在带 `[D]` locator 上。默认先给文件级摘要,避免把大段补丁塞进 Agent 上下文;`--diff=<文件>` 再展开单个文件,原始 artifact 路径始终保留:
160
+ diff 是被测 Agent 在 Sandbox 工作区造成的文件变化,不是 Eval 源码的新旧差异。只有 sandbox eval 才会收集到 diff——非 sandbox eval 或 agent 确实没碰任何文件时,attempt 页的 `available:` 列表会省略 `--diff`。默认先给文件级摘要,避免把大段补丁塞进 Agent 上下文;`--diff=<文件>` 再展开单个文件,原始 artifact 路径始终保留:
159
161
 
160
162
  ```text
161
163
  $ niceeval show @1c3h6tbn --diff
@@ -215,7 +217,7 @@ AI 通常按任务选择这些入口:
215
217
 
216
218
  ```text
217
219
  Failing:
218
- ✗ weather/brooklyn · @1k2m9qtr[E,X,⏱]
220
+ ✗ weather/brooklyn · @1k2m9qtr
219
221
  gate calledTool("get_weather"): tool was never called
220
222
 
221
223
  Results: 14 passed, 1 failed, 0 errored, 0 skipped
@@ -117,8 +117,10 @@ t.check(result.stdout.trim(), includes(/ok/));
117
117
  也可以运行项目自己的 test/lint/build 脚本:
118
118
 
119
119
  ```ts
120
+ import { commandSucceeded } from "niceeval/expect";
121
+
120
122
  const result = await t.sandbox.runCommand("npm", ["test"]);
121
- t.check(result.exitCode, includes(0));
123
+ t.check(result, commandSucceeded());
122
124
  ```
123
125
 
124
126
  ## 用 experiment 做对照组
@@ -14,7 +14,7 @@ description: "NiceEval 内置的 Sandbox 和非 Sandbox 适配器分别是什么
14
14
 
15
15
  - **鉴权**:`ANTHROPIC_API_KEY`(工厂参数 `apiKey` 可覆盖),可选 `ANTHROPIC_BASE_URL`(工厂参数 `baseUrl`)。
16
16
  - **装 MCP server**:`mcpServers` 配置项,`setup` 阶段写进沙箱里用户级的 `~/.claude.json`(顶层 `mcpServers` 字段)。
17
- - **装 Skill**:`skills: SkillSpec[]`——本地 Skill(`{ kind: "local", path }`,从项目根读文件或目录)或 Repo Skill(`{ kind: "repo", source, ref, skills }`,可钉 commit/tag、可只启用多 Skill 仓库里的一部分)。装进沙箱的 project 级 `.claude/skills/<name>/`,claude CLI 原生发现(原生 `Skill` 工具,可用 `t.calledTool("Skill", { input: { skill } })` 断言,不是 `t.loadedSkill()`——那是给 `load_skill` 这个不同协议约定的语法糖)。
17
+ - **装 Skill**:`skills: SkillSpec[]`——本地 Skill(`{ kind: "local", path }`,从项目根读文件或目录)或 Repo Skill(`{ kind: "repo", source, ref, skills }`,可钉 commit/tag、可只启用多 Skill 仓库里的一部分)。装进沙箱的 project 级 `.claude/skills/<name>/`,claude CLI 原生发现(原生 `Skill` 工具调用被 adapter 归一为 `skill.loaded` 事件,不重复记成工具调用;用 `t.loadedSkill()` 断言,不是 `t.calledTool("Skill", ...)`)。
18
18
  - **装原生 Plugin**:`plugins: ClaudeCodePluginSpec[]`,每一项声明 Marketplace 连接(`name` / `source` / 可选 `ref`)和其中的 Plugin 名。这个类型只属于 claude-code,传不进 codex。
19
19
  - **tracing**:claude CLI 的 beta 原生遥测(`CLAUDE_CODE_ENHANCED_TELEMETRY_BETA`),span 只有结构和计时,细节见 [OTel 接入](/zh/guides/connect-otel)。
20
20
 
@@ -10,7 +10,7 @@ description: "把报告站接进 CI:.niceeval/ 提交进仓库,构建命令一
10
10
 
11
11
  ```gitignore
12
12
  # .niceeval/ 提交进仓库:它是线上报告的数据源,CI 构建时直接从这里生成整站。
13
- # 只排除 diff.json —— 单轮可达上百 MB,查看器也不读它。
13
+ # 只排除 diff.json —— 单轮可达上百 MB,体积太大不适合进仓库;Attempt 详情仍会读取它,只是不随静态导出复制。
14
14
  .niceeval/**/diff.json
15
15
  ```
16
16
 
@@ -32,7 +32,7 @@ description: "报告文件里能摆的全部官方双面组件:每个组件回
32
32
  - **诚实渲染。** 缺数据渲染 `—` 不补 0;覆盖不全的格子带 `12/15` 角标;截断如实标注剩余数量与原始 artifact 路径。
33
33
  - **排序与方向随指标的 `better`。** higher 的指标降序、lower 的升序,「好」的一头恒在上、在右上。
34
34
  - **证据室深链。** 网页面的格子、点和条目深链到 Attempt 详情;终端面用同一 Attempt locator 交给 `niceeval show @<id>`。
35
- - **终端输出形成反馈闭环。** 每个 Attempt 有一个以 `@` 开头的短 locator,例如 `@1k2m9qrs`。它唯一指向 experiment、结果快照、Eval 和 Attempt。`✓` / `✗` / `!` / `–` 是 passed / failed / errored / skipped;`[E]` 表示运行时 Eval 源码可用,`[X]` 表示执行步骤可用,`[⏱]` 表示 OTel 时间可用,`[D]` 表示工作区 diff 可用。执行步骤统一包含消息、thinking、tool call/result 和 Skill load;OTel 只给这些步骤补时间,不另开一份输出。
35
+ - **终端输出形成反馈闭环。** 每个 Attempt 有一个以 `@` 开头的短 locator,例如 `@1k2m9qrs`。它唯一指向 experiment、结果快照、Eval 和 Attempt。`✓` / `✗` / `!` / `–` 是 passed / failed / errored / skipped。列表不用字母缩写编码证据可用性——locator 本身就是证据入口;打开 attempt(`niceeval show @<locator>`)后再列出实际可用的证据命令。执行步骤统一包含消息、thinking、tool call/result 和 Skill load;OTel 只给这些步骤补时间,不另开一份输出。
36
36
  - **两面同口径,网页面不依赖 JS 也完整。** 排序在计算时由 `sort` 定死,终端和网页看到同一份基准顺序;下钻是普通链接、展开折叠用 `<details>`。网页面另有一层浏览操作:点表头就地重排、榜单行过滤、图表点位悬停看数值——只影响眼前的视图,不改数据口径,刷新即回基准顺序;浏览器禁用 JS 时这些操作消失,内容一样不少(悬停信息退化为图内提示)。
37
37
 
38
38
  ## 排版原语
@@ -147,15 +147,15 @@ const experiments = await ExperimentList.data(selection);
147
147
  ```text
148
148
  compare/bub-gpt-5.4 · bub · gpt-5.4
149
149
  pass 87% · 13 passed / 1 failed / 1 errored · 42 attempts · 4m 12s · $0.42
150
- ✓ algebra/quadratic @12f9k3aq✓[E,X,⏱] @141bm7cx✓[E,X,⏱] 18s avg · $0.02 avg
151
- ✗ weather/brooklyn @1k2m9qrs✗[E,X,⏱] @1nx4dpqr✗[E,X] @19vq2jex✗[E,X] gate calledTool("get_weather")
152
- ! fixtures/button @1c3h6twx![E,D] command timed out after 120s
150
+ ✓ algebra/quadratic @12f9k3aq✓ @141bm7cx✓ 18s avg · $0.02 avg
151
+ ✗ weather/brooklyn @1k2m9qrs✗ @1nx4dpqr✗ @19vq2jex✗ gate calledTool("get_weather")
152
+ ! fixtures/button @1c3h6twx! command timed out after 120s
153
153
 
154
154
  compare/codex-gpt-5.4 · codex · gpt-5.4
155
155
  pass 80% · 12 passed / 3 failed · 45 attempts · 5m 03s · $0.51
156
- ✓ algebra/quadratic @1d8p4kwz✓[E,X,⏱] 16s · $0.02
157
- ✗ algebra/matrix @1e2j7mxa✗[E,X] @1f5r9nab✗[E,X,⏱] @1g4w8pcd✗[E,X,⏱] gate equals("42")
158
- ✗ weather/brooklyn @1h6t3vbe✗[E,X,⏱] @1j9m2qfg✗[E,X,⏱] @1k7c5rxh✗[E,X,⏱] gate calledTool("get_weather")
156
+ ✓ algebra/quadratic @1d8p4kwz✓ 16s · $0.02
157
+ ✗ algebra/matrix @1e2j7mxa✗ @1f5r9nab✗ @1g4w8pcd✗ gate equals("42")
158
+ ✗ weather/brooklyn @1h6t3vbe✗ @1j9m2qfg✗ @1k7c5rxh✗ gate calledTool("get_weather")
159
159
 
160
160
  inspect: niceeval show @<id> [--eval|--execution|--diff]
161
161
  ```
@@ -181,13 +181,13 @@ const evals = await EvalList.data(selection);
181
181
  ```text
182
182
  weather/brooklyn · compare/bub-gpt-5.4 · failed
183
183
  score 0.20 · 3 attempts · 41s avg · $0.04 avg
184
- @1k2m9qrs✗[E,X,⏱] · gate calledTool("get_weather") — tool was never called
185
- @1nx4dpqr✗[E,X] · gate calledTool("get_weather") — tool was never called
186
- @19vq2jex✗[E,X] · gate calledTool("get_weather") — tool was never called
184
+ @1k2m9qrs✗ · gate calledTool("get_weather") — tool was never called
185
+ @1nx4dpqr✗ · gate calledTool("get_weather") — tool was never called
186
+ @19vq2jex✗ · gate calledTool("get_weather") — tool was never called
187
187
 
188
188
  fixtures/button · compare/bub-gpt-5.4 · errored
189
189
  score — · 1 attempt · 2m 00s · $0.09
190
- @1c3h6twx![E,D] · command timed out after 120s
190
+ @1c3h6twx! · command timed out after 120s
191
191
 
192
192
  inspect: niceeval show @<id> [--eval|--execution|--diff]
193
193
  ```
@@ -209,13 +209,13 @@ const attempts = await AttemptList.data(selection, { redact });
209
209
  `niceeval show` 每项完整输出一个 Attempt,不再折叠到 Eval 汇总。这里已经是叶子层,只在末尾给一条与该 Attempt 可用证据对应的模板:
210
210
 
211
211
  ```text
212
- ✗ @1k2m9qrs · weather/brooklyn · compare/bub-gpt-5.4 · 41s · $0.04 · [E,X,⏱]
212
+ ✗ @1k2m9qrs · weather/brooklyn · compare/bub-gpt-5.4 · 41s · $0.04
213
213
  gate calledTool("get_weather") · failed
214
214
  tool was never called
215
215
  soft judge("回答基于实时数据") · 0.2/1
216
216
  reply invents a temperature
217
217
 
218
- ! @1c3h6twx · fixtures/button · compare/bub-gpt-5.4 · 2m 00s · $0.09 · [E,D]
218
+ ! @1c3h6twx · fixtures/button · compare/bub-gpt-5.4 · 2m 00s · $0.09
219
219
  command timed out after 120s
220
220
  inspect: niceeval show @<id> [--eval|--execution|--diff]
221
221
 
@@ -83,6 +83,31 @@ await attempt.sources(); // SourceArtifact[] | null
83
83
  - **读不了的落盘不静默。** 版本不兼容、目录损坏的快照进 `skipped` 并带原因;要不要展示由你定,但缺口永远被算出来。
84
84
  - **同进程内按句柄记忆化。** 两处都读同一个 `diff()` 不会把上百 MB 读两遍。
85
85
 
86
+ ## 超大输出会被截断
87
+
88
+ Agent 跑一条命令,输出可以大得离谱——一次递归 `grep` 扫进 `node_modules`,撞上压缩过的 JS 文件,单行就有几 MB。这种输出会同时进 `events.json` 和 `trace.json`,不管的话一个 attempt 就能占上百 MB。
89
+
90
+ 所以 NiceEval 落盘时会削:`events.json` 和 `trace.json` 里任何超过 256 KiB 的字符串,只保留前 256 KiB,末尾留一行说明:
91
+
92
+ ```text
93
+ [niceeval] truncated 51467156 → 262144 bytes
94
+ ```
95
+
96
+ **这不影响判定。** 断言读的是运行时的完整输出,截断只发生在写文件的那一刻——落盘的 artifact 是证据,不是评分的输入。断言该过还是过,该挂还是挂。
97
+
98
+ 要在自己的报告里如实标出「这里少了东西」,别去匹配上面那行文本,读结构化字段:被截断的事件和 span 都带 `truncated`,里面是被截断的位置和原始字节数。
99
+
100
+ ```typescript
101
+ const events = await attempt.events();
102
+ for (const e of events ?? []) {
103
+ for (const t of e.truncated ?? []) {
104
+ console.log(`${t.path} 原始 ${t.originalBytes} 字节,已截断`);
105
+ }
106
+ }
107
+ ```
108
+
109
+ `diff.json` 不截断——里面每个文件都要完整才能用来核对 agent 改了什么。它是唯一可能拖到上百 MB 的 artifact,所以发布时通常不带它(见下面的 `copySnapshots`)。
110
+
86
111
  ## 版本:谁写的、读不读得了
87
112
 
88
113
  每个快照都带自己的出身:`producer` 是写这份结果的工具与版本(niceeval 自己,或经写入面转换的第三方 harness),`schemaVersion` 是磁盘格式版本。格式只在破坏兼容时递增版本,读取器只认相同版本——**版本不兼容的落盘不解析、不迁移、不猜**,整个 run 进 `skipped`:
@@ -205,8 +230,9 @@ import { openResults, copySnapshots } from "niceeval/results";
205
230
 
206
231
  const results = await openResults(".niceeval");
207
232
  await copySnapshots(results.latest(), "site-data/run", {
208
- artifacts: ["sources", "events", "trace", "o11y"], // diff 可达百 MB,发布时常见地不带;
209
- }); // o11y 只有几 KB,报告用到 turns 这类
233
+ artifacts: ["sources", "events", "trace", "o11y"], // diff 不截断、可达百 MB,发布时常见地不带;
234
+ }); // events / trace 256 KiB 上限,带上是安全的;
235
+ // o11y 只有几 KB,报告用到 turns 这类
210
236
  // 读 o11y 的指标就把它带上,不然渲染成「—」
211
237
  ```
212
238
 
@@ -88,6 +88,7 @@ export default defineExperiment({
88
88
  createSandbox
89
89
  → sandbox spec 的 .setup() 钩子? # 环境准备(按实验装东西);没挂就跳过
90
90
  → git init && git commit
91
+ → eval 的 setup? # 这条 eval 的任务夹具(如果定义了)
91
92
  → adapter.setup? # 装 CLI / 写 agent 配置
92
93
  → test(t): uploadDirectory(...) # 写入这条 eval 的起始文件
93
94
  → adapter.send(input, ctx)
@@ -96,7 +96,7 @@ Vercel Sandbox 适合需要云端隔离、更多资源或更稳定环境的任
96
96
 
97
97
  - 减少 fixture 依赖体积。
98
98
  - 使用缓存或预热机制。
99
- - 控制 `sandboxConcurrency`,避免本地 Docker 资源耗尽。
99
+ - 控制 `maxConcurrency`(experiment 字段或 `--max-concurrency`),避免本地 Docker 资源耗尽。
100
100
  - 把慢测试拆成必要的 gate 和可选的 soft 检查。
101
101
 
102
102
  Warm pools 和复用属于 runner / scheduler 层面的能力,详见 [Runner](/zh/guides/runner)。
@@ -91,8 +91,8 @@ niceeval show weather/brooklyn --history # 跨 run 时间轴:抖动与回
91
91
  $ niceeval show weather/brooklyn
92
92
  weather/brooklyn — 布鲁克林天气查询
93
93
 
94
- compare/bub-gpt-5.4 ✓ passed 1 attempt 38.0s $0.03 (2h ago) @1xqur9kx[E,X,⏱]
95
- compare/codex-gpt-5.4 ✗ failed 3 attempts 41.2s $0.12 (40s ago) @1k2m9qrs[E,X,⏱] gate calledTool("get_weather")
94
+ compare/bub-gpt-5.4 ✓ passed 1 attempt 38.0s $0.03 (2h ago) @1xqur9kx
95
+ compare/codex-gpt-5.4 ✗ failed 3 attempts 41.2s $0.12 (40s ago) @1k2m9qrs gate calledTool("get_weather")
96
96
 
97
97
  attempt 3 · compare/codex-gpt-5.4 · failed · 41.2s · 12.3k tokens · $0.04
98
98
  ✗ gate calledTool("get_weather") — tool was never called
@@ -101,7 +101,7 @@ attempt 3 · compare/codex-gpt-5.4 · failed · 41.2s · 12.3k tokens · $0.04
101
101
 
102
102
  artifacts: .niceeval/compare_codex-gpt-5.4/2026-07-09T10-00-00-000Z-x1f2/weather/brooklyn/a2/
103
103
  attempt locator: @1k2m9qrs
104
- next: niceeval show @1k2m9qrs [--eval|--execution|--diff]
104
+ next: niceeval show @1k2m9qrs [--eval|--execution]
105
105
  ```
106
106
 
107
107
  每个 experiment 一行的紧凑索引末尾就带着代表 attempt 的 `@<locator>`(`compare/bub-gpt-5.4` 只有一次 attempt 就代表它自己,`compare/codex-gpt-5.4` 代表的是默认展开的那一次失败);展开的明细块结尾再重复一次精确的 `attempt locator:`,两处给的是同一个值。想看别的 attempt(比如 `compare/codex-gpt-5.4` 更早失败的那次),去 `--history` 或 artifact 目录里找到它的 locator,`niceeval show @<locator>` 直接跳过去,不需要先回到这张列表。
@@ -130,9 +130,10 @@ timing: OTel spans recorded for this attempt — see --execution for per-step ti
130
130
 
131
131
  changes: diff unavailable · no workspace diff was recorded for this attempt
132
132
 
133
- evidence: eval source [E] · execution [X] · OTel timing [⏱]
134
133
  artifacts: .niceeval/compare_codex-gpt-5.4/2026-07-09T10-00-00-000Z-x1f2/weather/brooklyn/a2/
135
- next: niceeval show @1k2m9qrs [--eval|--execution|--diff]
134
+ available:
135
+ niceeval show @1k2m9qrs --eval
136
+ niceeval show @1k2m9qrs --execution
136
137
  ```
137
138
 
138
139
  这份全景只给摘要,不复现某个切面的完整内容——完整源码、完整事件流或完整文件列表分别要对应的证据 flag 才给。`changes` 一行永远说明 diff 不可用的具体原因:这里是「这次 Attempt 从未收集过 diff」(`weather/brooklyn` 不是 sandbox eval,压根不会有工作区改动可收集);如果是 sandbox eval 但 agent 确实没碰任何文件,会是另一句「没有产生工作区文件改动」;两种「不可用」原因不同,不共用一句含糊的提示。
@@ -137,7 +137,7 @@ export default defineAgent({
137
137
 
138
138
  ## 第四步:把工具解析成事件
139
139
 
140
- 应用的返回里不只有回复文本——Chat Completions 形返回的 `tool_calls` 记录了这轮调过什么工具。Adapter 最重要的工作就是**把接口的返回归一成标准事件流**:本轮发生的每件事一个对象,按真实发生顺序排进 `Turn.events`,对象是下面九种类型之一(各字段的实际值,[契约页有一轮的完整示例](/zh/concepts/adapter)):
140
+ 应用的返回里不只有回复文本——Chat Completions 形返回的 `tool_calls` 记录了这轮调过什么工具。Adapter 最重要的工作就是**把接口的返回归一成标准事件流**:本轮发生的每件事一个对象,按真实发生顺序排进 `Turn.events`,对象是下面十种类型之一(各字段的实际值,[契约页有一轮的完整示例](/zh/concepts/adapter)):
141
141
 
142
142
  ```ts
143
143
  type StreamEvent =
@@ -145,6 +145,7 @@ type StreamEvent =
145
145
  | { type: "action.called"; callId: string; name: string; input: JsonValue; tool?: ToolName }
146
146
  | { type: "action.result"; callId: string; output?: JsonValue;
147
147
  status: "completed" | "failed" | "rejected" }
148
+ | { type: "skill.loaded"; skill: string; callId?: string }
148
149
  | { type: "subagent.called"; callId: string; name: string; remoteUrl?: string }
149
150
  | { type: "subagent.completed"; callId: string; output?: JsonValue;
150
151
  status: "completed" | "failed" }
@@ -25,7 +25,7 @@ description: "NiceEval CLI 参考:exp、show、view、init、list 和 clean
25
25
  在终端读取结果;用 `@<locator>` 或 eval ID 前缀下钻断言、Eval 源码、执行事件流、diff 和历史。
26
26
  </Card>
27
27
  <Card title="npx niceeval view" icon="eye">
28
- 打开网页结果查看器,读默认报告并交互浏览 Eval 源码、执行事件流、diff 与历史;默认范围与 `show` 相同。
28
+ 打开网页结果查看器,读默认报告并交互浏览 Eval 源码、执行事件流与 diff;默认范围与 `show` 相同。跨 run 时间轴(`--history`)是 `show` 专用的终端能力。
29
29
  </Card>
30
30
  </CardGroup>
31
31
 
@@ -138,9 +138,9 @@ npx niceeval show fixtures/button --diff
138
138
  npx niceeval show weather/brooklyn --history
139
139
  ```
140
140
 
141
- `show` 是终端结果入口,适合人直接阅读,也适合 coding agent 在上下文窗口里逐级下钻。位置参数选「看哪些 eval」(ID 前缀)或直接用 `@<locator>` 精确选一个 attempt;不带位置参数时显示默认报告(每行带紧凑 attempt 索引),指定到单个 eval 时显示各 experiment 的 attempt、断言、错误与用量。
141
+ `show` 是终端结果入口,适合人直接阅读,也适合 coding agent 在上下文窗口里逐级下钻。位置参数选「看哪些 eval」(ID 前缀)或直接用 `@<locator>` 精确选一个 attempt;不带位置参数时显示同一张紧凑 attempt 表,指定 eval ID 前缀只是收窄这张表的行数,不会切换成另一种视图。
142
142
 
143
- `@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动。`--eval`、`--execution`、`--diff` 也能配一个 eval ID 前缀使用,但要求最终只匹配一个 eval——多 experiment 或多 attempt 时默认选择最新一次失败的 attempt;用 `--experiment` 收窄实验,或直接用 `@<locator>` 精确指名。`--run` 钉住某次结果目录,`--history` 查看跨 run 趋势。完整的阅读顺序、输出示例和 artifact 说明见[查看结果](/zh/guides/viewing-results)。
143
+ `@<locator>` 不带证据 flag 时给出该 attempt 的紧凑全景(断言摘要、执行摘要、可选 OTel 时间、diff 摘要);`--eval`、`--execution`、`--diff` 是同一 attempt 的证据切面,分别展开运行时保存的 Eval 源码、标准执行事件流、工作区文件改动,因此必须搭配 `@<locator>` 精确指名一个 attempt。`--run` 钉住某次结果目录,`--history` 查看跨 run 趋势。完整的阅读顺序、输出示例和 artifact 说明见[查看结果](/zh/guides/viewing-results)。
144
144
 
145
145
  ## `--early-exit` 与 `--strict`
146
146
 
@@ -245,7 +245,9 @@ experiment 跑(如脱离 CLI、直接构造 `AgentRun` 的场景)时为 undefine
245
245
  readonly sandbox: Sandbox;
246
246
  ```
247
247
 
248
- 仅沙箱型 agent 有(运行器按 --sandbox 备好)。
248
+ 所有 agent 都有:沙箱型是运行器按项目/experiment 配置备好的真实沙箱句柄,remote 型是
249
+ `createRemoteSandbox()` 产出的 stub(仅含 `workdir`/`sandboxId`/`otlpHost`/`stop` 等
250
+ 元信息,其余方法调用即抛错)。
249
251
 
250
252
  #### `session`
251
253