niceeval 0.2.1 → 0.4.0

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 (113) hide show
  1. package/README.md +34 -34
  2. package/README.zh.md +29 -15
  3. package/docs/README.md +2 -1
  4. package/docs/adapters/collection.md +1 -1
  5. package/docs/adapters/contract.md +6 -5
  6. package/docs/adapters/reference/claude-code-otel-telemetry.md +1 -1
  7. package/docs/architecture.md +20 -20
  8. package/docs/assertions.md +0 -1
  9. package/docs/capabilities-by-construction.md +2 -2
  10. package/docs/cli.md +2 -2
  11. package/docs/concepts.md +2 -2
  12. package/docs/e2e-ci.md +135 -98
  13. package/docs/eval-authoring.md +3 -3
  14. package/docs/experiments.md +7 -4
  15. package/docs/observability.md +9 -5
  16. package/docs/origin-integration.md +18 -18
  17. package/docs/references.md +1 -1
  18. package/docs/reports.md +551 -0
  19. package/docs/results-format.md +3 -3
  20. package/docs/results-lib.md +191 -0
  21. package/docs/runner.md +1 -1
  22. package/docs/sandbox.md +1 -1
  23. package/docs/source-map.md +22 -2
  24. package/docs/tier-sync.md +74 -58
  25. package/docs/view.md +41 -4
  26. package/package.json +29 -3
  27. package/src/agents/ai-sdk.ts +3 -0
  28. package/src/agents/claude-code.ts +18 -1
  29. package/src/agents/codex.ts +3 -2
  30. package/src/agents/index.ts +16 -0
  31. package/src/agents/openai-compat.test.ts +56 -0
  32. package/src/agents/openai-compat.ts +151 -0
  33. package/src/agents/types.ts +3 -1
  34. package/src/cli.ts +2 -1
  35. package/src/context/context.test.ts +78 -1
  36. package/src/context/context.ts +19 -11
  37. package/src/context/session.ts +2 -0
  38. package/src/context/types.ts +11 -5
  39. package/src/i18n/en.ts +8 -4
  40. package/src/i18n/zh-CN.ts +6 -3
  41. package/src/o11y/cost.test.ts +40 -0
  42. package/src/o11y/cost.ts +27 -5
  43. package/src/o11y/derive.ts +2 -2
  44. package/src/o11y/otlp/mappers/claude-code.test.ts +31 -0
  45. package/src/o11y/otlp/mappers/claude-code.ts +24 -0
  46. package/src/o11y/otlp/mappers/index.ts +1 -0
  47. package/src/o11y/otlp/parse.test.ts +127 -0
  48. package/src/o11y/prices.json +176 -119
  49. package/src/o11y/types.ts +2 -1
  50. package/src/report/aggregate.ts +215 -0
  51. package/src/report/compute.ts +405 -0
  52. package/src/report/format.ts +47 -0
  53. package/src/report/index.ts +40 -0
  54. package/src/report/metrics.ts +96 -0
  55. package/src/report/react/CaseList.tsx +70 -0
  56. package/src/report/react/DeltaTable.tsx +79 -0
  57. package/src/report/react/MetricMatrix.tsx +75 -0
  58. package/src/report/react/MetricScatter.tsx +217 -0
  59. package/src/report/react/MetricTable.tsx +71 -0
  60. package/src/report/react/RunOverview.tsx +87 -0
  61. package/src/report/react/Scoreboard.tsx +87 -0
  62. package/src/report/react/cell.tsx +49 -0
  63. package/src/report/react/colors.ts +42 -0
  64. package/src/report/react/data.ts +17 -0
  65. package/src/report/react/fixtures.ts +236 -0
  66. package/src/report/react/format.ts +34 -0
  67. package/src/report/react/index.tsx +34 -0
  68. package/src/report/react/render.test.tsx +303 -0
  69. package/src/report/react/styles.css +302 -0
  70. package/src/report/report.test.ts +667 -0
  71. package/src/report/types.ts +210 -0
  72. package/src/results/copy.ts +200 -0
  73. package/src/results/format.ts +75 -0
  74. package/src/results/index.ts +27 -0
  75. package/src/results/open.ts +207 -0
  76. package/src/results/results.test.ts +359 -0
  77. package/src/results/select.ts +101 -0
  78. package/src/results/types.ts +98 -0
  79. package/src/runner/attempt.ts +3 -1
  80. package/src/runner/report.test.ts +111 -0
  81. package/src/runner/report.ts +52 -1
  82. package/src/runner/reporters/artifacts.ts +1 -1
  83. package/src/runner/reporters/braintrust.test.ts +106 -0
  84. package/src/runner/reporters/braintrust.ts +197 -0
  85. package/src/runner/reporters/console.ts +3 -3
  86. package/src/runner/reporters/index.ts +3 -1
  87. package/src/runner/reporters/live.ts +17 -5
  88. package/src/runner/reporters/shared.ts +3 -0
  89. package/src/runner/reporters/table.ts +1 -0
  90. package/src/runner/run.ts +31 -18
  91. package/src/runner/types.ts +20 -1
  92. package/src/sandbox/types.ts +3 -3
  93. package/src/shared/format.ts +8 -2
  94. package/src/view/app/App.tsx +82 -17
  95. package/src/view/app/components/AttemptModal.tsx +9 -2
  96. package/src/view/app/components/CodeView.tsx +2 -2
  97. package/src/view/app/components/CostScoreChart.tsx +127 -0
  98. package/src/view/app/components/LazyArtifact.tsx +2 -1
  99. package/src/view/app/i18n.ts +13 -1
  100. package/src/view/app/lib/artifact-url.ts +6 -0
  101. package/src/view/app/lib/attempt-route.test.ts +89 -0
  102. package/src/view/app/lib/attempt-route.ts +52 -0
  103. package/src/view/app/lib/chart.ts +72 -0
  104. package/src/view/app/lib/outcome.ts +1 -1
  105. package/src/view/app/types.ts +4 -5
  106. package/src/view/client-dist/app.css +1 -1
  107. package/src/view/client-dist/app.js +18 -18
  108. package/src/view/index.ts +36 -7
  109. package/src/view/loader.ts +14 -7
  110. package/src/view/server.ts +7 -0
  111. package/src/view/shared/types.ts +14 -1
  112. package/src/view/styles.css +43 -0
  113. package/src/view/template.html +0 -1
package/docs/e2e-ci.md CHANGED
@@ -1,15 +1,17 @@
1
- # E2E CI(设计提案,未实现)
1
+ # E2E CI(部分落地)
2
2
 
3
- > 状态:设计提案。本文描述 niceeval 的端到端 CI 测试方案——用真实的 `niceeval exp` 全链路(发现 调度 断言 评分 工件 退出码)验证框架和官方适配路径,而不是只跑 typecheck。当前 `.github/workflows/ci.yml` 只有 typecheck / site:build / docs 校验,没有任何 workflow 真正执行过 eval
3
+ > 状态:核心骨架已落地并在本地跑绿(2026-07-07)。`e2e/` 目录已建:shared factory/profile 套件、从 tier1 拷来的五个被测应用(`e2e/apps/`)、五个 SDK 薄项目(`e2e/projects/`)、`e2e/scripts/verify.mjs`——全矩阵 6 次 CLI 调用(5 ci 全绿 exit 0 + ai-sdk-v7 verdicts 按期望 exit 1)全部符合期望表。**尚未落地**:`.github/workflows/e2e.yml`(GitHub secrets 还没建)、`aisdk-transformer` / `http-mapping` 两个纯框架项目、示例冒烟 job(第 6 节,P0 文档债也还没修)、L1 nightly 沙箱矩阵。与原提案的实现偏差:verdicts fixture 挂在最便宜的 ai-sdk-v7 上(纯框架项目未建);`ciExperiment(agent)` 不吃 profile——能力过滤靠"stub 文件是否存在",ci/verdicts 切分靠 `evals` 谓词按 `deliberate-` 前缀;profile 实际字段是 `weatherToolName / calcToolName / usage / sandboxTools / workspaceDir`(hitl 由 `calcToolName` 非空推导);`e2e/apps` 不是逐字节拷贝而是**只保 backend 的协议夹具**——前端、vite/开发工作流、langgraph 的 node 侧文件和 Jaeger compose 都已裁掉(前端不参与被测协议,裁掉能显著缩短 CI 安装),将来 tier1 → e2e/apps 的 drift check 对比范围只框 `src/backend/`;另注意 `budget` 护栏对不报 usage 的 agent 无法执行(见 `memory/e2e-suite-landing-gotchas.md`)
4
+ >
5
+ > 本文其余部分保持原提案文本——用真实的 `niceeval exp` 全链路(发现 → 调度 → 断言 → 评分 → 工件 → 退出码)验证框架和官方适配路径,而不是只跑 typecheck。当前 `.github/workflows/ci.yml` 只有 typecheck / site:build / docs 校验,没有任何 workflow 真正执行过 eval。**e2e 全程不允许假 AI**:所有被测应用和框架级项目都调真实模型,CI 里的 key 从 GitHub Actions secrets 注入,本地开发者自己在 `.env` 放真实 key——这和 `examples/zh/origin` 那 6 个示例的既有政策一致(见 `memory/origin-examples-real-ai-credentials.md`),e2e 不搞例外。
4
6
 
5
7
  ## 1. 目标
6
8
 
7
9
  一次 e2e CI 要同时证明五件事:
8
10
 
9
11
  1. **完整路径**:从 `evals/` 发现、experiment 展开运行矩阵、`t.*` 断言收集、gate/soft 判决、`.niceeval/<run>/` 工件落盘,到进程退出码,每一环都被真实执行并被机器校验——包括"该红的时候红"(deliberate-fail 必须 exit 1),不是只测 happy path。
10
- 2. **一套 suite,全矩阵 100% pass**:所有 SDK 适配项目共用**同一份** eval/experiment 定义(单一事实来源,见第 3 节),CI 把每个项目都跑一遍,期望全部 pass——任何一个 SDK 的适配层回归都会把矩阵打红。
12
+ 2. **一套 suite,全矩阵可靠通过**:所有 SDK 适配项目共用**同一份** eval/experiment 定义(单一事实来源,见第 3 节),CI 把每个项目都跑一遍。真实模型跑真实调用,单次尝试允许抖动,但有限重试(见 4.1)吸收不了的失败一定是真回归——任何一个 SDK 的适配层回归都会把矩阵打红,不会被"模型这次没答好"这种噪音长期掩盖,也不会被 retry 永久掩盖。
11
13
  3. **正反验证所有功能**:套件对每个功能都配正反两条用例——该调工具时 `t.calledTool`、不该调时 `t.notCalledTool`/`t.usedNoTools`、HITL 有 approve 也有 deny、会话有记忆也有 `newSession` 隔离;再加 deliberate-fail/error 验证"断言真的会挂"。防止"断言永真"这类静默失效。
12
- 4. **重复运行统计**:一个 experiment 配 `runs: 100, earlyExit: false`,验证 100 次重复的调度、并发、pass 率计数正确(niceeval 的字段就叫 `runs`,不叫 pass/trials;`earlyExit` 不关掉的话第一次通过就会 abort 其余 attempt,拿不到完整分布)
14
+ 4. **重复运行统计**:一个 experiment 配 `runs: N, earlyExit: false` 跑重复调度、并发、pass 率计数(niceeval 的字段就叫 `runs`,不叫 pass/trials;`earlyExit` 不关掉的话第一次通过就会 abort 其余 attempt,拿不到完整分布)。真实模型调用要花钱,PR 门禁上 `N` 取小值(如 10),完整的大样本统计(如 100)挪到 nightly 跑,见第 4 节。
13
15
  5. **官方适配矩阵**:内建 `uiMessageStreamAgent`、`fromAiSdk`/`fromClaudeSdkMessages`/`fromCodexThreadEvents`/`fromPiAgentEvents` 转换器、自写 `defineAgent` 映射,以及沙箱型 claude-code / codex / bub × docker / e2b / vercel,每条官方路径至少一条 CI 覆盖。
14
16
 
15
17
  ## 2. 现状盘点
@@ -24,11 +26,17 @@ e2e 从 tier1 拷的是**被测应用和 adapter**(各 SDK 真正不同的部分
24
26
 
25
27
  所以 e2e 的"一套 eval"不能靠字面复制或软链实现,要靠参数化新写(见第 3 节);tier1 的这些差异正好告诉我们 profile 里要有哪些开关。另外源码层面确认:`src/runner/discover.ts` 固定扫 `<root>/evals` 与 `<root>/experiments`,`walkFiles` 用 Dirent 的 `isDirectory()/isFile()` 判断——**symlink 条目两者都返回 false,逐文件或逐子目录软链不会被发现**;整个 `evals/` 目录本身做软链虽可行(readdir 跟随路径),但前提是文件逐字同构,上面已经否定了。
26
28
 
29
+ 另外,tier1 各项目本来就已经在调真实模型——`claude-sdk`/`pi-sdk`/`langgraph` 的 backend 用 `process.env.AGENT_MODEL ?? "deepseek-v4-flash"` 走 DeepSeek 兼容端点,`codex-sdk` 默认 `"gpt-5.4"` 走 Codex 代理,凭据映射和 `examples/zh/origin` 一致(见 `memory/origin-examples-real-ai-credentials.md`)。e2e/apps 直接复用这套映射即可,不需要为"跑得起来"发明新机制,只需要在 CI 里把对应 key 从 GitHub Actions secrets 注入(见第 3.2、7 节)。
30
+
31
+ ### `examples/zh/ai-sdk` 的 README 是需要清理的过期文档
32
+
33
+ `examples/zh/ai-sdk/README.md` 现在写着"默认是 `AGENT_MODE=mock`,不需要 API key",但实测 `src/server.ts`、`src/ai-sdk-runtime.ts`、`src/models.ts` 里**完全没有** `AGENT_MODE` 分支或任何 mock provider——`resolveModel()` 无条件要求真实 `OPENAI_API_KEY`/`DEEPSEEK_API_KEY`,`.env.example` 里也不提 `AGENT_MODE`。这条 mock 声明本身就是过期文档,不是 e2e 该保留或恢复的能力——落地 L1(第 6 节)之前要先把 README 这几行改成真实 key 说明,而不是去"实现"一个文档里写着但代码里没有、而且按项目政策也不该有的 mock 分支。
34
+
27
35
  ### 框架侧对 CI 重要的事实(源码已确认)
28
36
 
29
- - **退出码**:全过/跳过 → 0;任一 failed 或 errored → 1;框架崩溃 → 2(`src/cli.ts:414`)。CI 判成败首选退出码,细分读 `summary.json`。
30
- - **指纹缓存会静默跳过上次 passed 的 eval**(`src/runner/run.ts:117-135`)。CI 必须加 `--force`,或保证 `.niceeval/` 不跨 run 复用,否则回归会被缓存掩盖。
31
- - **judge 无 key 时 no-op**(`src/scoring/judge.ts:162`):不配 judge key,`t.judge.autoevals.*` 断言静默跳过、不判红。mock 层利用这一点零 key 跑;"judge 真的在判"要单独在有 key 的层验证。
37
+ - **退出码**:全过/跳过 → 0;任一 failed 或 errored → 1;框架崩溃 → 2(`src/cli.ts:461,468`)。CI 判成败首选退出码,细分读 `summary.json`。
38
+ - **指纹缓存会静默跳过上次 passed 的 eval**(`src/runner/run.ts` 的 `run()`,匹配逻辑在文件开头 ~60 行附近,以 `cacheKey`/`computeFingerprint` 为核心)。CI 必须加 `--force`,或保证 `.niceeval/` 不跨 run 复用,否则回归会被缓存掩盖。
39
+ - **judge 无 key 时 no-op**(`src/scoring/judge.ts` 的 `resolveJudge()`/`buildJudge()`,`if (!resolved.apiKey) return noOpJudge()`):不配 judge key,`t.judge.autoevals.*` 断言静默跳过、不判红。e2e "judge 真的在判"必须显式配 judge key 验证,不能靠这条 no-op 蒙混过关——这一点和是否用真实模型无关,是两个独立的开关。
32
40
  - **可用 flags**:`--runs`、`--no-early-exit`、`--force`、`--junit <path>`、`--strict`、`--max-concurrency`。**不要用** `--json`(死 flag)、`--reporter`(不存在)、`--agent`/`--model`(exp 下报错)、`--sandbox`(已移除)。
33
41
  - **`runs` 的语义**:每个 `(agent × model × eval)` 组合跑 `runs` 次;被 earlyExit abort 的 attempt 不计入分母。
34
42
 
@@ -41,9 +49,9 @@ e2e/
41
49
  shared/ # 唯一一份 eval / experiment 定义(单一事实来源)
42
50
  profile.ts # AgentProfile 类型:工具名、能力开关
43
51
  evals.ts # weatherTool(p) / basicQa(p) / hitlApprove(p) ... 的 factory
44
- experiments.ts # ciExperiment(agent, p) / pass100(agent, p) factory
52
+ experiments.ts # ciExperiment(agent, p) / passN(agent, p) factory
45
53
  verdicts.ts # deliberateFail() / deliberateError() factory(只进 verdicts 实验)
46
- apps/ # 被测应用,从 examples/zh/tier1/<name> 拷来,加确定性 mock 开关
54
+ apps/ # 被测应用,从 examples/zh/tier1/<name> 拷来,原样保留真实调用
47
55
  ai-sdk-v7/ claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
48
56
  projects/ # 每个 SDK 一个 niceeval 项目:adapter + profile + 3 行 stub
49
57
  ai-sdk-v7/
@@ -54,10 +62,9 @@ e2e/
54
62
  evals/... # 其余 stub,一 eval 一文件,保住可读的 eval id
55
63
  experiments/ci.ts # stub:export default ciExperiment(agent, profile)
56
64
  claude-sdk/ codex-sdk/ pi-sdk/ langgraph/
57
- mock-ai-sdk/ # 纯框架项目:defineAgent + fromAiSdk 消费本地 mock server
58
- mock-http/ # 纯框架项目:defineAgent + 手写映射,自定义 JSON 协议
65
+ aisdk-transformer/ # 纯框架项目:defineAgent + 官方 fromAiSdk,进程内直调真实 DeepSeek
66
+ http-mapping/ # 纯框架项目:defineAgent + 手写映射,进程内直调真实 DeepSeek REST
59
67
  scripts/
60
- mock-server.mjs # mock-ai-sdk / mock-http 共用的确定性本地 server
61
68
  verify.mjs # 元校验:跑 CLI 子进程,对照期望表检查退出码 + summary.json
62
69
  ```
63
70
 
@@ -66,12 +73,14 @@ e2e/
66
73
  eval 定义本来就不 import agent(agent 由 experiment 绑定),所以共享 eval 在框架模型上是顺的;挡路的只有两件事:discover 只认 `<root>/evals` 下的实体文件(symlink 条目不被发现,见第 2 节),以及各 SDK 的协议差异(工具名、usage、HITL 支持)。两个问题一个解法——共享层导出**参数化的 factory**,每个项目用一个 `profile.ts` 声明自己的协议现实,`evals/` 下只放 stub 文件喂给 discover:
67
74
 
68
75
  ```typescript
69
- // e2e/shared/profile.ts
76
+ // e2e/shared/profile.ts(与源码同步)
70
77
  export interface AgentProfile {
71
- weatherToolName: string; // "get_weather" 或 "mcp__demo-tools__get_weather"
72
- usage: boolean; // 协议是否携带 usage(决定 basic-qa 是否断言 maxTokens)
73
- hitl: boolean; // 是否支持 approve/deny(codex-sdk 为 false)
74
- sandboxTools: boolean; // 是否是 coding agent(决定 create-file/run-command 是否生效)
78
+ weatherToolName: string | null; // "get_weather" 或 MCP 命名空间名;coding agent 为 null
79
+ calcToolName: string | null; // 经审批门控的计算器;不支持 HITL(codex-sdk)为 null
80
+ searchToolName: string | null; // 网络搜索工具;只有 ai-sdk-v7 的被测应用注册了
81
+ usage: boolean; // 协议是否携带 usage(决定 basic-qa 是否断言 maxTokens)
82
+ sandboxTools: boolean; // 是否是 coding agent(决定 create-file/run-command 等是否生效)
83
+ workspaceDir?: string; // coding agent 的工作目录(eval 直接读磁盘核实)
75
84
  }
76
85
  ```
77
86
 
@@ -131,56 +140,89 @@ export function noToolChitchat(p: AgentProfile) {
131
140
 
132
141
  **防静默失配**:profile 关掉一个能力,对应 eval stub 就不该存在于该项目——verify.mjs 按 profile 算出每个项目的期望 eval 数,和 `summary.json` 的 results 数对账,防止"少排了用例还全绿"。
133
142
 
134
- ### 3.2 apps:拷贝自 tier1,允许受控漂移
143
+ ### 3.2 apps:拷贝自 tier1,真实 key 从 secrets 注入
135
144
 
136
- `e2e/apps/<name>` 从 `examples/zh/tier1/<name>` 拷贝,保留原有的高位端口分配,只加一件事:**确定性 mock 开关**——CI 100% pass,被测应用就不能连真模型。规则和现有 `examples/zh/ai-sdk` `AGENT_MODE=mock` 一致:提示词含"天气"就走一次天气工具调用并回答含 "25" 的固定文案,否则纯文本寒暄作答。mock 发生在**模型层**,SDK 的协议栈(流式帧、工具调用事件、HITL 中断)全部真实走一遍——这正是 e2e 要测的东西。
145
+ `e2e/apps/<name>` 从 `examples/zh/tier1/<name>` 拷贝,保留原有的高位端口分配,**不加任何 mock 分支**——tier1 里这些应用本来就已经是真实调用(`AGENT_MODEL ?? "deepseek-v4-flash"` / Codex 代理),e2e 直接复用同一套凭据映射即可。CI 里跑这些应用时,workflow `DEEPSEEK_API_KEY`(claude-sdk / pi-sdk / langgraph / ai-sdk-v7,分别走 Anthropic 兼容和 OpenAI 兼容两个 base URL)、`CODEX_API_KEY`(codex-sdk)从 GitHub Actions secrets 注入进子进程环境,本地开发者跑同样的套件就在自己的 `.env` 里放同一把 key( tier1 项目现在的开发方式完全一致,没有新概念)
137
146
 
138
- SDK mock 可行性不同,按下表分层(不确定的先验证再落地,验证结果记 `memory/`):
147
+ 为了让 PR 门禁的花费可控,e2e/apps 统一钉住**便宜模型**(DeepSeek `deepseek-v4-flash` / Codex 代理默认模型),更贵的模型对比矩阵挪到 4.2 的 nightly 层。
139
148
 
140
- | app | mock 策略 | PR 门禁? |
141
- |---|---|---|
142
- | ai-sdk-v7 | app 内加 mock 分支(AI SDK 支持自定义 LanguageModel,或规则函数直出) | 是 |
143
- | langgraph | LangChain 系有 FakeChatModel 类工具,Python 侧替换模型即可 | 是 |
144
- | pi-sdk | 待验证 pi SDK 的模型注入点;不可行则留 nightly | 待验证 |
145
- | claude-sdk | Claude Agent SDK 走 claude CLI → Anthropic API,mock 要 `ANTHROPIC_BASE_URL` 指本地 API 形状 mock,可行性待验证;不可行则留 nightly | 待验证 |
146
- | codex-sdk | 同理,待验证 codex 的 base-url 注入;不可行则留 nightly | 待验证 |
149
+ 拷贝的代价是和 tier1 漂移。缓解:`scripts/sync-tiers.mjs` 已经在做 origin tier1 的同步,给它加一条 tier1 → `e2e/apps` 的 check,`pnpm tiers:check` 顺带守住。e2e/apps 的定位是"协议保真的测试夹具",不承担 tier1 的文档叙事职责,允许它为可测性加代码(比如把端口做成可配置的),但不允许加假响应。
147
150
 
148
- 拷贝的代价是和 tier1 漂移。缓解:`scripts/sync-tiers.mjs` 已经在做 origin → tier1 的同步,给它加一条 tier1 → `e2e/apps` check(允许 mock 开关相关文件在白名单里差异),`pnpm tiers:check` 顺带守住。e2e/apps 的定位是"协议保真的测试夹具",不承担 tier1 的文档叙事职责,允许它为可测性加代码。
151
+ ### 3.3 aisdk-transformer / http-mapping:不依赖任何 app 进程的框架基线
149
152
 
150
- ### 3.3 mock-ai-sdk / mock-http:不依赖任何 app 的框架基线
153
+ PR 门禁需要一个启动成本最低、最便宜的真实基线,不依赖起 5 个 SDK app 进程。`e2e/projects/aisdk-transformer` 和 `http-mapping` 不需要 `e2e/apps` 里的任何应用——adapter 在进程内直接调真实 DeepSeek,不经过任何被测服务器:
151
154
 
152
- 即使 claude-sdk / codex-sdk 的 mock 验证不可行,PR 门禁也必须有一个永远零依赖的全绿基线。`e2e/projects/mock-ai-sdk` 和 `mock-http` 不需要 `e2e/apps` 里的任何应用,只消费 `scripts/mock-server.mjs` 起的本地确定性 server:
155
+ ```typescript
156
+ // e2e/projects/aisdk-transformer/agents/aisdk-transformer.ts(示意)
157
+ import { defineAgent } from "niceeval";
158
+ import { fromAiSdk } from "niceeval/agents/ai-sdk";
159
+ import { createOpenAI } from "@ai-sdk/openai";
160
+ import { generateText, tool } from "ai";
161
+ import { z } from "zod";
162
+
163
+ const deepseek = createOpenAI({
164
+ apiKey: process.env.DEEPSEEK_API_KEY,
165
+ baseURL: "https://api.deepseek.com",
166
+ });
167
+
168
+ export default defineAgent({
169
+ name: "aisdk-transformer",
170
+ async send(ctx) {
171
+ const result = await generateText({
172
+ model: deepseek.chat("deepseek-v4-flash"),
173
+ messages: ctx.messages,
174
+ tools: {
175
+ get_weather: tool({
176
+ inputSchema: z.object({ city: z.string() }),
177
+ execute: async ({ city }) => ({ city, tempC: 25, condition: "晴" }),
178
+ }),
179
+ },
180
+ });
181
+ return fromAiSdk(result); // 官方转换器,真实的 generateText 结果直喂
182
+ },
183
+ });
184
+ ```
153
185
 
154
- - **mock-ai-sdk**:`defineAgent` + 官方 `fromAiSdk` 转换器。server 的 `/chat` 返回贴近 AI SDK `generateText` 结果形状的 JSON(`fromAiSdk` import `ai` 包、只认形状,见 `src/agents/ai-sdk.ts`),adapter `fetch` 后用 `fromAiSdk` 转标准事件流,不手写映射。
155
- - **mock-http**:`defineAgent` + 手写映射,自定义 JSON 协议,实现参照 `examples/zh/ai-sdk/adapter/adapter.ts` 裁剪。
186
+ - **aisdk-transformer**:`defineAgent` + 官方 `fromAiSdk` 转换器,喂真实 `generateText` 结果(不是伪造成 AI SDK 形状的假数据)
187
+ - **http-mapping**:`defineAgent` + 手写映射,直接 `fetch` DeepSeek 的 chat completions REST 接口,自己把 JSON 响应映射成标准事件流,实现参照 `examples/zh/ai-sdk/adapter/adapter.ts` 裁剪。
156
188
 
157
- 两个项目复用同一份 shared 套件(profile:`usage: false, hitl: false`),分别覆盖"官方转换器"和"自写映射"两条官方支持的事件流生成方式。`runs: 100`、deliberate-fail/error、缓存行为这些框架级专项也都挂在这两个项目上,因为它们最便宜、最确定。
189
+ 两个项目复用同一份 shared 套件(profile:`usage: false, hitl: false`),分别覆盖"官方转换器"和"自写映射"两条官方支持的事件流生成方式,都是对真实 DeepSeek 端点的真实调用——省的是"起被测服务器进程"和"更贵的模型"这两项成本,不是省"真实推理"这一项。`runs: N` 统计、deliberate-fail/error、缓存行为这些框架级专项都挂在这两个项目上,因为它们最便宜、启动最快。
158
190
 
159
- deliberate-fail / deliberate-error 这类"故意红"的 fixture 只进 `verdicts` 实验(不进 `ci`),由 verify.mjs 以"期望 exit 1"的方式正向消费——这是第 1 节"正反验证"在退出码层的那一半。
191
+ deliberate-fail / deliberate-error 这类"故意红"的 fixture 设计成**对模型输出不敏感**——比如断言一个真实压根不会被调用的工具名(`t.calledTool("this_tool_does_not_exist")`),或者给 adapter 喂一个格式错误的输入触发运行时异常——不管真实模型这次答得好不好,这两条断言都必然会挂,不依赖任何伪造的坏响应。只进 `verdicts` 实验(不进 `ci`),由 verify.mjs 以"期望 exit 1"的方式正向消费——这是第 1 节"正反验证"在退出码层的那一半。
160
192
 
161
- ## 4. 三层执行模型
193
+ ## 4. 两层执行模型
162
194
 
163
- "需要什么秘密、花多少钱、多久跑一次"分三层:
195
+ 不再按"要不要秘密"分层(e2e 全程都要真实 key),而是按"多久跑一次、花多少钱、跑多大的模型/样本量"分两层:
164
196
 
165
- | 层 | 触发 | 秘密/外部依赖 | 跑什么 |
197
+ | 层 | 触发 | 模型档位 & runs | 跑什么 |
166
198
  |---|---|---|---|
167
- | **L0 框架 + 适配矩阵**(mock) | 每个 PR + push main | | `e2e/projects/*` 全矩阵:mock-ai-sdk / mock-http 必跑;apps mock 已落地的 SDK 项目逐个纳入。共享套件 100% pass + verdicts 期望 exit 1 + `runs: 100` 统计 + 缓存行为 |
168
- | **L1 示例冒烟**(mock) | 每个 PR + push main | | `examples/zh/ai-sdk` `AGENT_MODE=mock` 下真的能 `niceeval exp` 跑绿——保证公开示例不烂 |
169
- | **L2 真实层** | nightly cron + 手动 dispatch | 各家 API key;Docker | e2e/projects 切真模型跑同一份套件(验证 mock 没有把真实协议测歪)+ 沙箱矩阵 + judge 真产分 |
199
+ | **L0 PR 门禁**(真实 key,便宜模型) | 每个 PR + push main | `deepseek-v4-flash` 等便宜模型;`runs: 3, earlyExit: true`(第一次过就收尾,吸收单次抖动) | `e2e/projects/*` 全矩阵:aisdk-transformer / http-mapping 必跑;apps 里已接入的 SDK 项目逐个纳入。共享套件通过 + verdicts 期望 exit 1 + `runs` 统计(小样本) + 缓存行为 |
200
+ | **L1 真实矩阵**(nightly) | schedule cron + 手动 dispatch | 完整官方模型矩阵(gpt-4o、gpt-5.4 等) + `runs: 20~100`,统计更稳的通过率分布 | 同一份共享套件切到完整模型矩阵 + judge 真产分(配 judge key)+ 沙箱矩阵 |
170
201
 
171
- L0/L1 是合并门禁;L2 是每日健康信号(真模型有随机性,不阻塞 PR)。注意 L0 里"CI 起 app"不违反 tier1 的"eval 不代管进程"原则——起应用的是 CI workflow(扮演用户),不是 eval 框架。
202
+ L0 是合并门禁;L1 是每日健康信号(模型矩阵更贵、样本更大,允许波动,不阻塞 PR)。注意 L0 里"CI 起 app"不违反 tier1 的"eval 不代管进程"原则——起应用的是 CI workflow(扮演用户),不是 eval 框架。`examples/zh/ai-sdk` 的示例冒烟(原第 6 节的"L1")并入 L0 的 PR 门禁,作为独立 job 跑,详见第 6 节。
172
203
 
173
- ### 4.1 L0 的 100% pass 语义
204
+ ### 4.1 L0 的通过语义:确定性部分精确断言,模型相关部分容忍抖动
174
205
 
175
- mock 是确定性的,所以期望值是精确的,verify.mjs 对每个项目断言:
206
+ verify.mjs 对每个项目区分两类断言:
176
207
 
177
- - `ci` 实验:exit 0,且 `passed === 期望 eval × runs, failed === 0, errored === 0`;期望 eval 数由该项目 profile 推出(对账防少排,见 3.1)。
178
- - `verdicts` 实验(仅 mock-ai-sdk / mock-http):exit 1,failed errored 计数符合期望。
179
- - `pass-100` 实验(仅 mock-ai-sdk):`runs: 100, earlyExit: false, maxConcurrency: 16`,断言 `passed === 200`(正反两条 eval × 100)。任何一次计数不对都说明调度/评分/汇总有回归。
208
+ - **确定性部分**(和真实模型答得好不好无关):discover 出的 eval 数量按 profile 精确对账(防止少排用例);`verdicts` 实验里 deliberate-fail/error 的失败/报错次数是精确断言,因为这些 fixture 本来就设计成对模型输出不敏感。
209
+ - **模型相关部分**(工具是否调对、judge 打分):允许有限重试吸收单次抖动——`runs: 3, earlyExit: true`,一次通过就算过。断言口径从"exact count"改成"通过率下限",例如 `ci` 实验断言 `passed >= 期望 eval × 0.95`(留一点余量给真实模型的单次波动;如果某个 eval 连续 3 次都没过,说明是适配层真回归,不是抖动,矩阵照样打红)。
210
+
211
+ ```javascript
212
+ // e2e/scripts/verify.mjs(示意,节选)
213
+ const PROJECTS = [
214
+ { dir: "projects/aisdk-transformer", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("aisdk-transformer")) },
215
+ { dir: "projects/aisdk-transformer", exp: "verdicts", expectExit: 1, expect: { failedAtLeast: 1, erroredAtLeast: 1 } },
216
+ { dir: "projects/aisdk-transformer", exp: "pass-n", expectExit: 0, expect: { passedAtLeast: 18 /* 2 条 eval × 10 runs,留 10% 余量 */ } },
217
+ { dir: "projects/http-mapping", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("http-mapping")) },
218
+ { dir: "projects/ai-sdk-v7", exp: "ci", expectExit: 0, expect: toleratedCounts(profileOf("ai-sdk-v7")) },
219
+ // ... 其余已接入的 SDK 项目
220
+ ];
221
+ ```
180
222
 
181
- ### 4.2 L2:真实适配层(nightly)
223
+ ### 4.2 L1:nightly 真实矩阵 + 沙箱
182
224
 
183
- 两块内容。第一块:e2e/projects 里已接真模型开关的项目,用**同一份共享套件**跑 `--runs 2`,加 judge key 顺带验证 judge 断言真的产出分数而非 no-op;真模型允许波动,失败发通知(GitHub issue / 通知渠道)而不是标红 main。
225
+ 两块内容。第一块:e2e/projects 里的共享套件切到**完整官方模型矩阵**,`--runs 20` 起步,加 judge key 顺带验证 judge 断言真的产出分数而非 no-op;更大的模型矩阵和样本量本来就更贵更慢,放 nightly 而不是 PR 门禁,失败发通知(GitHub issue / 通知渠道)而不是标红 main。
184
226
 
185
227
  第二块:沙箱矩阵,按"官方适配器 × 沙箱后端"取最小生成集,不做全叉乘:
186
228
 
@@ -196,54 +238,34 @@ mock 是确定性的,所以期望值是精确的,verify.mjs 对每个项目断
196
238
 
197
239
  已知约束(来自项目 memory,落地时直接采纳):e2b 的 `base` 模板 node20/481MB 会 OOM,必须用预制的 `fasteval-agents` 模板;vercel 后端默认并发 1(避免 429),session 上限约 360s;沙箱内不要 hardcode `/home/node`。
198
240
 
199
- L2 每个 experiment 都设 `budget`(建议单 job ≤ $2)和 `timeoutMs`,workflow 层再加 job timeout 兜底。
241
+ L1 每个 experiment 都设 `budget`(建议单 job ≤ $2)和 `timeoutMs`,workflow 层再加 job timeout 兜底——PR 门禁(L0)因为跑得频繁,单个 job 的 `budget` 应该卡得更紧(建议 ≤ $0.5),便宜模型 + 小 runs 是控制单次 PR 成本的主要手段。
200
242
 
201
243
  ## 5. 元校验脚本(e2e 的"真正的测试")
202
244
 
203
- L0 的本体不是那些 eval——eval 是 fixture;本体是 `e2e/scripts/verify.mjs`:它把 CLI 当黑盒子进程跑,对照期望表校验退出码和 `summary.json`:
245
+ L0 的本体不是那些 eval——eval 是 fixture;本体是 `e2e/scripts/verify.mjs`:它把 CLI 当黑盒子进程跑,对照期望表校验退出码和 `summary.json`(完整示意见 4.1)。verify.mjs 还负责两个专项:
204
246
 
205
- ```javascript
206
- // e2e/scripts/verify.mjs(示意)
207
- import { execFileSync } from "node:child_process";
208
- import { readFileSync } from "node:fs";
209
-
210
- const PROJECTS = [
211
- { dir: "projects/mock-ai-sdk", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("mock-ai-sdk")) },
212
- { dir: "projects/mock-ai-sdk", exp: "verdicts", expectExit: 1, expect: { failedAtLeast: 1, erroredAtLeast: 1 } },
213
- { dir: "projects/mock-ai-sdk", exp: "pass-100", expectExit: 0, expect: { passed: 200, failed: 0, errored: 0 } },
214
- { dir: "projects/mock-http", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("mock-http")) },
215
- { dir: "projects/ai-sdk-v7", exp: "ci", expectExit: 0, expect: exactCounts(profileOf("ai-sdk-v7")) },
216
- // ... 其余已接 mock 的 SDK 项目
217
- ];
218
-
219
- for (const c of PROJECTS) {
220
- const code = runNiceeval(c.dir, ["exp", c.exp, "--force", "--junit", `junit-${c.exp}.xml`]);
221
- assert.equal(code, c.expectExit, `${c.dir}/${c.exp} 退出码`);
222
- const summary = JSON.parse(readFileSync(latestRunDir(c.dir) + "/summary.json", "utf8"));
223
- assertCounts(summary, c.expect); // exactCounts 按 profile 推期望 eval 数,对账防少排
224
- }
225
- ```
226
-
227
- verify.mjs 还负责两个专项:
247
+ - **缓存行为**:同一 experiment 连跑两次,第二次**不带** `--force` → 断言 summary 里携入的 cached 结果仍计为 passed 且没有真跑(比较 `durationMs` 或 attempt 工件时间戳,顺带省一次真实调用的钱);第二次**带** `--force` → 断言全部真跑。这把"CI 忘了 --force 会静默跳过"的坑变成被测行为。
248
+ - **工件形状**:抽查一个 attempt 目录,断言 `events.json` 是 JSON array、`summary.json` 顶层有 `format/schemaVersion/producer/passed/failed/errored/results[]`、`results[].artifactsDir` 指向存在的目录。防止 results-format 无声漂移。
228
249
 
229
- - **缓存行为**:同一 experiment 连跑两次,第二次**不带** `--force` → 断言 summary 里携入的 cached 结果仍计为 passed 且没有真跑(比较 `durationMs` 或 attempt 工件时间戳);第二次**带** `--force` → 断言全部真跑。这把"CI 忘了 --force 会静默跳过"的坑变成被测行为。
230
- - **工件形状**:抽查一个 attempt 目录,断言 `events.json` 是 JSON array、`summary.json` 顶层有 `passed/failed/errored/results[]`、`results[].artifactsDir` 指向存在的目录。防止 results-format 无声漂移(当前 writer 没有 schemaVersion,只能靠这层守)。
250
+ ## 6. L0 里的示例冒烟 job
231
251
 
232
- ## 6. L1:示例冒烟层
233
-
234
- 目的只有一个:公开示例必须能按 README 跑通。首选 `examples/zh/ai-sdk`,因为它的 server 有零 key 的 `AGENT_MODE=mock`:
252
+ 目的只有一个:公开示例必须能按 README 跑通。首选 `examples/zh/ai-sdk`——落地前必须先完成第 2 节提到的清理:把 README 里"默认是 `AGENT_MODE=mock`"那几行改成真实 key 说明,因为代码里从来就没有这个开关。清理之后,这个 job 和 L0 里其它项目一样,从 GitHub Actions secrets 注入真实 key:
235
253
 
236
254
  ```yaml
237
255
  # job 示意
238
256
  - run: pnpm install --dir examples/zh/ai-sdk
239
- - run: AGENT_MODE=mock node server & # 起 127.0.0.1:5188,等端口就绪
240
- - run: pnpm exec niceeval list # eval 发现冒烟
241
- - run: pnpm exec niceeval exp compare-models --runs 1 --force --junit junit.xml
257
+ - run: node server & # 起 127.0.0.1:5188,等端口就绪
258
+ env:
259
+ OPENAI_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
260
+ OPENAI_BASE_URL: https://api.deepseek.com
261
+ AGENT_MODEL: deepseek-v4-flash
262
+ - run: pnpm exec niceeval list # eval 发现冒烟
263
+ - run: pnpm exec niceeval exp compare-models --runs 3 --force --junit junit.xml
242
264
  ```
243
265
 
244
- 验收前提(落地时先确认,不满足就先修示例的 mock):mock 模式下 `weather-tool` 等 gate 断言必须可过——即 mock server 对天气提问要真的产生 `get_weather` 工具调用事件。judge 断言无 key 自动跳过,正好符合本层"零秘密"的定位。
266
+ 验收前提(落地时先确认,不满足就先修示例本身,而不是加 mock 绕过去):`weather-tool` 等 gate 断言必须能在真实 DeepSeek 调用下稳定通过——即真实模型对天气提问确实会触发 `get_weather` 工具调用。judge 断言没配 judge key 时自动跳过,是否额外配 judge key 走完整评分是 nightly(4.2)的事,不是这个冒烟 job 的职责。
245
267
 
246
- 注意 L1L0 的分工:L0 测的是框架和适配层(fixture 允许为可测性改造);L1 测的是"用户照 README 操作会不会翻车"(fixture 一个字都不许为 CI 改)。`examples/zh/tier1/*` 不进 L1——它们的可测化版本已经在 `e2e/apps` 里了。
268
+ 这个 jobe2e/projects 矩阵的分工:e2e/projects 测的是框架和适配层(fixture 允许为可测性改造);这个 job 测的是"用户照 README 操作会不会翻车"(fixture 一个字都不许为 CI 改)。`examples/zh/tier1/*` 不用这个 job 覆盖——它们的可测化版本已经在 `e2e/apps` 里了。
247
269
 
248
270
  ## 7. Workflow 编排
249
271
 
@@ -253,43 +275,58 @@ verify.mjs 还负责两个专项:
253
275
  on:
254
276
  push: { branches: [main] }
255
277
  pull_request:
256
- schedule: [{ cron: "0 3 * * *" }] # L2 nightly
278
+ schedule: [{ cron: "0 3 * * *" }] # L1 nightly
257
279
  workflow_dispatch:
258
280
 
259
281
  jobs:
260
282
  e2e-matrix: # L0,PR 门禁
261
283
  runs-on: ubuntu-latest
262
- steps: [checkout, pnpm install, 起 mock server 与已接 mock 的 apps, node e2e/scripts/verify.mjs]
284
+ env:
285
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
286
+ CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
287
+ steps: [checkout, pnpm install, 起已接入的 apps, node e2e/scripts/verify.mjs]
263
288
 
264
- e2e-examples: # L1,PR 门禁
289
+ e2e-examples: # 第 6 节的示例冒烟,PR 门禁
265
290
  runs-on: ubuntu-latest
291
+ env:
292
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
266
293
  # 见第 6 节
267
294
 
268
- e2e-real: # L2,仅 schedule / dispatch
295
+ e2e-nightly: # L1,仅 schedule / dispatch
269
296
  if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
270
- strategy: { fail-fast: false, matrix: { target: [claude-docker, codex-docker, bub-docker, claude-e2b, claude-vercel, sdk-projects-real] } }
297
+ strategy: { fail-fast: false, matrix: { target: [claude-docker, codex-docker, bub-docker, claude-e2b, claude-vercel, sdk-projects-full-matrix] } }
271
298
  runs-on: ubuntu-latest # ubuntu runner 自带 Docker
299
+ env:
300
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
301
+ CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
302
+ BUB_API_KEY: ${{ secrets.BUB_API_KEY }}
303
+ E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
304
+ NICEEVAL_JUDGE_KEY: ${{ secrets.NICEEVAL_JUDGE_KEY }}
272
305
  ```
273
306
 
307
+ 需要建的 GitHub repo secrets:`DEEPSEEK_API_KEY`(claude-sdk / pi-sdk / langgraph / ai-sdk-v7 / examples/zh/ai-sdk 共用,分饰 Anthropic 兼容与 OpenAI 兼容两个 base URL,同一把 key)、`CODEX_API_KEY`(codex-sdk & codex 沙箱)、`ANTHROPIC_API_KEY`(claude-code 沙箱)、`BUB_API_KEY`、`E2B_API_KEY`、Vercel token、`NICEEVAL_JUDGE_KEY`(nightly judge)。值可以复用 `examples/zh/origin` 已经在用的那套凭据(见 `memory/origin-examples-real-ai-credentials.md`),不需要重新申请。
308
+
274
309
  所有 job 统一约定:
275
310
 
276
311
  - 每次调用 `niceeval exp` 都带 `--force`(禁指纹缓存)+ `--junit <path>`;junit 和整个 `.niceeval/` 目录用 `actions/upload-artifact` 上传,失败时可下载 `events.json` / `trace.json` 排查。
277
- - 成败判据 = 进程退出码;verify.mjs 层再做计数级校验。
278
- - L0/L1 不配置任何 API key secret——judge 断言 no-op 是预期行为,一旦某个 mock job 意外需要 key,说明 fixture 写错了。
312
+ - 成败判据 = 进程退出码;verify.mjs 层再做计数级校验(L0 用容忍区间,见 4.1)。
313
+ - 只有 pull_request 触发的 workflow 才能读到 repo secrets(同仓库 PR;fork 来的 PR 默认拿不到 secrets,这是 GitHub 本身的安全模型,不是本设计要解决的问题)——如果未来要支持 fork PR 跑 e2e,需要额外做 `pull_request_target` + 手动审核这类隔离,不在本提案范围内。
279
314
 
280
315
  ## 8. 分阶段落地
281
316
 
282
- 1. **P1(先立骨架)**:建 `e2e/shared` + `e2e/projects/{mock-ai-sdk,mock-http}` + verify.mjs(共享套件、正反 eval、deliberate-fail/error、pass-100),加 `e2e-matrix` job PR 门禁。这一步零外部依赖,收益最大——完整路径、正反断言、100 次统计、退出码、缓存行为全部落网,factory/profile 机制也在最便宜的项目上定型。
283
- 2. **P2**:拷 `examples/zh/tier1/ai-sdk-v7` `e2e/apps`,加 mock 模型分支,`e2e/projects/ai-sdk-v7` 接入共享套件——第一个真实 SDK 协议栈进 L0 矩阵。同步确认/修好 `examples/zh/ai-sdk` mock 可过 gate 断言,加 `e2e-examples` job
284
- 3. **P3**:langgraph、pi-sdk 逐个验证 mock 注入点并进矩阵;claude-sdk / codex-sdk base-url mock 可行性调研,结论记 `memory/`,不可行的留 L2。给 `sync-tiers.mjs` 加 tier1 → e2e/apps 的 drift check
285
- 4. **P4**:补 `sandbox-smoke` factory;开 L2 的 claude-code × docker(第一条真实沙箱链路),随后铺满 L2 矩阵(codex / bub / e2b / vercel + sdk-projects 真模型 + judge)
286
- 5. **之后**:给 mock 加"确定性失败注入"(如 server 每第 4 次请求返回坏答案)以校验非 0/100 pass 率计算;`e2e/projects/{codex-sdk,langgraph}` OTel 瀑布图链路(`spanMapper` / 固定端口)补一条"trace.json 非空"的冒烟断言,纳入 L2
317
+ 1. **P0(先修文档债)**:把 `examples/zh/ai-sdk/README.md` 里过期的 `AGENT_MODE=mock` 声明改成真实 key 说明,确认 `weather-tool` gate 断言在真实 DeepSeek 调用下能稳定通过——这是第 6 节能落地的前提,和 e2e 骨架搭建可以并行。
318
+ 2. **P1(先立骨架)**:建 `e2e/shared` + `e2e/projects/{aisdk-transformer,http-mapping}` + verify.mjs(共享套件、正反 eval、deliberate-fail/error、`runs: N` 统计),申请 `DEEPSEEK_API_KEY` secret,加 `e2e-matrix` job 进 PR 门禁。这一步依赖最小、成本最低——完整路径、正反断言、重复运行统计、退出码、缓存行为全部落网,factory/profile 机制也在最便宜的项目上定型。
319
+ 3. **P2**:拷 `examples/zh/tier1/ai-sdk-v7` `e2e/apps`,`e2e/projects/ai-sdk-v7` 接入共享套件、复用 P0 已修好的 secrets——第一个真实 SDK 协议栈进 L0 矩阵,加 `e2e-examples` job
320
+ 4. **P3**:langgraph、pi-sdk、claude-sdk、codex-sdk 逐个接入 `e2e/apps` 并纳入矩阵(凭据映射直接照抄 tier1,不需要额外调研)。给 `sync-tiers.mjs` tier1 e2e/apps drift check
321
+ 5. **P4**:补 `sandbox-smoke` factory;开 L1 nightly claude-code × docker(第一条真实沙箱链路),随后铺满 L1 矩阵(codex / bub / e2b / vercel + sdk-projects 完整模型矩阵 + judge)。
322
+ 6. **之后**:`e2e/projects/{codex-sdk,langgraph}` 的 OTel 瀑布图链路(`spanMapper` / 固定端口)补一条"trace.json 非空"的冒烟断言,纳入 L1;评估 L0 的 retry/容忍区间参数(3.1 提到的 `runs: 3`、95% 阈值)是否需要按项目调整——不同 SDK 的真实抖动率可能不一样。
287
323
 
288
324
  ## 9. 明确不做的
289
325
 
290
326
  - 不做字面共享:不把同一份 `.eval.ts` 软链/复制进多个项目——discover 不跟随 symlink 条目,且各 SDK 的工具名和能力差异是协议现实,抹平它们等于测一个不存在的协议。共享的单位是 factory,差异收敛进 profile。
291
- - 不和 tier1 共享 eval:`examples/zh/tier1` 的 eval 继续服务文档叙事,e2e 的套件独立编写、独立演进,两边不建同步关系;从 tier1 拷的只有应用和 adapter(受 drift check 约束),也不要求公开示例携带 CI 专用的 mock 开关(`examples/zh/ai-sdk` 本来就有的除外)
327
+ - 不和 tier1 共享 eval:`examples/zh/tier1` 的 eval 继续服务文档叙事,e2e 的套件独立编写、独立演进,两边不建同步关系;从 tier1 拷的只有应用和 adapter(受 drift check 约束)。
328
+ - 不用任何形式的假 AI/本地 mock server:e2e 全程调真实模型,费用通过便宜模型档位、小 `runs`、per-experiment `budget` 护栏控制,不通过伪造响应控制;PR 门禁和 nightly 的区别是"模型档位、样本量、跑多勤",不是"真的假的"。
292
329
  - 不测"OTel 派生事件"这类断言——OTel 只喂瀑布图,e2e 要测的是它产出了 `trace.json`,不是它替代了事件映射。
293
- - 不在 PR 门禁里跑任何真模型——随机性 + 费用 + secret 暴露面都不适合。
294
330
  - 不用 `--tag` 做 CI 内的用例切分(当前实现只收单值,与文档不一致;分层用 experiment 的 `evals` 过滤器表达,更明确)。
295
- - 不依赖 `summary.json` 里的版本字段(writer 尚未写 `schemaVersion`),形状校验只断言当前实际字段。
331
+ - 不支持 fork 来的 PR 跑需要 secrets 的 e2e job(GitHub 默认不把 secrets 传给 fork PR;需要更严格的隔离方案的话另开提案)
332
+ - 版本字段只做格式契约校验,不把具体 `producer.version` 写死到测试里;版本不兼容的读取行为由 view loader 自己测。
@@ -51,7 +51,7 @@ export default rows.map((row) =>
51
51
 
52
52
  - `t.*` 保留"聚合整个 eval run"的语义——这次 eval 执行的全部轮次、含 `t.newSession()` 开的额外 session,直接对应 eve 的 `timing: "final"` 层。这一层聚合是有意为之,不是要移除的"黑箱"。
53
53
  - `session.*`(`t.newSession()` 的返回值)复用 `t.*` 的同一套**作用域断言词汇**,但只看这个 session 在断言记录时已有的事件。
54
- - `turn.*`(`t.send()` 的返回值)也复用同一套**作用域断言词汇**,但只看这一轮自己的事件和用量,不再是旧版文档里的 4 个手写方法。`turn.expectOk` / `turn.outputEquals` / `turn.outputMatches` 是 turn 独有的(只对单轮结果有意义,聚合层不需要),继续保留。
54
+ - `turn.*`(`t.send()` 的返回值)也复用同一套**作用域断言词汇**,但只看这一轮自己的事件和用量,不再是旧版文档里的 4 个手写方法。`turn.outputEquals` / `turn.outputMatches` 是 turn 独有的(只对单轮结果有意义,聚合层不需要),继续保留。
55
55
 
56
56
  也就是:**接收者决定作用域,不是断言名字决定作用域。** author-facing 接收者是 `t` / `session` / `turn`;`Attempt` 只作为 runner/result 里的执行单位存在,不是写 eval 时要操作的一层。完整清单见 [Assertions · 作用域规则](assertions.md#作用域规则)。
57
57
 
@@ -96,7 +96,7 @@ export default defineEval({
96
96
  });
97
97
  ```
98
98
 
99
- `t.reply` 是最后一条 assistant 消息;`t.sessionId` 是当前主会话 id;`t.events` 是主 session 目前捕获到的强类型事件流。`t.send(input)` 接受字符串或结构化消息,返回一个不可变的 **Turn**,带 `message` / `data`(结构化输出)/ `toolCalls` / `status` / `events` / `expectOk()`。带本地文件的一轮用 `t.sendFile(path, text?)`,文件会作为 data URL 附加到这一轮输入里,MIME 类型按 `path` 扩展名推断。
99
+ `t.reply` 是最后一条 assistant 消息;`t.sessionId` 是当前主会话 id;`t.events` 是主 session 目前捕获到的强类型事件流。`t.send(input)` 接受字符串或结构化消息,返回一个不可变的 **Turn**,带 `message` / `data`(结构化输出)/ `toolCalls` / `status` / `events`。带本地文件的一轮用 `t.sendFile(path, text?)`,文件会作为 data URL 附加到这一轮输入里,MIME 类型按 `path` 扩展名推断。
100
100
 
101
101
  ## 多轮
102
102
 
@@ -111,7 +111,7 @@ export default defineEval({
111
111
  description: "先拟稿,确认后再发送",
112
112
  async test(t) {
113
113
  const draft = await t.send("帮我拟一封跟进邮件。");
114
- draft.expectOk(); // 上一轮若失败,这里抛
114
+ draft.succeeded(); // 只看这一轮:失败就记一条断言
115
115
  t.check(draft.message, includes("此致"));
116
116
  draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);
117
117
 
@@ -25,6 +25,7 @@ export default defineExperiment({
25
25
  description?: string; // 人读
26
26
  agent: Agent; // 跑哪个 agent(adapter 实例)
27
27
  model?: string; // 单个模型(agent 留空);省略=原生默认。跨模型对比写多个实验文件
28
+ reasoningEffort?: string; // 推理努力程度(agent 留空);省略=原生默认。经 ctx.reasoningEffort / t.reasoningEffort 透传
28
29
  flags?: Record<string, unknown>; // feature flags,透传到 ctx.flags / t.flags(见下)
29
30
  runs?: number; // 每个 (agent × model × eval) 跑几次(默认 1)
30
31
  earlyExit?: boolean; // 先过一次即停其余(默认 true)
@@ -35,22 +36,24 @@ export default defineExperiment({
35
36
  });
36
37
  ```
37
38
 
38
- > experiment 只有"运行矩阵"字段,**没有环境起停钩子**。要在跑 agent 前准备环境,放 `test(t)`(沙箱预置)、`SandboxAgent.setup`(连 agent / 装 CLI)或外部编排(整轮共享服务),见 [环境预置放哪](sandbox.md#环境预置放哪)。
39
+ > experiment 只有"运行矩阵"字段,**没有 run / experiment 级环境起停钩子**。要在跑 agent 前准备环境,放 `EvalDef.setup` / `test(t)`(单个 eval 的沙箱预置)、`SandboxAgent.setup`(连 agent / 装 CLI)或外部编排(整轮共享服务),见 [环境预置放哪](sandbox.md#环境预置放哪)。
39
40
 
40
41
  id 从**路径**推导:`experiments/compare/bub-gpt-5.4.ts` → `compare/bub-gpt-5.4`(路径即身份,和 eval 一致,禁止手写 id);其中目录段 `compare/` 就是"可对比组",见 [下节](#实验怎么组织文件夹--一组可对比的实验)。
41
42
 
42
- ### model 与 flags:agent 留空,实验决定
43
+ ### model / reasoningEffort 与 flags:agent 留空,实验决定
43
44
 
44
- agent 定义里**不写死模型、不写死开关**(那样就锁死了复用)。这两样由实验给,经 `ctx` 透传:
45
+ agent 定义里**不写死模型、不写死开关**(那样就锁死了复用)。这几样由实验给,经 `ctx` 透传:
45
46
 
46
47
  - **`model`** —— 单个模型字符串,agent 的 `send` 从 `ctx.model` 拿;省略则不传 `--model`,用 agent CLI 的原生默认。**跨模型对比写多个实验文件**(各钉一个 model),别在一个实验里塞数组。
47
- - **`flags`** —— 任意 KV feature flags,**两处可见**:agent 的 `send`(`ctx.flags`)、eval 的 `test`(`t.flags`)。用来开关联网、注入某个 skill、调 effort、或让某条 eval 只在某 flag 下断言。
48
+ - **`reasoningEffort`** —— 单个推理努力程度字符串(取值由具体模型定义,如 `"low"`/`"medium"`/`"high"`),归属与 `model` 完全一致:agent 的 `send``ctx.reasoningEffort` 拿,eval 的 `test``t.reasoningEffort` 拿;省略则用 agent 原生默认。跨档位对比同样写多个实验文件。
49
+ - **`flags`** —— 任意 KV 的 feature flags,**两处可见**:agent 的 `send`(`ctx.flags`)、eval 的 `test`(`t.flags`)。用来开关联网、注入某个 skill、或让某条 eval 只在某 flag 下断言。
48
50
 
49
51
  ```typescript
50
52
  // experiments/research-mode.experiment.ts
51
53
  export default defineExperiment({
52
54
  agent: codexAgent(),
53
55
  model: "opus", // 模型在实验给,agent 留空
56
+ reasoningEffort: "high", // → ctx.reasoningEffort / t.reasoningEffort
54
57
  flags: { webResearch: true, skill: "memory-v2" }, // → ctx.flags / t.flags
55
58
  runs: 3,
56
59
  });
@@ -136,7 +136,7 @@ otel 在 agent 定义里其实是**两个互不相干的责任,分开放**,别
136
136
 
137
137
  **为什么要分:** 导出配置是「沙箱里怎么发」,mapper 是「发回来怎么读」—— 一个需要沙箱、一个是纯函数,生命周期和测试方式都不同。混在 `setup`/`send` 里,既难单测 mapper、又让 adapter 把 otel 拼装逻辑揉进主流程。`ctx.telemetry` 则统一带上 `{ endpoint, env? }`:env-based agent 拿 `env` 直接 spread,file-based agent 在 `configure` 里用 `endpoint`。
138
138
 
139
- > **claude-code 缺口:** 它根本不发 OTLP(没有 `tracing` 块)。但它的 transcript JSONL 带时间戳,可从 `StreamEvent` + 时间戳**合成 span**(synthetic spans),再走同一个 canonical schema —— 让三个 agent 都有瀑布图、对比表不缺一角。合成器同样住 core o11y,与 mapper 并列。
139
+ > **claude-code:** 已接原生 OTLP(beta 遥测):adapter 的 `tracing.env` 注入 `CLAUDE_CODE_ENABLE_TELEMETRY=1` + `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1` + `OTEL_TRACES_*`( `src/agents/claude-code.ts`)。span 层级 `claude_code.interaction llm_request / tool`,只有结构与计时、内容默认脱敏——内容与断言仍来自 transcript 旁读,trace 只补瀑布图。span 量级小(每回合个位数),`selectTraceSpans` 的 small-trace 路径整段保留,不需要专属 mapper。曾考虑过的「transcript 时间戳合成 span」方案不再需要。
140
140
 
141
141
  ### span 怎么归属到轮
142
142
 
@@ -302,18 +302,22 @@ interface Reporter {
302
302
  - **`Artifacts()`** —— 默认写 `.niceeval/<timestamp>/summary.json` 与 attempt 级 JSON 工件(`events.json`、`sources.json`、`trace.json`、`o11y.json`、`diff.json`),供 `niceeval view` 读取。具体格式见 [Results Format](results-format.md)。
303
303
  - **`JUnit(path)`** —— JUnit XML,接 CI 测试报告 UI。
304
304
  - **`Json(path)`** —— 机器可读全量。
305
- - **第三方实验跟踪** —— Braintrust 这类平台,把每次运行作为一个实验上报,跨提交比较。
305
+ - **`Braintrust(config?)`** —— 把一次运行作为一个 Braintrust experiment 上报,每个 attempt 一行:soft 断言按名字记分,gate 断言记在 `gate:` 前缀下(实验 diff 里 gate 回归和 soft 分数回归用同一套机制看);metrics 带 start/end、token 用量与估算成本,metadata 带 agent / model / experiment / flags 身份维度与失败断言明细。`braintrust` 包是可选 peer 依赖(动态 import,没装时 onRunStart 报错并提示安装);鉴权走 `BRAINTRUST_API_KEY` 或工厂参数 `apiKey`。源码 `src/runner/reporters/braintrust.ts`。
306
306
 
307
307
  配置全局或单 eval 专用:
308
308
 
309
309
  ```typescript
310
- // niceeval.config.ts —— 全局,观测所有 eval
311
- defineConfig({ reporters: [Console(), JUnit(".niceeval/junit.xml")] });
310
+ import { Braintrust, JUnit } from "niceeval/reporters";
312
311
 
313
- // 某个 eval 专用
312
+ // niceeval.config.ts —— 全局,观测所有 eval(Console / Artifacts 由 CLI 始终自带,不用写)
313
+ defineConfig({ reporters: [JUnit(".niceeval/junit.xml"), Braintrust({ project: "weather" })] });
314
+
315
+ // 某个 eval 专用:实例只观测引用它的 eval
314
316
  defineEval({ reporters: [Braintrust({ project: "weather" })], async test(t) { ... } });
315
317
  ```
316
318
 
319
+ eval 级 reporter 经作用域包装接入(`scopeReporter`,见 `src/runner/report.ts`):`onEvalComplete` 按 eval id 过滤,`onRunComplete` 收到重新计数的子集汇总;同一实例被多个 eval 引用时合并观测集(共享一个目的地,比如同一个 Braintrust 实验),已经挂在全局 `reporters` 里的实例在 eval 上再列一遍也不会重复上报。
320
+
317
321
  ## 失败分类(可选,沙箱型)
318
322
 
319
323
  沙箱型可开 AI 失败分类:跑完用一个小模型(给它只读探索结果目录的工具:list/read/grep)把失败归为三类:
@@ -1,18 +1,18 @@
1
1
  # Origin 应用接入手册
2
2
 
3
- 这份文档最初是**工单**:`examples/zh/origin/` 下五个独立应用,按应用逐个说明怎么把它们接进 niceeval。**五个应用已经全部接完**(见文末「现状」),产出在 `examples/zh/tier1/<name>/`。本文现在的作用是记录接入时踩过的坑和当前实现的形状,再接一个类似的应用时可以照抄;当前实现的权威来源永远是 `examples/zh/tier1/<name>/` 的源码和各自的 README,本文只是导读。
3
+ 这份文档最初是**工单**:`examples/zh/origin/` 下五个独立应用,按应用逐个说明怎么把它们接进 niceeval。**五个应用已经全部接完**(见文末「现状」),产出按分档铺在 `examples/zh/tier1|tier2|tier3/<name>/`(分层索引见 [examples/zh · 接入分层](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3))。本文现在的作用是记录接入时踩过的坑和当前实现的形状,再接一个类似的应用时可以照抄;当前实现的权威来源永远是各 tier 目录的源码和各自的 README,本文只是导读。
4
4
 
5
5
  先记住三条铁律(接类似应用时依然适用):
6
6
 
7
- 1. **不改 origin 的任何文件。** 接入产物放 `examples/zh/tier1/<同名>/`:从 origin 复制整个应用,被复制的文件保持逐字节不变(除 `package.json` / `pnpm-workspace.yaml` / `tsconfig.json` 三个集成脚手架文件),接入代码全部是**新增**文件。`pnpm run gen:diff-code` 会 diff origin 和 tier1 两个目录生成 before/after 文档页,"应用侧零改动"是这些页面的核心卖点,改一个字节都会破坏它。
7
+ 1. **不改 origin 的任何文件。** 接入产物放 `examples/zh/tier1/<同名>/`:从 origin 复制整个应用,被复制的文件保持逐字节不变(除 `package.json` / `pnpm-workspace.yaml` / `tsconfig.json` 三个集成脚手架文件,以及 `.env.example`——tier 侧要补 judge 独立凭证等 eval 变量),接入代码全部是**新增**文件。`pnpm run gen:diff-code` 会 diff origin 和 tier1 两个目录生成 before/after 文档页,"应用侧零改动"是这些页面的核心卖点,改一个字节都会破坏它;这条铁律由 CI 的 `pnpm tiers:check`(verbatim 校验)看守,见 [tier-sync](tier-sync.md)。
8
8
  2. **协议以实际输出为准。** 动手写映射之前,先把应用跑起来,`curl -N` 打一轮 `/api/chat` 把 SSE 帧看一遍。本文的帧格式描述来自当前代码,但代码会演化,别背文档。
9
9
  3. **被测应用由你自己按它的方式启动,eval 不代管进程、不另开端口。** adapter 只经环境变量(如 `CODEX_SDK_URL`)指向一个已经在跑的实例——没有 `server-lifecycle.ts` 这类"eval 侧拉起子进程"的机制,这是刻意的取舍,理由见[接入你的 Agent · 为什么不直调](../docs-site/zh/guides/connect-your-agent.mdx)同一条脉络(eval 不代管被测进程)。
10
10
 
11
- ## Tier 是什么,这次做到哪一档
11
+ ## Tier 是什么,产出长什么样
12
12
 
13
13
  接入分三档(定义见 [docs-site · Tier](../docs-site/zh/concepts/tier.mdx)):**Tier 1(只接 send)**、**Tier 2(send + OTel)**、**Tier 3(侵入改造 + experiment flags)**。
14
14
 
15
- **本工单做的是 Tier 1 + Tier 2:** 应用代码一行不改,adapter 适配应用现有的 HTTP + SSE 接口;有 OTel 输出的应用(ai-sdk-v7、langgraph、codex-sdk)额外接了 trace 瀑布图。**Tier 3 的建议改法在文末列出,但那是单独的工作,不要顺手做。**
15
+ 三档现在各有物化目录,同一个应用逐层叠 delta:`tier1/<name>`(纯无侵入,全套断言)、`tier2/<name>`(有 OTel 输出的三个应用:ai-sdk-v7、codex-sdk、langgraph——加 telemetry 配置与 spanMapper/收尾宽限,换瀑布图)、`tier3/<name>`(五个应用都有——按文末「Tier 3 侵入点」改应用内部代码,暴露 experiment flags)。哪个应用有哪几层见 [examples/zh 分层索引](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3);层间用 `pnpm tiers:sync` 保持同步(机制见 [tier-sync](tier-sync.md))。本文余下部分讲 Tier 1 的接入配方——那是每个应用的地基。
16
16
 
17
17
  ## 统一的接入配方
18
18
 
@@ -87,11 +87,11 @@ adapter 要这样做:
87
87
 
88
88
  没有审批流的应用(codex-sdk)跳过这一整节,永远不返回 `waiting`。
89
89
 
90
- ### OTel:只画瀑布图,和事件映射完全无关
90
+ ### OTel:只画瀑布图,和事件映射完全无关(这一节的产物在 tier2)
91
91
 
92
92
  不管应用有没有 OTel 输出,事件映射(上面几节讲的)都一样要做——**OTel 现在只喂 `niceeval view` 的调用瀑布图,不产出任何事件,也不影响任何断言**,详见 [Observability · OTLP traces](observability.md#otlp-traces--统一瀑布图)。
93
93
 
94
- 五个应用里,ai-sdk-v7、codex-sdk、langgraph 发 OTel span,claude-sdk、pi-sdk 不发(claude-sdk 的 CLI 遥测只有 metrics+logs,niceeval 只消费 trace spans;pi-agent-core 没有官方 OTel 集成)。有 span 的应用接法都一样:
94
+ 五个应用里,ai-sdk-v7、codex-sdk、langgraph 发 OTel span,claude-sdk、pi-sdk 不发(claude-sdk 的 CLI 遥测只有 metrics+logs,niceeval 只消费 trace spans;pi-agent-core 没有官方 OTel 集成)。这层接线是 Tier 2 的 delta,落在 `examples/zh/tier2/<name>/`(tier1 不配 telemetry、没有瀑布图);有 span 的应用接法都一样:
95
95
 
96
96
  1. `niceeval.config.ts` 钉住固定接收端口:`defineConfig({ telemetry: { port: 4318 } })`——写了这个配置就等于给非沙箱 agent 打开了 OTel 接收。
97
97
  2. 启动应用时,把标准 OTel 环境变量指向这个端口(`OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces` 或应用自己的等价配置)。
@@ -142,7 +142,7 @@ adapter 要这样做:
142
142
 
143
143
  ### langgraph
144
144
 
145
- - 唯一的 Python 应用,也是唯一**完全手写帧映射、零 OTel 依赖用于事件**的应用:自定义 JSON 帧(`tool-input` → `action.called`、`tool-output` → `action.result`(completed)、`tool-output-denied` → `action.result`(rejected)、`text-delta` 累积成 `message`、`tool-approval-request` → `input.requested` + `waiting`)。
145
+ - 唯一的 Python 应用,也是唯一**完全手写帧映射、零 OTel 依赖用于事件**的应用:自定义 JSON 帧(`tool-input` → `action.called`、`tool-output` → `action.result`(completed,帧带 `isError: true` 时 failed)、`tool-output-denied` → `action.result`(rejected)、`text-delta` 累积成 `message`、`tool-approval-request` → `input.requested` + `waiting`)。工具异常靠 `ToolRetryMiddleware(max_retries=0, on_failure="continue")` 落成 `status="error"` 的 ToolMessage——`create_agent` 默认让工具异常炸穿整张图,没有这层就表达不了"执行了但失败"
146
146
  - HITL 标准配方,**approve 端点字段是 `toolCallId`**(别照抄 claude/pi 的 `toolUseId`)。
147
147
  - session 是 `InMemorySaver`,同 pi:应用不要中途重启。
148
148
  - OTel:LangSmith 导出的 span 只用来画瀑布图(`niceeval.config.ts` 固定端口 + 应用侧 `LANGSMITH_TRACING` / `LANGSMITH_OTEL_ENABLED` / `LANGSMITH_OTEL_ONLY` 三个环境变量),事件断言完全不依赖它。LangSmith 的 `BatchSpanProcessor` 调度和 SSE 流关闭是两条独立时间线,adapter 在轮次结束后主动等一小段(grace period)把最后一批 span 收进瀑布图。
@@ -167,21 +167,21 @@ experiment 至少两个:单配置基线 + 一个 `compare-models/` 实验组(ai-
167
167
  - [ ] 有 OTel 的:view 瀑布图非空
168
168
  - [ ] eval README 写明这个应用做不到什么(没有 OTel 的写清楚、负断言不完全靠事件完整性证明的写清楚)
169
169
 
170
- ## Tier 3 备忘(本工单不做)
170
+ ## Tier 3 侵入点(已实现,产出在 examples/zh/tier3/)
171
171
 
172
- 将来要做 feature A/B test 时,每个应用的最小侵入点(原则:加环境变量/请求字段开关,默认行为不变):
172
+ feature A/B test 已落地:每个应用挑一个最小侵入点做成**请求体可选字段**(不是环境变量——环境变量意味着换变体要重启应用,一次 experiment 运行里 A/B 不了;默认行为不变的铁律不变):
173
173
 
174
- - ai-sdk-v7:system prompt、工具集做成请求体可选字段;
175
- - claude-sdk:`allowedTools`、system prompt 提升为环境变量;
176
- - codex-sdk:`threadOptions`(sandbox mode 等)提升为环境变量;
177
- - pi-sdk:system prompt / 注册工具集提升为环境变量;
178
- - langgraph:`HumanInTheLoopMiddleware` 开关、prompt 提升为环境变量。
174
+ - ai-sdk-v7:system prompt(`instructions`)、工具子集(`tools`)→ `tier3/ai-sdk-v7`;
175
+ - claude-sdk:system prompt → `tier3/claude-sdk`;
176
+ - codex-sdk:`threadOptions`sandbox mode → `tier3/codex-sdk`;
177
+ - pi-sdk:system prompt → `tier3/pi-sdk`;
178
+ - langgraph:system prompt(变体各自编译图、共享 checkpointer)→ `tier3/langgraph`。
179
179
 
180
- experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experiments.md)
180
+ experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experiments.md);每个 tier3 目录的 README 有完整的 flags 流动链路。
181
181
 
182
- ## 现状(五个应用已全部接完)
182
+ ## 现状(五个应用已全部接完,三档分层落位)
183
183
 
184
- 本工单描述的五个应用已全部接入并实测通过,产出在 `examples/zh/tier1/<name>/`。实现比本工单最初设想的更简单,主要是两处收紧:
184
+ 本工单描述的五个应用已全部接入并实测通过,Tier 1 产出在 `examples/zh/tier1/<name>/`;OTel 接线后来按分档定义剥进 `tier2/`,侵入改造落在 `tier3/`(见上文各节)。实现比本工单最初设想的更简单,主要是两处收紧:
185
185
 
186
186
  - **没有 `server-lifecycle.ts` 这类"eval 侧拉起应用子进程"的机制。** 五个应用都由使用者自己按各自方式启动(`pnpm start` / `python server.py`),adapter 只经环境变量(`<NAME>_URL`)指向一个已经在跑的实例。这与"eval 不代管被测进程"的既定原则一致,比最初设想的自动拉起子进程更简单也更贴合真实用法。
187
187
  - **OTel 只画瀑布图,不派生任何事件。** 五个应用的事件断言全部来自 `send` 里的官方转换器或手写帧映射,与有没有接 OTel 无关。
@@ -192,4 +192,4 @@ experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experim
192
192
  - **`fromCodexThreadEvents` 补全**:ThreadEvent 的工具项(`command_execution` / `mcp_tool_call` / `file_change` → 配对的 `action.*`)与 `turn.completed` 的 usage 聚合,现在是 `tier1/codex-sdk` 事件断言的唯一数据来源(不再依赖任何 span 派生)。
193
193
  - **`mapCodexSpans`**(`src/o11y/otlp/mappers/codex.ts`,从 `niceeval/adapter` 导出):把 codex 自家 span 命名归一成 canonical GenAI 语义,只用来让瀑布图和内置 `codexAgent` 保持一致。
194
194
 
195
- 五个 `niceeval exp` 基线全绿(`ai-sdk-v7` 的多模型对比也验证过),五个 before/after 文档页已生成并挂进 `docs-site/docs.json` 导航。Tier 3(feature A/B test)仍未做,见上面「Tier 3 备忘」。
195
+ 五个 `niceeval exp` 基线在 Tier 1 阶段全绿实测过(`ai-sdk-v7` 的多模型对比也验证过),五个 before/after 文档页已生成并挂进 `docs-site/docs.json` 导航。tier3 的五组 compare-* 实验也已对着真实 key 整轮实测:四个 prompt A/B 全绿(pi-sdk 的极简变体曾让模型心算跳过工具,见 `tier3/pi-sdk/experiments/compare-prompts/concise.ts` 注释),codex-sdk compare-sandbox 按预期呈现 read-only 变体下 create-file 断言失败。这轮实测还顺带揪出并修掉了 runner 的一个丢结果 bug(`memory/runner-earlyexit-key-misses-experiment.md`)。