niceeval 0.11.0 → 0.11.1-canary.10

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 (48) hide show
  1. package/INDEX.md +1 -0
  2. package/dist/i18n/en.d.ts +1 -1
  3. package/dist/i18n/en.js +6 -5
  4. package/dist/i18n/index.d.ts +5 -0
  5. package/dist/i18n/index.js +14 -3
  6. package/dist/i18n/zh-CN.d.ts +2 -2
  7. package/dist/i18n/zh-CN.js +5 -5
  8. package/dist/runner/fingerprint.d.ts +49 -5
  9. package/dist/runner/types.d.ts +32 -4
  10. package/docs-site/zh/examples/integrations/langgraph.mdx +3 -3
  11. package/docs-site/zh/explanation/judge.mdx +7 -5
  12. package/docs-site/zh/explanation/runner.mdx +1 -1
  13. package/docs-site/zh/reference/cli.mdx +11 -17
  14. package/docs-site/zh/reference/define-config.mdx +14 -1
  15. package/docs-site/zh/troubleshooting/recover-after-kill.mdx +1 -1
  16. package/docs-site/zh/tutorials/agent-feedback-loop.mdx +11 -10
  17. package/docs-site/zh/tutorials/ci-integration.mdx +2 -3
  18. package/docs-site/zh/tutorials/configuration.mdx +165 -0
  19. package/docs-site/zh/tutorials/connect-your-agent.mdx +3 -1
  20. package/docs-site/zh/tutorials/custom-reports.mdx +30 -3
  21. package/docs-site/zh/tutorials/quickstart.mdx +1 -1
  22. package/docs-site/zh/tutorials/viewing-results.mdx +2 -2
  23. package/docs-site/zh/tutorials/write-experiment.mdx +41 -9
  24. package/package.json +1 -1
  25. package/src/agents/bub.ts +5 -8
  26. package/src/cli.ts +27 -20
  27. package/src/define.ts +6 -0
  28. package/src/i18n/en.ts +6 -5
  29. package/src/i18n/index.ts +16 -3
  30. package/src/i18n/locale.test.ts +41 -0
  31. package/src/i18n/zh-CN.ts +5 -5
  32. package/src/runner/feedback/human.test.ts +35 -6
  33. package/src/runner/feedback/human.ts +3 -1
  34. package/src/runner/feedback/io.ts +1 -1
  35. package/src/runner/feedback/reducer.test.ts +19 -0
  36. package/src/runner/feedback/reducer.ts +6 -3
  37. package/src/runner/fingerprint.test.ts +124 -0
  38. package/src/runner/fingerprint.ts +110 -11
  39. package/src/runner/run.test.ts +48 -0
  40. package/src/runner/run.ts +19 -6
  41. package/src/runner/types.ts +32 -4
  42. package/src/scoring/judge.test.ts +47 -6
  43. package/src/scoring/judge.ts +15 -22
  44. package/src/show/index.ts +1 -1
  45. package/src/show/json.test.ts +3 -5
  46. package/src/show/show.test.ts +3 -5
  47. package/src/view/data.ts +33 -2
  48. package/src/view/index.ts +1 -0
package/INDEX.md CHANGED
@@ -20,6 +20,7 @@
20
20
  - `docs-site/zh/tutorials/agent-onboarding.mdx` — Coding Agent 从零接入项目:给 Coding Agent 的完整接入流程:探索项目、与用户确认路径、配置 Judge、写出 Adapter / Experiment / 评估用例并跑通第一个实验。
21
21
  - `docs-site/zh/tutorials/authoring.mdx` — 编写评估用例: 单轮、多轮和数据集模式:用 defineEval 编写评估用例,包括单轮对话、多轮对话、数据驱动测试、Sandbox Workspace 和评估的生命周期与 Fixture。
22
22
  - `docs-site/zh/tutorials/ci-integration.mdx` — 在 GitHub Actions 和 CI 中运行 NiceEval:把 NiceEval 接入 GitHub Actions 或任意 CI。评估用例失败时非零退出,输出 JUnit XML,并通过缓存加速重复运行。
23
+ - `docs-site/zh/tutorials/configuration.mdx` — 把配置和密钥各放到该放的地方:跑几次、超时、并发、judge、语言这些值写进代码的哪一层,API key 和 provider token 用哪些环境变量。
23
24
  - `docs-site/zh/tutorials/connect-otel.mdx` — OTel 接入:把应用已有的 OTel span 发送给 NiceEval,在 niceeval view 中查看每轮调用瀑布图;评估用例断言仍以 Send 返回的事件和用量为准。
24
25
  - `docs-site/zh/tutorials/connect-your-agent.mdx` — 接入你的 Agent:写一个 Adapter、配置一个 Experiment,并运行第一条评估用例;再把配置和参数从 Experiment 传给 Adapter 与被测应用。
25
26
  - `docs-site/zh/tutorials/custom-reports.mdx` — 编写自定义报告:用 defineReport、内置报告组件和双面组件编写一份同时用于 niceeval show 与 niceeval view 的自定义报告。
package/dist/i18n/en.d.ts CHANGED
@@ -63,7 +63,6 @@ export declare const en: {
63
63
  "judge.modelMissing": string;
64
64
  "loaders.yamlMissing": string;
65
65
  "cli.flag.parseError": string;
66
- "cli.envInvalidNumber": string;
67
66
  "cli.help": string;
68
67
  "cli.show.noResults": string;
69
68
  "cli.show.runDirMissing": string;
@@ -134,6 +133,7 @@ export declare const en: {
134
133
  "define.experimentAgentRequired": string;
135
134
  "define.experimentFlagNotJson": string;
136
135
  "define.experimentLabelInvalid": string;
136
+ "define.experimentProvenanceFlagsInvalid": string;
137
137
  "define.experimentSetupNotFunction": string;
138
138
  "define.experimentClassifyFailureNotFunction": string;
139
139
  "define.experimentIdRejected": string;
package/dist/i18n/en.js CHANGED
@@ -65,11 +65,10 @@ export const en = {
65
65
  "runner.gateLeaseWaiting": "waiting on another run for experiment {{experimentId}}'s concurrency slots: all {{effectiveN}} in use ({{holders}}). Concurrent runs share this experiment's slots, and the smallest maxConcurrency in play wins — this run declared {{declaredN}}. Nothing dispatches until a slot frees up; the other run's slots release when its attempts finish, or 30s after it dies.\n",
66
66
  "runner.dispatchHaltedExperiment": "experiment halted (dispatch-halted): {{message}}\n",
67
67
  "runner.dispatchHaltedEval": "eval halted: {{message}}\n",
68
- "judge.modelMissing": "No judge model configured. Set it in defineConfig({ judge: { model: \"...\" } }), the eval's judge config, or the NICEEVAL_JUDGE_MODEL environment variable (there is no built-in default model).\n" +
68
+ "judge.modelMissing": "No judge model configured. Set it in defineConfig({ judge: { model: \"...\" } }) or the eval's judge config (there is no built-in default model, and no environment variable for it).\n" +
69
69
  " Docs: node_modules/niceeval/docs-site/zh/tutorials/scoring-guide.mdx",
70
70
  "loaders.yamlMissing": "loadYaml(\"{{path}}\") needs a YAML parser: run `pnpm add yaml` first (or switch to loadJson with a JSON dataset).",
71
71
  "cli.flag.parseError": "{{message}}\nRun `niceeval --help` for usage.\n",
72
- "cli.envInvalidNumber": "Environment variable {{name}} is not a number: \"{{value}}\".\n",
73
72
  "cli.help": "niceeval — agent-native evals\n\n" +
74
73
  "Usage:\n" +
75
74
  " niceeval exp [path|experiment] [eval-id-prefix…] run experiments\n" +
@@ -122,8 +121,9 @@ export const en = {
122
121
  " --json (machine feed: NDJSON on stdout; default is human text)\n" +
123
122
  " --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
124
123
  "Positional args only select which evals to run (id prefixes); which agent and\n" +
125
- "how to run come from experiments/ + flags. Env overrides (flag > env > config):\n" +
126
- " NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
124
+ "how to run come from experiments/ + flags. Resolution: flag > experiment >\n" +
125
+ "niceeval.config.ts > built-in default. Configuration has no environment layer;\n" +
126
+ "environment variables hold credentials such as API keys.\n",
127
127
  "cli.show.noResults": "No results found under {{root}}. Run `niceeval exp` first, then `niceeval show`.\n",
128
128
  "cli.show.runDirMissing": "Results directory not found: {{dir}}\n",
129
129
  "cli.show.noEvalMatch": "No results matched: {{patterns}}. Evals with results: {{evals}}\n",
@@ -202,6 +202,7 @@ export const en = {
202
202
  "define.experimentAgentRequired": "defineExperiment requires agent.",
203
203
  "define.experimentFlagNotJson": "experiment.flags.{{key}} is not JSON-serializable (functions / undefined / cycles / bigint are not allowed); flags are persisted verbatim into result snapshots and must be plain JSON.",
204
204
  "define.experimentLabelInvalid": "experiment.labels.{{key}} must be a string or a finite number; labels are report-side grouping coordinates persisted verbatim into result snapshots.",
205
+ "define.experimentProvenanceFlagsInvalid": "experiment.provenanceFlags must be an array of flag key names (strings); it lists the flags recorded for provenance only, which stay out of the cache fingerprint.",
205
206
  "define.experimentSetupNotFunction": "experiment.setup must be a function ((ctx) => void); use experiment.teardown for cleanup; to prepare the in-sandbox environment per experiment, chain .setup() hooks on the sandbox spec instead.",
206
207
  "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure must be a function ((failure) => FailureClass | undefined); it classifies failures that surface as third-party errors and must return undefined for anything it does not recognize.",
207
208
  "define.experimentIdRejected": "defineExperiment does not accept id; ids are derived from file paths.",
@@ -273,7 +274,7 @@ export const en = {
273
274
  "hitl.respondAllEmpty": "There is no pending input.requested request; respond() / respondAll() cannot work. Confirm the turn parked with t.parked(), then answer via t.requireInputRequest() or t.respond().",
274
275
  "hitl.respondEmpty": "t.respond(...) requires at least one answer.",
275
276
  "hitl.stringAmbiguous": "There are {{count}} pending input requests; a plain-string answer cannot be matched to one. Use the { request, optionId } or { request, text } object form to name it explicitly.",
276
- "judge.apiKeyMissing": "judge is missing an API key (CODEX_API_KEY / OPENAI_API_KEY).",
277
+ "judge.apiKeyMissing": "judge is missing an API key: set NICEEVAL_JUDGE_KEY, or point judge.apiKeyEnv at another environment variable.",
277
278
  "judge.httpError": "judge HTTP {{status}}: {{body}}",
278
279
  "judge.probeFailed": "judge precheck failed ({{model}}): {{error}}",
279
280
  "judge.probeTimeout": "judge precheck timed out after {{seconds}}s ({{model}}): the endpoint accepted the connection but never responded — check the judge baseUrl / gateway, or point NICEEVAL_JUDGE_BASE at a responsive one",
@@ -1,5 +1,10 @@
1
1
  import { type MessageKey } from "./zh-CN.ts";
2
2
  import { type Locale, type Vars } from "./core.ts";
3
3
  export type { Locale, Vars } from "./core.ts";
4
+ /**
5
+ * 注入项目配置声明的界面语言。传 undefined(没配 / 没有配置文件)时清空,回到系统 locale 判定。
6
+ * 无法归一的值(如 "C")同样按未声明处理——不为一个装饰性设置让命令失败。
7
+ */
8
+ export declare function setConfiguredLocale(raw: string | undefined): void;
4
9
  export declare function detectLocale(env?: NodeJS.ProcessEnv): Locale;
5
10
  export declare function t(key: MessageKey, vars?: Vars): string;
@@ -1,4 +1,7 @@
1
- // CLI 侧 i18n:内核(插值/归一)在 core.ts;这里只注入 env 来源与 zh-CN 默认值。
1
+ // CLI 侧 i18n:内核(插值/归一)在 core.ts;这里只注入来源(config.locale + 系统 locale)与
2
+ // zh-CN 默认值。界面语言是配置,家在 `defineConfig({ locale })`;系统 locale 是「输出到哪个
3
+ // 终端」的环境事实,只作没配时的判定依据。niceeval 自己不发明 NICEEVAL_LANG 这类配置变量
4
+ // (边界见 docs/architecture.md「配置从代码来,凭据从环境来」)。
2
5
  import { en } from "./en.js";
3
6
  import { zhCN } from "./zh-CN.js";
4
7
  import { interpolate, normalizeLocale } from "./core.js";
@@ -6,9 +9,17 @@ const dictionaries = {
6
9
  "zh-CN": zhCN,
7
10
  en,
8
11
  };
12
+ /** `defineConfig({ locale })` 的归一结果;CLI 装载配置后调 setConfiguredLocale 注入一次。 */
13
+ let configuredLocale;
14
+ /**
15
+ * 注入项目配置声明的界面语言。传 undefined(没配 / 没有配置文件)时清空,回到系统 locale 判定。
16
+ * 无法归一的值(如 "C")同样按未声明处理——不为一个装饰性设置让命令失败。
17
+ */
18
+ export function setConfiguredLocale(raw) {
19
+ configuredLocale = normalizeLocale(raw);
20
+ }
9
21
  export function detectLocale(env = process.env) {
10
- return (normalizeLocale(env.NICEEVAL_LANG) ??
11
- normalizeLocale(env.NICEEVAL_LOCALE) ??
22
+ return (configuredLocale ??
12
23
  normalizeLocale(env.LC_ALL) ??
13
24
  normalizeLocale(env.LC_MESSAGES) ??
14
25
  normalizeLocale(env.LANG) ??
@@ -63,7 +63,6 @@ export declare const zhCN: {
63
63
  readonly "judge.modelMissing": string;
64
64
  readonly "loaders.yamlMissing": "loadYaml(\"{{path}}\") 需要 YAML 解析器:请先 `pnpm add yaml`(或改用 loadJson + JSON 数据集)。";
65
65
  readonly "cli.flag.parseError": "{{message}}\n运行 `niceeval --help` 查看用法。\n";
66
- readonly "cli.envInvalidNumber": "环境变量 {{name}} 不是数字:\"{{value}}\"。\n";
67
66
  readonly "cli.help": string;
68
67
  readonly "cli.show.noResults": "{{root}} 下没有结果。先 `niceeval exp` 跑一轮,再 `niceeval show`。\n";
69
68
  readonly "cli.show.runDirMissing": "Results directory not found: {{dir}}\n";
@@ -134,6 +133,7 @@ export declare const zhCN: {
134
133
  readonly "define.experimentAgentRequired": "defineExperiment 需要 agent。";
135
134
  readonly "define.experimentFlagNotJson": "experiment.flags.{{key}} 不是可 JSON 序列化的值(函数 / undefined / 循环引用 / bigint 不允许);flags 会原样进入结果快照,必须是纯 JSON。";
136
135
  readonly "define.experimentLabelInvalid": "experiment.labels.{{key}} 必须是字符串或有限数字;labels 是报告侧的归类坐标,会原样进入结果快照。";
136
+ readonly "define.experimentProvenanceFlagsInvalid": "experiment.provenanceFlags 必须是 flag 键名(字符串)数组;它列出只作为出处记录、不进缓存指纹的那些 flag。";
137
137
  readonly "define.experimentSetupNotFunction": "experiment.setup 必须是函数((ctx) => void);要清理请挂 experiment.teardown;要按实验准备沙箱内环境请挂 sandbox spec 的 .setup() 钩子链。";
138
138
  readonly "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure 必须是函数((failure) => FailureClass | undefined):它识别以第三方错误形态浮出的失败,认不出的一律返回 undefined 交给后续链路。";
139
139
  readonly "define.experimentIdRejected": "defineExperiment 不接受 id —— id 由文件路径推导。";
@@ -205,7 +205,7 @@ export declare const zhCN: {
205
205
  readonly "hitl.respondAllEmpty": "没有待回答的 input.requested 请求,respond() / respondAll() 无法工作;先用 t.parked() 确认停轮,再用 t.requireInputRequest() 或 t.respond() 回答。";
206
206
  readonly "hitl.respondEmpty": "t.respond(...) 至少需要一个回答。";
207
207
  readonly "hitl.stringAmbiguous": "有 {{count}} 条待回答请求,字符串回答无法对位,请用 { request, optionId } 或 { request, text } 对象形式显式指名。";
208
- readonly "judge.apiKeyMissing": "judge 缺少 API key(CODEX_API_KEY / OPENAI_API_KEY)。";
208
+ readonly "judge.apiKeyMissing": "judge 缺少 API key:设置 NICEEVAL_JUDGE_KEY,或用 judge.apiKeyEnv 指向别的环境变量。";
209
209
  readonly "judge.httpError": "judge HTTP {{status}}: {{body}}";
210
210
  readonly "judge.probeFailed": "judge 预检失败({{model}}): {{error}}";
211
211
  readonly "judge.probeTimeout": "judge 预检 {{seconds}}s 超时({{model}}):端点接受了连接但一直不回 —— 检查 judge 的 baseUrl / 网关,或把 NICEEVAL_JUDGE_BASE 指向一个有响应的网关";
@@ -65,11 +65,10 @@ export const zhCN = {
65
65
  "runner.gateLeaseWaiting": "在等别的运行让出实验 {{experimentId}} 的并发名额:生效的 {{effectiveN}} 个位子全被占着({{holders}})。并行运行共用同一实验的名额,生效值取在场声明里最小的那个——本次运行声明的是 {{declaredN}}。名额腾出来之前不会派发任何 attempt;对方的名额会在它的 attempt 跑完时释放,它若已死则 30s 后过期被接管。\n",
66
66
  "runner.dispatchHaltedExperiment": "实验已止损(dispatch-halted):{{message}}\n",
67
67
  "runner.dispatchHaltedEval": "eval 已止损:{{message}}\n",
68
- "judge.modelMissing": "judge 未配置模型:在 defineConfig({ judge: { model: \"...\" } })eval 的 judge 配置或环境变量 NICEEVAL_JUDGE_MODEL 里指定裁判模型(没有内置默认模型)。\n" +
68
+ "judge.modelMissing": "judge 未配置模型:在 defineConfig({ judge: { model: \"...\" } })eval 的 judge 配置里指定裁判模型(没有内置默认模型,也没有对应的环境变量)。\n" +
69
69
  " 文档:node_modules/niceeval/docs-site/zh/tutorials/scoring-guide.mdx",
70
70
  "loaders.yamlMissing": "loadYaml(\"{{path}}\") 需要 YAML 解析器:请先 `pnpm add yaml`(或改用 loadJson + JSON 数据集)。",
71
71
  "cli.flag.parseError": "{{message}}\n运行 `niceeval --help` 查看用法。\n",
72
- "cli.envInvalidNumber": "环境变量 {{name}} 不是数字:\"{{value}}\"。\n",
73
72
  "cli.help": "niceeval — agent-native evals\n\n" +
74
73
  "用法:\n" +
75
74
  " niceeval exp [路径|实验] [eval-id 前缀…] 跑实验\n" +
@@ -117,8 +116,8 @@ export const zhCN = {
117
116
  " --json (机器面:stdout 上的 NDJSON 事件流;默认是人读文本)\n" +
118
117
  " --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
119
118
  "位置参数只选「跑哪些 eval」(id 前缀);对着哪个 agent、怎么跑来自 experiments/ 与\n" +
120
- "标志。环境变量覆盖(标志 > 环境变量 > config):\n" +
121
- " NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
119
+ "标志。取值优先级:标志 > experiment > niceeval.config.ts > 内置默认;配置项没有\n" +
120
+ "环境变量层,环境变量只放 API key 这类凭据。\n",
122
121
  // show 的错误文案保持英文(错误文案英文的仓库约定);noResults 是提示,翻译。
123
122
  "cli.show.noResults": "{{root}} 下没有结果。先 `niceeval exp` 跑一轮,再 `niceeval show`。\n",
124
123
  "cli.show.runDirMissing": "Results directory not found: {{dir}}\n",
@@ -198,6 +197,7 @@ export const zhCN = {
198
197
  "define.experimentAgentRequired": "defineExperiment 需要 agent。",
199
198
  "define.experimentFlagNotJson": "experiment.flags.{{key}} 不是可 JSON 序列化的值(函数 / undefined / 循环引用 / bigint 不允许);flags 会原样进入结果快照,必须是纯 JSON。",
200
199
  "define.experimentLabelInvalid": "experiment.labels.{{key}} 必须是字符串或有限数字;labels 是报告侧的归类坐标,会原样进入结果快照。",
200
+ "define.experimentProvenanceFlagsInvalid": "experiment.provenanceFlags 必须是 flag 键名(字符串)数组;它列出只作为出处记录、不进缓存指纹的那些 flag。",
201
201
  "define.experimentSetupNotFunction": "experiment.setup 必须是函数((ctx) => void);要清理请挂 experiment.teardown;要按实验准备沙箱内环境请挂 sandbox spec 的 .setup() 钩子链。",
202
202
  "define.experimentClassifyFailureNotFunction": "experiment.classifyFailure 必须是函数((failure) => FailureClass | undefined):它识别以第三方错误形态浮出的失败,认不出的一律返回 undefined 交给后续链路。",
203
203
  "define.experimentIdRejected": "defineExperiment 不接受 id —— id 由文件路径推导。",
@@ -269,7 +269,7 @@ export const zhCN = {
269
269
  "hitl.respondAllEmpty": "没有待回答的 input.requested 请求,respond() / respondAll() 无法工作;先用 t.parked() 确认停轮,再用 t.requireInputRequest() 或 t.respond() 回答。",
270
270
  "hitl.respondEmpty": "t.respond(...) 至少需要一个回答。",
271
271
  "hitl.stringAmbiguous": "有 {{count}} 条待回答请求,字符串回答无法对位,请用 { request, optionId } 或 { request, text } 对象形式显式指名。",
272
- "judge.apiKeyMissing": "judge 缺少 API key(CODEX_API_KEY / OPENAI_API_KEY)。",
272
+ "judge.apiKeyMissing": "judge 缺少 API key:设置 NICEEVAL_JUDGE_KEY,或用 judge.apiKeyEnv 指向别的环境变量。",
273
273
  "judge.httpError": "judge HTTP {{status}}: {{body}}",
274
274
  "judge.probeFailed": "judge 预检失败({{model}}): {{error}}",
275
275
  "judge.probeTimeout": "judge 预检 {{seconds}}s 超时({{model}}):端点接受了连接但一直不回 —— 检查 judge 的 baseUrl / 网关,或把 NICEEVAL_JUDGE_BASE 指向一个有响应的网关",
@@ -1,14 +1,25 @@
1
- import type { DiscoveredEval, EvalResult, SandboxOption } from "../types.ts";
1
+ import type { DiscoveredEval, EvalResult, JsonValue, SandboxOption } from "../types.ts";
2
2
  import type { AgentRun } from "./types.ts";
3
3
  export declare function cacheKey(run: AgentRun, evalId: string): string;
4
4
  /**
5
5
  * @param sourceCache 按 sourcePath 缓存文件内容:一个矩阵(实验 × eval)会对同一批源文件
6
6
  * 反复算指纹,不带缓存会在任何 attempt 起跑前做 E×N 次重复文件读。
7
+ * @param flagsOverride 用这份 flags 代替 `run.flags` 的指纹口径算一遍。只有一个用途:
8
+ * 对已落盘结果做**反事实重算**——「把 flags 换成它当时那份,指纹还相等吗」等价于问
9
+ * 「除 flags 外的一切是否都没变」,`acceptableFingerprints` 用它判定某条历史结果与本次
10
+ * 规划的差异是否完全落在 provenance flag 上。
7
11
  */
8
- export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>, configSandbox?: SandboxOption): Promise<string>;
12
+ export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>, configSandbox?: SandboxOption, flagsOverride?: Record<string, JsonValue>): Promise<string>;
9
13
  export interface CarryPlan {
10
14
  /** `cacheKey(run, evalId)` → 本次规划出的指纹,供调用方按同一口径判断"这条要不要携入"。 */
11
15
  plannedFingerprints: Map<string, string>;
16
+ /**
17
+ * `cacheKey(run, evalId)` → 这条组合**可以携带的全部指纹**:本次规划的那个,加上
18
+ * 「只在 provenance flag 上与本次不同」的历史口径(见 `acceptableFingerprints`)。
19
+ * 没声明 provenance flag 时恒是单元素集合 = `plannedFingerprints` 的那一个。
20
+ * 携带判定一律读这个集合,`plannedFingerprints` 只用来给新跑的 attempt 落盘打戳。
21
+ */
22
+ acceptableFingerprints: Map<string, Set<string>>;
12
23
  /**
13
24
  * 携带以 attempt 为粒度:命中携入条件(该 attempt 自身 passed/failed 终态 + 指纹匹配)的
14
25
  * `${experimentId}|${evalId}` → 该 eval 下具体携入的 attempt 序号集合(0-based)。同一个
@@ -35,14 +46,16 @@ export declare function resolvedTimeoutMsForCarry(run: AgentRun, evalDef: Discov
35
46
  * 1. 该 attempt 自己是终态(`passed` / `failed`)。`errored` 是框架/环境层面的不确定失败,
36
47
  * 判定本身不可信;`skipped` 根本没跑。同一 eval 的别的序号命中不能连带把它捎上
37
48
  * (反例与修法见 memory 的 carry-must-be-per-attempt-not-whole-eval-key)。
38
- * 2. 该 attempt 落盘的 `fingerprint` 与本次规划的 `fingerprint` 相等。
49
+ * 2. 该 attempt 落盘的 `fingerprint` 落在本次的可携带指纹集合里(`CarryPlan.acceptableFingerprints`
50
+ * 的那一条,通常只有本次规划出的那一个;声明了 provenance flag 时还含「只在这些键上与本次
51
+ * 不同」的历史口径)。
39
52
  * 3. 该 attempt 的 `durationMs` 不超过本次 resolved 的 `timeoutMs`——`timeoutMs` 是携带资格
40
53
  * 判据、不进指纹哈希(docs/runner.md「缓存:指纹去重」)。
41
54
  *
42
55
  * `planCarry`(整场静态规划)与 run.ts 派发时刻的携带重查共用这一个函数:两条路径一旦把判据
43
56
  * 各写一份就会分叉,重查会携入静态规划判过不可携带的条目(或反过来)。
44
57
  */
45
- export declare function carriableAttempts(priorResults: EvalResult[] | undefined, key: string, fingerprint: string | undefined, timeoutMs: number): EvalResult[];
58
+ export declare function carriableAttempts(priorResults: EvalResult[] | undefined, key: string, fingerprints: ReadonlySet<string> | undefined, timeoutMs: number): EvalResult[];
46
59
  /**
47
60
  * 算出这一批 (agentRun × eval) 的指纹,并据此从 priorResults 里筛出可以携入(跳过重跑)的结果。
48
61
  * run.ts 与 cli.ts(live 表格构建)必须共用这同一份计算 —— 否则两边一旦对"哪些携入"的判断
@@ -58,4 +71,35 @@ export declare function carriableAttempts(priorResults: EvalResult[] | undefined
58
71
  * `resolvedTimeoutMsForCarry`)。省略时按未配置处理,不是当作 0——只有 `run.timeoutMs` /
59
72
  * `evalDef.timeoutMs` 都缺席时才轮到它兜底。
60
73
  */
61
- export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined, configSandbox?: SandboxOption, configTimeoutMs?: number): Promise<CarryPlan>;
74
+ export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined, configSandbox?: SandboxOption, configTimeoutMs?: number, flagBagsByExperiment?: Map<string, Record<string, JsonValue>[]>): Promise<CarryPlan>;
75
+ /**
76
+ * 这条 `(experimentId, evalId)` 本次可以携带的指纹全集。
77
+ *
78
+ * 没声明 provenance flag 时就是 `{ primary }`——判据与「指纹相等」逐字等价,一条历史结果都
79
+ * 不会因此多携入。声明了之后多出一类:**只在 provenance flag 上与本次不同**的历史口径。
80
+ *
81
+ * 判定不靠比对两串哈希的差异(哈希不可差分),而是**反事实重算**:取该历史结果所属快照记下的
82
+ * `ExperimentRunInfo.flags`(整袋原样,`applySnapshotDefaults` 已把它挂在 `EvalResult.experiment`
83
+ * 上),用它替换本次的 flags 口径重算一遍指纹——算出来等于历史那一串,就证明「除 flags 外的
84
+ * 一切(eval 源码、agent、model、sandbox、strict…)都没变」。再要求两袋 flags 抹掉 provenance
85
+ * 键之后逐字相等,才把这串历史指纹计入可携带集合:真改了某个影响行为的 flag(`webResearch`
86
+ * 从 true 改成 false)照旧作废,不会被这条通道放行。
87
+ *
88
+ * 历史结果落盘时的指纹口径是「整袋 flags」(provenance 概念引入之前),所以两个口径都要试:
89
+ * 整袋(老结果)与抹掉 provenance 键的那袋(声明之后跑出来的结果,与 primary 相同则自然去重)。
90
+ */
91
+ export declare function acceptableFingerprints(args: {
92
+ evalDef: DiscoveredEval;
93
+ run: AgentRun;
94
+ key: string;
95
+ priorResults: EvalResult[] | undefined;
96
+ /** 本次规划出的指纹(新跑的 attempt 用它落盘打戳)。 */
97
+ primary: string;
98
+ /**
99
+ * 该实验历史快照记下过的 flags(见 `loadCarryInputs`)。候选假设的来源之一,与结果自带的那袋
100
+ * 并列——携带条目带着**产出它那一轮**的指纹合入新快照,那一轮的 flags 只在更早的快照里留着。
101
+ */
102
+ historicalFlagBags?: readonly Record<string, JsonValue>[];
103
+ sourceCache?: Map<string, Promise<string>>;
104
+ configSandbox?: SandboxOption;
105
+ }): Promise<Set<string>>;
@@ -280,7 +280,7 @@ export interface InvocationShape {
280
280
  configs: number;
281
281
  /** 总 attempt 数(evals × configs × runs);逐行输出与汇总计数都按它。 */
282
282
  totalAttempts: number;
283
- /** 本次运行实际生效的全局并发数(flag/env/config/sandbox 默认值解析后的结果);
283
+ /** 本次运行实际生效的全局并发数(flag/config/sandbox 默认值解析后的结果);
284
284
  * 实验级 maxConcurrency 只在该实验内部限流,不改这个全局值。 */
285
285
  maxConcurrency: number;
286
286
  /**
@@ -500,6 +500,21 @@ export interface ExperimentDef {
500
500
  * (defineExperiment 解析时校验,非 JSON 直接报错),经 ctx.flags 透传给 adapter、
501
501
  * t.flags 暴露给 eval,并原样进入结果快照的 ExperimentRunInfo.flags。 */
502
502
  flags?: Record<string, JsonValue>;
503
+ /**
504
+ * `flags` 里只作为**出处记录**的键名:照常落盘、照常透给 `ctx.flags` / `t.flags`,但不参与
505
+ * 可比性配置——值变了不作废任何已有结果,已跑完的照常携带(carry)。
506
+ *
507
+ * 给的是「每次跑都可能换、但换了不改变 attempt 里发生什么」的坐标:隧道 / 反向代理 URL、
508
+ * 服务端实例地址、跑批时刻这类。它们要留在 `flags` 里(报告要按 `flag()` 看这轮连的是哪个,
509
+ * eval 或 adapter 也可能要读),又不该像 `webResearch: true → false` 那样让缓存全部失效。
510
+ *
511
+ * 声明前跑出来的结果同样携带得到:携带判定按快照记下的历史 flags 做一次反事实重算,
512
+ * 确认差异完全落在这些键上(见 `runner/fingerprint.ts` 的 `acceptableFingerprints`)。
513
+ * 键不必存在于 `flags` 里——把一个键从 `flags` 移走时留着这条声明,历史结果照样不作废。
514
+ *
515
+ * 完全不需要在运行时被看见的事实用 `labels`,那是报告侧坐标(本来就不进指纹)。
516
+ */
517
+ provenanceFlags?: readonly string[];
503
518
  /**
504
519
  * 报告归类标注:实验在各对比轴上的坐标(如 `{ line: "codex", memory: "mempal" }`)。
505
520
  * 值域 string | number(解析时校验)。与 `flags` 的分界是「会不会改变 attempt 里发生的事」:
@@ -600,6 +615,11 @@ export interface Config {
600
615
  * 可传字符串,或按 locale 提供多语言(如 `{ en: "...", "zh-CN": "..." }`),随 view 语言切换。
601
616
  */
602
617
  name?: LocalizedText;
618
+ /**
619
+ * CLI 与运行时文案的界面语言(BCP 47,如 `"en"` / `"zh-CN"`);CI 里想让日志恒定一种语言就写这个。
620
+ * 省略则按系统 locale(`LC_ALL` / `LC_MESSAGES` / `LANG`)判定,都没有时用 `zh-CN`。
621
+ */
622
+ locale?: string;
603
623
  /** 项目级默认 Sandbox provider(docker / vercel / e2b / custom);experiment 可覆盖。 */
604
624
  sandbox?: SandboxOption;
605
625
  /** 上传进 Sandbox 的工作区根目录,省略则用项目根;评估用例的 sandbox 视图从这里起步。 */
@@ -608,7 +628,7 @@ export interface Config {
608
628
  judge?: JudgeConfig;
609
629
  /** 项目级默认 reporter 列表(如落盘 / 上传结果);EvalDef.reporters 会与它合并。 */
610
630
  reporters?: Reporter[];
611
- /** 项目级默认并发上限;CLI flag / env / experiment 的同名设置优先级更高。 */
631
+ /** 项目级默认并发上限;CLI flag / experiment 的同名设置优先级更高(没有环境变量层)。 */
612
632
  maxConcurrency?: number;
613
633
  /** 项目级默认单次 attempt 超时(毫秒);CLI flag / experiment / EvalDef 的同名设置优先级更高。 */
614
634
  timeoutMs?: number;
@@ -669,6 +689,8 @@ export interface AgentRun {
669
689
  model?: string;
670
690
  reasoningEffort?: string;
671
691
  flags: Record<string, JsonValue>;
692
+ /** 只作为出处记录、不进指纹的 flag 键(来自 ExperimentDef.provenanceFlags);见该字段说明。 */
693
+ provenanceFlags?: readonly string[];
672
694
  runs: number;
673
695
  earlyExit: boolean;
674
696
  sandbox?: SandboxOption;
@@ -794,8 +816,14 @@ export interface ActiveAttempt {
794
816
  /** 展示 label,等价 `runWho()` 的结果;渲染要用,但绝不作为 identity/key。 */
795
817
  who: string;
796
818
  phase: LifecyclePhase;
797
- /** 进入当前 phase 的墙钟时间(epoch ms),用于渲染阶段耗时;每次 phase 变化都会更新。 */
798
- phaseStartedAt: number;
819
+ /**
820
+ * 这条 attempt 被派发的墙钟时间(epoch ms,取 `attempt:start` 的 `at`)—— active 行时间列的
821
+ * **唯一**基准,`attempt:phase` 不得改写它:live 面板不做 spinner 动画,存活性完全由这一列
822
+ * 持续增长证明(见 docs/feature/experiments/cli.md「active 行的列序」),一列会归零的时间既
823
+ * 证明不了存活,也让人误以为这条 eval 重跑了。阶段各自的耗时不进这里——它由结果的
824
+ * `timing.phases` 完整落盘,live 面板要回答的是「这条还活着吗、跑了多久、正在干什么」。
825
+ */
826
+ startedAt: number;
799
827
  detail?: string;
800
828
  }
801
829
  /** 实验级钩子只有 setup 与它返回的 teardown 两员,同一实验内两者永不并发
@@ -19,7 +19,7 @@ description: "一个纯 Python LangGraph + LangSmith OTel 导出的应用,接
19
19
 
20
20
  接入的全部代码变更(生成时从两个目录实测统计):
21
21
 
22
- <table className="gd-summary"><tbody><tr><th>{"类别"}</th><th>{"文件数"}</th><th>{"行数"}</th></tr><tr><td>{"评估用例侧 TS 项目脚手架(必要:被测应用是 Python,全新文件)"}</td><td>{"3"}</td><td>{"+42"}</td></tr><tr><td>{"adapter(必要:传输粘合,协议映射在官方包里)"}</td><td>{"2"}</td><td>{"+182"}</td></tr><tr><td>{"evals 与 experiments(评测内容,按需增长)"}</td><td>{"6"}</td><td>{"+118"}</td></tr><tr className="gd-total"><td>{"合计"}</td><td>{"11"}</td><td>{"+342"}</td></tr></tbody></table>
22
+ <table className="gd-summary"><tbody><tr><th>{"类别"}</th><th>{"文件数"}</th><th>{"行数"}</th></tr><tr><td>{"评估用例侧 TS 项目脚手架(必要:被测应用是 Python,全新文件)"}</td><td>{"3"}</td><td>{"+42"}</td></tr><tr><td>{"adapter(必要:传输粘合,协议映射在官方包里)"}</td><td>{"2"}</td><td>{"+181"}</td></tr><tr><td>{"evals 与 experiments(评测内容,按需增长)"}</td><td>{"6"}</td><td>{"+118"}</td></tr><tr className="gd-total"><td>{"合计"}</td><td>{"11"}</td><td>{"+341"}</td></tr></tbody></table>
23
23
 
24
24
  ## 文件清单
25
25
 
@@ -67,9 +67,9 @@ langgraph/
67
67
  ## 新增的 adapter、evals 与 experiments
68
68
 
69
69
  <div className="gd-file">
70
- <div className="gd-head"><span className="gd-name">{"niceeval.config.ts"}</span><span className="gd-stats"><span className="gd-plus">{"+14"}</span></span></div>
70
+ <div className="gd-head"><span className="gd-name">{"niceeval.config.ts"}</span><span className="gd-stats"><span className="gd-plus">{"+13"}</span></span></div>
71
71
  <div className="gd-body">
72
- <table className="gd-table"><tbody><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"1"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt4">{"import"}</span><span className="gdt0">{" { defineConfig } "}</span><span className="gdt4">{"from"}</span><span className="gdt0">{" "}</span><span className="gdt2">{"\"niceeval\""}</span><span className="gdt0">{";"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"2"}</td><td className="gd-sign">{"+"}</td><td className="gd-code">{" "}</td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"3"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 注:这个 app 的 .env 把标准的 OPENAI_API_KEY / OPENAI_BASE_URL 挪用给了 DeepSeek"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"4"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// (agent.py 里 ChatOpenAI 直接读这两个 env 名)。niceeval 的 judge(t.judge.autoevals.*)"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"5"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 兜底链路最后也会读这两个名字,和应用自己的凭证会撞车——真的要用 judge 时在 .env 里另配"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"6"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// NICEEVAL_JUDGE_KEY / NICEEVAL_JUDGE_BASE(judge.ts 里优先级最高),judge 走独立凭证,"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"7"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 不和应用的模型配置互相干扰。"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"8"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt4">{"export"}</span><span className="gdt0">{" "}</span><span className="gdt4">{"default"}</span><span className="gdt0">{" "}</span><span className="gdt5">{"defineConfig"}</span><span className="gdt0">{"({"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"9"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" name: { "}</span><span className="gdt2">{"\"zh-CN\""}</span><span className="gdt0">{": "}</span><span className="gdt2">{"\"LangGraph 示例\""}</span><span className="gdt0">{", en: "}</span><span className="gdt2">{"\"LangGraph example\""}</span><span className="gdt0">{" },"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"10"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" judge: { model: "}</span><span className="gdt2">{"\"gpt-5.4\""}</span><span className="gdt0">{" },"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"11"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" timeoutMs: "}</span><span className="gdt1">{"120_000"}</span><span className="gdt0">{","}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"12"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" "}</span><span className="gdt6">{"// 被测应用是用户自己起的长驻服务,别开太高并发。"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"13"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" maxConcurrency: "}</span><span className="gdt1">{"2"}</span><span className="gdt0">{","}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"14"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{"});"}</span></td></tr></tbody></table>
72
+ <table className="gd-table"><tbody><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"1"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt4">{"import"}</span><span className="gdt0">{" { defineConfig } "}</span><span className="gdt4">{"from"}</span><span className="gdt0">{" "}</span><span className="gdt2">{"\"niceeval\""}</span><span className="gdt0">{";"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"2"}</td><td className="gd-sign">{"+"}</td><td className="gd-code">{" "}</td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"3"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 注:这个 app 的 .env 把标准的 OPENAI_API_KEY / OPENAI_BASE_URL 挪用给了 DeepSeek"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"4"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// (agent.py 里 ChatOpenAI 直接读这两个 env 名)。niceeval 的 judge(t.judge.autoevals.*)"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"5"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 不碰这两个名字:端点写在 judge.baseUrl,key 只读 NICEEVAL_JUDGE_KEY(或 judge.apiKeyEnv"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"6"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt6">{"// 指定的变量名),judge 走独立凭证,不和应用的模型配置互相干扰。"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"7"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt4">{"export"}</span><span className="gdt0">{" "}</span><span className="gdt4">{"default"}</span><span className="gdt0">{" "}</span><span className="gdt5">{"defineConfig"}</span><span className="gdt0">{"({"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"8"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" name: { "}</span><span className="gdt2">{"\"zh-CN\""}</span><span className="gdt0">{": "}</span><span className="gdt2">{"\"LangGraph 示例\""}</span><span className="gdt0">{", en: "}</span><span className="gdt2">{"\"LangGraph example\""}</span><span className="gdt0">{" },"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"9"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" judge: { model: "}</span><span className="gdt2">{"\"gpt-5.4\""}</span><span className="gdt0">{" },"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"10"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" timeoutMs: "}</span><span className="gdt1">{"120_000"}</span><span className="gdt0">{","}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"11"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" "}</span><span className="gdt6">{"// 被测应用是用户自己起的长驻服务,别开太高并发。"}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"12"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{" maxConcurrency: "}</span><span className="gdt1">{"2"}</span><span className="gdt0">{","}</span></td></tr><tr className="gd-add"><td className="gd-ln"></td><td className="gd-ln">{"13"}</td><td className="gd-sign">{"+"}</td><td className="gd-code"><span className="gdt0">{"});"}</span></td></tr></tbody></table>
73
73
  </div>
74
74
  </div>
75
75
 
@@ -83,7 +83,7 @@ defineEval({
83
83
  t.judge.autoevals.closedQA("rubric", { on: t.reply, model: "openai/gpt-4o" });
84
84
  ```
85
85
 
86
- 三级都没配时,最后还会读环境变量 `NICEEVAL_JUDGE_MODEL`;连它也没有就是配置错误,调用点直接报错——judge 没有内置默认模型。
86
+ 三级都没配就是配置错误,调用点直接报错——judge 没有内置默认模型,也没有对应的环境变量(模型是配置,只从代码来)。
87
87
 
88
88
  ## 评判端点与 key:OpenAI 兼容协议
89
89
 
@@ -100,15 +100,17 @@ defineConfig({
100
100
  });
101
101
  ```
102
102
 
103
- `baseUrl` 和 key 都按从具体到笼统解析,配置字段优先、环境变量兜底:
103
+ 端点是配置,key 是凭据,两者的来源分开:
104
104
 
105
105
  | 项 | 解析顺序 |
106
106
  |---|---|
107
- | 端点 | `judge.baseUrl` → `NICEEVAL_JUDGE_BASE` → `CODEX_BASE_URL` → `OPENAI_BASE_URL` → `https://api.openai.com/v1` |
108
- | key | `judge.apiKeyEnv` 指定的环境变量 → `NICEEVAL_JUDGE_KEY` → `CODEX_API_KEY` → `OPENAI_API_KEY` |
107
+ | 端点 | `judge.baseUrl` → `https://api.openai.com/v1` |
108
+ | key | `judge.apiKeyEnv` 指定的环境变量 → `NICEEVAL_JUDGE_KEY` |
109
+
110
+ 接自己的网关或 OpenAI 兼容代理时把地址显式写进 `judge.baseUrl`——NiceEval 不去环境里翻 `OPENAI_BASE_URL` 这类变量猜端点。key 同理只读一个名字:`judge.apiKeyEnv` 指定的那个,或默认的 `NICEEVAL_JUDGE_KEY`;judge 有自己的凭据,不会借用被测应用或某个 agent 的 key。
109
111
 
110
112
  <Warning>
111
- key 一个都解析不到时,judge 断言**静默跳过**——不报错、不记分,eval 照常跑。全绿不代表 judge 真的评过。配完 key 先跑一条带 `t.judge` 的 eval,在 `niceeval view` 里确认出现了 judge 分数。
113
+ 模型或 key 解析不到时,这条 judge 断言记成 `unavailable`(带原因),不会静默消失:除非显式链了 `.optional()`,评不了就让这次 attempt 记 `errored`——评不出来的结论既不算通过,也不该算 agent 答错。配完 key 先跑一条带 `t.judge` 的 eval,在 `niceeval view` 里确认出现了 judge 分数。
112
114
  </Warning>
113
115
 
114
116
  ## 严重度:judge 默认 soft
@@ -54,7 +54,7 @@ npx niceeval exp local fixtures/button --runs 5 --early-exit
54
54
 
55
55
  ## 缓存
56
56
 
57
- [NiceEval](https://niceeval.com/) 可以根据输入、配置和相关文件 fingerprint 跳过已判定为 `passed` 或 `failed` 的结果——两者都是判定确定的终态。`errored`(超时、Sandbox 异常等框架/环境层面的不确定失败)永远重试。缓存适合加速迭代,但如果你在调试非确定性行为,应该明确关闭(`--force`)或清理相关缓存。
57
+ [NiceEval](https://niceeval.com/) 可以根据输入、配置和相关文件 fingerprint 跳过已判定为 `passed` 或 `failed` 的结果——两者都是判定确定的终态。`errored`(超时、Sandbox 异常等框架/环境层面的不确定失败)永远重试。缓存适合加速迭代,但如果你在调试非确定性行为,应该明确关闭(`--rerun all`)或清理相关缓存。
58
58
 
59
59
  ## 并行开多个终端
60
60
 
@@ -31,14 +31,17 @@ description: "NiceEval CLI 参考:exp、show、view、init、list 和 clean
31
31
 
32
32
  ## 输出语言
33
33
 
34
- [NiceEval](https://niceeval.com/) 的 CLI 和运行时文案支持本地化,不需要在 `niceeval.config.ts` 里加配置。需要固定输出语言时用环境变量:
34
+ [NiceEval](https://niceeval.com/) 的 CLI 和运行时文案支持本地化。要固定输出语言(CI 日志尤其需要),写在 `niceeval.config.ts` 里:
35
35
 
36
- ```bash
37
- NICEEVAL_LANG=en npx niceeval list
38
- NICEEVAL_LANG=zh-CN npx niceeval list
36
+ ```ts
37
+ import { defineConfig } from "niceeval";
38
+
39
+ export default defineConfig({
40
+ locale: "en",
41
+ });
39
42
  ```
40
43
 
41
- 检测顺序是 `NICEEVAL_LANG`、`NICEEVAL_LOCALE`、`LC_ALL`、`LC_MESSAGES`、`LANG`。以 `zh` 开头的值使用 `zh-CN`,其它语言使用 `en`;都没有时默认 `zh-CN`。这只影响终端/runtime 文案,不改变结果 JSON 里的机器字段,也不翻译 LLM judge prompt。
44
+ 没写 `locale` 时按系统 locale 判定,依次看 `LC_ALL`、`LC_MESSAGES`、`LANG`。以 `zh` 开头的值使用 `zh-CN`,其它语言使用 `en`;都没有时默认 `zh-CN`。这只影响终端/runtime 文案,不改变结果 JSON 里的机器字段,也不翻译 LLM judge prompt。
42
45
 
43
46
  ## `npx niceeval exp [path|config] [id-prefix...]`
44
47
 
@@ -193,20 +196,11 @@ npx niceeval show weather/brooklyn --history
193
196
 
194
197
  `--strict` 不是"更严格地报错",而是改变软阈值断言的判定:`.atLeast(n)` 这类软阈值(`soft` severity)平时失败不会把整条评估用例判为 `failed`(只是记一条不达标的断言),加了 `--strict` 之后,软阈值没达标也会让整条评估用例的 verdict 计为 `failed`。CI 中推荐加上,避免"断言分数不够但评估用例显示通过"的情况被放过。
195
198
 
196
- ## 环境变量覆盖
197
-
198
- 以下环境变量对应部分 flag,用于不便传 CLI 参数的场景(如 CI 环境配置):
199
-
200
- | 环境变量 | 对应 flag |
201
- |---|---|
202
- | `NICEEVAL_RUNS` | `--runs` |
203
- | `NICEEVAL_MAX_CONCURRENCY` | `--max-concurrency` |
204
- | `NICEEVAL_TIMEOUT` | `--timeout` |
205
- | `NICEEVAL_BUDGET` | `--budget` |
199
+ ## 环境变量只放凭据
206
200
 
207
- 优先级从高到低:CLI flag > 环境变量 > experiment / `niceeval.config.ts` 里的值 > 内置默认值。四个环境变量都要求合法数字,解析失败会直接报错退出。
201
+ 跑几次、超时、并发、预算、judge 模型和端点、界面语言——这些都是配置,只从 CLI flag、`experiments/` 下的 experiment 文件和 `niceeval.config.ts` 读。优先级从高到低:CLI flag > experiment > `niceeval.config.ts` > 内置默认值。没有对应的环境变量,同一个值不会有第三条来路。
208
202
 
209
- [NiceEval](https://niceeval.com/) 还会在启动时自动加载 cwd 下的 `.env` 文件(不需要额外配置,也不覆盖已经存在的环境变量),常用于本地放 `ANTHROPIC_API_KEY` 这类鉴权信息,不用每次手动 `export`。
203
+ 环境变量留给凭据(API key、provider token)和终端环境(`NO_COLOR`、系统 locale)。每个 agent / sandbox / judge 只认自己那一个变量名,不会在环境里翻找其它 key;启动时自动加载 cwd 下的 `.env`(不覆盖已经存在的环境变量)。完整清单和迁移对照见[配置与环境变量](/zh/tutorials/configuration)。
210
204
 
211
205
  ## 退出码
212
206
 
@@ -8,14 +8,18 @@ description: "defineConfig 参考:judge、reporters、并发、超时和 sandb
8
8
 
9
9
  ```ts
10
10
  import { defineConfig } from "niceeval";
11
+ import site from "./reports/site";
11
12
 
12
13
  export default defineConfig({
13
14
  judge: { model: "gpt-5.4-mini" },
14
15
  maxConcurrency: 4,
15
16
  timeoutMs: 300_000,
17
+ report: site,
16
18
  });
17
19
  ```
18
20
 
21
+ `report` 收 `defineReport` 的产物本身(import 自己的报告文件),不是路径字符串。`niceeval show` 与 `niceeval view` 不带 `--report` 时装载它,没写就装载内置的默认报告;`--report` 按次覆盖,`--report standard` 回到内置报告。写法与整套报告能力见[编写自定义报告](/zh/tutorials/custom-reports#设成项目默认)。
22
+
19
23
  ## Config 字段
20
24
 
21
25
  {/* GENERATED:BEGIN config-fields */}
@@ -31,6 +35,15 @@ name?: LocalizedText;
31
35
  项目名,显示在 `niceeval view` 顶部 hero(`<h1>`),省略则回退到通用标题。
32
36
  可传字符串,或按 locale 提供多语言(如 `{ en: "...", "zh-CN": "..." }`),随 view 语言切换。
33
37
 
38
+ #### `locale`
39
+
40
+ ```ts
41
+ locale?: string;
42
+ ```
43
+
44
+ CLI 与运行时文案的界面语言(BCP 47,如 `"en"` / `"zh-CN"`);CI 里想让日志恒定一种语言就写这个。
45
+ 省略则按系统 locale(`LC_ALL` / `LC_MESSAGES` / `LANG`)判定,都没有时用 `zh-CN`。
46
+
34
47
  #### `sandbox`
35
48
 
36
49
  ```ts
@@ -69,7 +82,7 @@ reporters?: Reporter[];
69
82
  maxConcurrency?: number;
70
83
  ```
71
84
 
72
- 项目级默认并发上限;CLI flag / env / experiment 的同名设置优先级更高。
85
+ 项目级默认并发上限;CLI flag / experiment 的同名设置优先级更高(没有环境变量层)。
73
86
 
74
87
  #### `timeoutMs`
75
88
 
@@ -18,7 +18,7 @@ niceeval exp compare/bub-e2b memory/commit0
18
18
  - 只补跑缺的部分:`--runs 5` 已经落盘 3 次,就只再跑 2 次。
19
19
  - 被强杀的实验如果留了没做完的收尾,重跑会先补一次实验级 `teardown` 再开始,泄漏不会越积越多。
20
20
  - 判定为 `errored` 的 Attempt 不复用,照常重跑。
21
- - 想全部重来,加 `--force`。
21
+ - 想全部重来,加 `--rerun all`。
22
22
 
23
23
  ## 收回没清理的 Sandbox 实例
24
24