niceeval 0.4.4 → 0.4.6
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.
- package/README.md +21 -12
- package/README.zh.md +8 -9
- package/docs-site/.mintignore +13 -0
- package/docs-site/AGENTS.md +46 -0
- package/docs-site/concepts/adapter.mdx +287 -0
- package/docs-site/concepts/assert.mdx +243 -0
- package/docs-site/concepts/drive.mdx +118 -0
- package/docs-site/concepts/evals.mdx +108 -0
- package/docs-site/concepts/experiment.mdx +37 -0
- package/docs-site/concepts/hitl.mdx +83 -0
- package/docs-site/concepts/judge.mdx +129 -0
- package/docs-site/concepts/overview.mdx +98 -0
- package/docs-site/concepts/tier.mdx +42 -0
- package/docs-site/docs-ref/00-index.md +23 -0
- package/docs-site/docs-ref/01-page-types.md +43 -0
- package/docs-site/docs-ref/03-style-rules.md +45 -0
- package/docs-site/docs-ref/04-code-examples.md +37 -0
- package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
- package/docs-site/docs-ref/06-checklists.md +53 -0
- package/docs-site/docs.json +256 -0
- package/docs-site/example/ai-agent-application.mdx +148 -0
- package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
- package/docs-site/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/example/showcase.mdx +39 -0
- package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
- package/docs-site/example/tier1-claude-sdk.mdx +119 -0
- package/docs-site/example/tier1-codex-sdk.mdx +111 -0
- package/docs-site/example/tier1-langgraph.mdx +119 -0
- package/docs-site/example/tier1-pi-sdk.mdx +119 -0
- package/docs-site/favicon.svg +5 -0
- package/docs-site/github-diff.css +135 -0
- package/docs-site/github-diff.js +11 -0
- package/docs-site/guides/authoring.mdx +129 -0
- package/docs-site/guides/ci-integration.mdx +97 -0
- package/docs-site/guides/connect-otel.mdx +209 -0
- package/docs-site/guides/connect-your-agent.mdx +221 -0
- package/docs-site/guides/dataset-fanout.mdx +98 -0
- package/docs-site/guides/experiments.mdx +71 -0
- package/docs-site/guides/fixtures.mdx +147 -0
- package/docs-site/guides/reporters.mdx +113 -0
- package/docs-site/guides/runner.mdx +79 -0
- package/docs-site/guides/sandbox-agent.mdx +138 -0
- package/docs-site/guides/sandbox-backends.mdx +64 -0
- package/docs-site/guides/scoring-guide.mdx +92 -0
- package/docs-site/guides/viewing-results.mdx +195 -0
- package/docs-site/guides/write-experiment.mdx +81 -0
- package/docs-site/guides/write-send.mdx +347 -0
- package/docs-site/images/adapter-tiers-zh.svg +60 -0
- package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
- package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
- package/docs-site/images/hitl-handshake-zh.svg +89 -0
- package/docs-site/images/logo.svg +6 -0
- package/docs-site/index.mdx +181 -0
- package/docs-site/introduction.mdx +141 -0
- package/docs-site/quickstart.mdx +136 -0
- package/docs-site/reference/builtin-agents.mdx +161 -0
- package/docs-site/reference/capabilities.mdx +76 -0
- package/docs-site/reference/cli.mdx +120 -0
- package/docs-site/reference/define-agent.mdx +168 -0
- package/docs-site/reference/define-config.mdx +41 -0
- package/docs-site/reference/define-eval.mdx +160 -0
- package/docs-site/reference/events.mdx +131 -0
- package/docs-site/reference/expect.mdx +112 -0
- package/docs-site/tracker.js +8 -0
- package/docs-site/zh/concepts/adapter.mdx +286 -0
- package/docs-site/zh/concepts/assert.mdx +243 -0
- package/docs-site/zh/concepts/drive.mdx +118 -0
- package/docs-site/zh/concepts/evals.mdx +124 -0
- package/docs-site/zh/concepts/experiment.mdx +37 -0
- package/docs-site/zh/concepts/hitl.mdx +83 -0
- package/docs-site/zh/concepts/judge.mdx +130 -0
- package/docs-site/zh/concepts/overview.mdx +109 -0
- package/docs-site/zh/concepts/tier.mdx +44 -0
- package/docs-site/zh/example/ai-agent-application.mdx +152 -0
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
- package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/zh/example/showcase.mdx +39 -0
- package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
- package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
- package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
- package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
- package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
- package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
- package/docs-site/zh/guides/authoring.mdx +129 -0
- package/docs-site/zh/guides/ci-integration.mdx +97 -0
- package/docs-site/zh/guides/connect-otel.mdx +208 -0
- package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
- package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
- package/docs-site/zh/guides/experiments.mdx +72 -0
- package/docs-site/zh/guides/fixtures.mdx +147 -0
- package/docs-site/zh/guides/official-adapters.mdx +132 -0
- package/docs-site/zh/guides/reporters.mdx +113 -0
- package/docs-site/zh/guides/runner.mdx +82 -0
- package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
- package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
- package/docs-site/zh/guides/scoring-guide.mdx +92 -0
- package/docs-site/zh/guides/viewing-results.mdx +195 -0
- package/docs-site/zh/guides/write-experiment.mdx +101 -0
- package/docs-site/zh/guides/write-send.mdx +353 -0
- package/docs-site/zh/index.mdx +180 -0
- package/docs-site/zh/introduction.mdx +140 -0
- package/docs-site/zh/quickstart.mdx +137 -0
- package/docs-site/zh/reference/builtin-agents.mdx +364 -0
- package/docs-site/zh/reference/capabilities.mdx +76 -0
- package/docs-site/zh/reference/cli.mdx +140 -0
- package/docs-site/zh/reference/define-agent.mdx +461 -0
- package/docs-site/zh/reference/define-config.mdx +107 -0
- package/docs-site/zh/reference/define-eval.mdx +674 -0
- package/docs-site/zh/reference/events.mdx +252 -0
- package/docs-site/zh/reference/expect.mdx +208 -0
- package/package.json +3 -2
- package/src/agents/bub.ts +15 -5
- package/src/agents/claude-code.ts +8 -5
- package/src/agents/codex.ts +9 -5
- package/src/agents/sdk-streams.test.ts +4 -4
- package/src/agents/sdk-streams.ts +14 -9
- package/src/agents/types.ts +40 -1
- package/src/agents/ui-message-stream.ts +1 -0
- package/src/cli.ts +36 -7
- package/src/context/types.ts +140 -0
- package/src/expect/index.ts +15 -8
- package/src/i18n/en.ts +19 -6
- package/src/i18n/zh-CN.ts +19 -6
- package/src/o11y/parsers/bub.test.ts +71 -0
- package/src/o11y/parsers/bub.ts +5 -0
- package/src/o11y/types.ts +27 -2
- package/src/runner/reporters/artifacts.ts +11 -3
- package/src/runner/reporters/live.ts +45 -8
- package/src/runner/run.ts +71 -21
- package/src/runner/types.ts +45 -2
- package/src/sandbox/types.ts +18 -0
- package/src/scoring/types.ts +5 -0
- package/src/shared/types.ts +3 -0
- package/src/util.test.ts +26 -1
- package/src/util.ts +16 -0
- package/src/view/app/App.tsx +2 -2
- package/src/view/app/components/AttemptModal.tsx +2 -0
- package/src/view/app/components/CopyControls.tsx +87 -28
- package/src/view/app/i18n.ts +3 -3
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/docs/README.md +0 -120
- package/docs/adapters/README.md +0 -60
- package/docs/adapters/authoring.md +0 -179
- package/docs/adapters/coding-agent-skills-plugins.md +0 -324
- package/docs/adapters/collection.md +0 -128
- package/docs/adapters/contract.md +0 -264
- package/docs/adapters/reference/agent-eval.md +0 -215
- package/docs/adapters/reference/agent-loop-apis.md +0 -69
- package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
- package/docs/adapters/reference/eve-protocol.md +0 -127
- package/docs/adapters/reference/otel-genai.md +0 -107
- package/docs/adapters/reference/otel-instrumentation.md +0 -65
- package/docs/adapters/targets.md +0 -99
- package/docs/architecture.md +0 -140
- package/docs/assertions.md +0 -387
- package/docs/capabilities-by-construction.md +0 -48
- package/docs/cli.md +0 -173
- package/docs/concepts.md +0 -106
- package/docs/e2e-ci.md +0 -332
- package/docs/eval-authoring.md +0 -319
- package/docs/experiments.md +0 -157
- package/docs/getting-started.md +0 -224
- package/docs/multi-agent.md +0 -145
- package/docs/observability.md +0 -337
- package/docs/origin-integration.md +0 -195
- package/docs/references.md +0 -35
- package/docs/reports.md +0 -551
- package/docs/results-format.md +0 -228
- package/docs/results-lib.md +0 -191
- package/docs/runner.md +0 -104
- package/docs/sandbox.md +0 -276
- package/docs/scoring.md +0 -119
- package/docs/source-map.md +0 -126
- package/docs/tier-sync.md +0 -193
- package/docs/view.md +0 -194
- package/docs/vision.md +0 -88
package/src/i18n/en.ts
CHANGED
|
@@ -15,7 +15,12 @@ export const en = {
|
|
|
15
15
|
"cli.all": "(all)",
|
|
16
16
|
"cli.browserOpenFailed": "Could not open the browser automatically. Open manually: {{url}}\n",
|
|
17
17
|
"cli.clean.done": "Deleted .niceeval/ historical run artifacts.\n",
|
|
18
|
-
"cli.config.missing":
|
|
18
|
+
"cli.config.missing":
|
|
19
|
+
"Could not find niceeval.config.ts.\n" +
|
|
20
|
+
"Ways to fix:\n" +
|
|
21
|
+
" - [init] Run `npx niceeval init` to scaffold niceeval.config.ts and evals/\n" +
|
|
22
|
+
" - [cd] Run from the project root that contains niceeval.config.ts\n" +
|
|
23
|
+
" Docs: node_modules/niceeval/docs-site/quickstart.mdx",
|
|
19
24
|
"cli.config.noDefault": "niceeval.config.ts must default export defineConfig(...).",
|
|
20
25
|
"cli.dry.header": "\n[dry] {{evals}} evals × {{configs}} run configs:\n",
|
|
21
26
|
"cli.dry.noMatches": "(no matches)",
|
|
@@ -25,7 +30,8 @@ export const en = {
|
|
|
25
30
|
"runner.budgetUnenforceable":
|
|
26
31
|
"budget for {{budgetKey}}: several attempts completed without any cost data (agent reports no usage and the model is not in the price table) — the budget cannot be enforced for this agent; continuing without the guard.\n",
|
|
27
32
|
"judge.modelMissing":
|
|
28
|
-
"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)
|
|
33
|
+
"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" +
|
|
34
|
+
" Docs: node_modules/niceeval/docs-site/guides/scoring-guide.mdx",
|
|
29
35
|
"loaders.yamlMissing":
|
|
30
36
|
"loadYaml(\"{{path}}\") needs a YAML parser: run `pnpm add yaml` first (or switch to loadJson with a JSON dataset).",
|
|
31
37
|
"cli.flag.parseError": "{{message}}\nRun `niceeval --help` for usage.\n",
|
|
@@ -55,13 +61,16 @@ export const en = {
|
|
|
55
61
|
"cli.experimentGroup": " group",
|
|
56
62
|
"cli.fallbackCleanupTimeout": "\ngraceful cleanup timed out; force-cleaning sandboxes...\n",
|
|
57
63
|
"cli.forceCleanupExit": "\nForce-cleaning sandboxes and exiting...\n",
|
|
58
|
-
"cli.init.done": "
|
|
64
|
+
"cli.init.done": "Ready: evals/, niceeval.config.ts, and the niceeval agent-rules block in AGENTS.md (points coding agents at node_modules/niceeval/docs-site).\n",
|
|
59
65
|
"cli.interruptCleanup": "\nInterrupted; cleaning up sandbox containers... (press again to force cleanup and exit)\n",
|
|
60
66
|
"cli.list.header": "Discovered {{count}} evals:\n",
|
|
61
67
|
"cli.noAgent": "No agent specified (use --agent <name>).\n",
|
|
62
68
|
"cli.none": "(none)",
|
|
63
69
|
"cli.pressCtrlC": "Press Ctrl+C to exit.\n",
|
|
64
|
-
"cli.
|
|
70
|
+
"cli.resultsPath": "Structured results: {{path}} (each result's artifactsDir holds that attempt's events.json / trace.json / diff.json)\n",
|
|
71
|
+
"cli.run.experimentRequired":
|
|
72
|
+
"Run evals through an experiment: use `niceeval exp [group|config] [eval id prefix]`.\n" +
|
|
73
|
+
" Docs: node_modules/niceeval/docs-site/guides/write-experiment.mdx\n",
|
|
65
74
|
"cli.run.experimentRequiredHint": "Hint: \"{{pattern}}\" is an experiment{{kind}}; you probably meant: niceeval exp {{pattern}}\n",
|
|
66
75
|
"cli.run.experimentRequiredKnown": "Discovered experiments: {{experiments}}\n",
|
|
67
76
|
"cli.sandboxFlagRemoved": "`--sandbox` is not a CLI flag. Set `sandbox` in the experiment (or `niceeval.config.ts` as a project-wide fallback) to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").\n",
|
|
@@ -70,7 +79,8 @@ export const en = {
|
|
|
70
79
|
"cli.view.incompatible": "{{dir}}: written by niceeval {{producer}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nRun `{{command}}` to view it.\n",
|
|
71
80
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
72
81
|
"context.capabilityMissing":
|
|
73
|
-
"Agent \"{{agent}}\" is not sandbox-backed (built with defineSandboxAgent), so t.{{method}} is unavailable. Use an agent built with defineSandboxAgent, or drop this assertion
|
|
82
|
+
"Agent \"{{agent}}\" is not sandbox-backed (built with defineSandboxAgent), so t.{{method}} is unavailable. Use an agent built with defineSandboxAgent, or drop this assertion.\n" +
|
|
83
|
+
" Docs: node_modules/niceeval/docs-site/guides/sandbox-agent.mdx",
|
|
74
84
|
"context.skipEmpty": "skip() requires a non-empty reason.",
|
|
75
85
|
"context.turnFailed": "This send returned failed (turn status = failed): {{message}}",
|
|
76
86
|
"context.turnFailedDefault": "This send returned failed (turn status = failed)",
|
|
@@ -99,6 +109,7 @@ export const en = {
|
|
|
99
109
|
"judge.httpError": "judge HTTP {{status}}: {{body}}",
|
|
100
110
|
"judge.probeFailed": "judge precheck failed ({{model}}): {{error}}",
|
|
101
111
|
"judge.probeMissingKey": "judge model {{model}} is missing an API key; configure {{envHint}}",
|
|
112
|
+
"live.more": "… {{hidden}} more ({{running}} running · {{waiting}} waiting · {{done}} done)",
|
|
102
113
|
"live.running": " Running {{totalRuns}} attempts ({{evals}} evals × {{configs}} configs, concurrency {{concurrency}}) {{completed}}/{{total}} done",
|
|
103
114
|
"live.runningUnknown": " Running... {{completed}}/{{total}} done",
|
|
104
115
|
"live.waiting": "waiting for a slot...",
|
|
@@ -159,7 +170,9 @@ export const en = {
|
|
|
159
170
|
"runner.resumeCarryDetail": " carried [{{experiment}}] {{evals}}\n",
|
|
160
171
|
"runner.useRemoteAgent": "using remote agent (no sandbox created)...",
|
|
161
172
|
"sandbox.backendNotImplemented": "{{backend}} sandbox backend is not implemented; use docker, vercel, or e2b",
|
|
162
|
-
"sandbox.missingSpec":
|
|
173
|
+
"sandbox.missingSpec":
|
|
174
|
+
"sandbox agent needs a sandbox, but none was given. niceeval no longer picks a default — set `sandbox` in defineExperiment()/defineConfig() to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").\n" +
|
|
175
|
+
" Docs: node_modules/niceeval/docs-site/guides/sandbox-backends.mdx",
|
|
163
176
|
"sandbox.dependencyMissing.docker": "Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode",
|
|
164
177
|
"sandbox.dependencyMissing.e2b": "E2B sandbox requires 'e2b'. Install it with: pnpm add e2b",
|
|
165
178
|
"sandbox.dependencyMissing.vercel": "Vercel sandbox requires '@vercel/sandbox'. Install it with: pnpm add @vercel/sandbox",
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -13,7 +13,12 @@ export const zhCN = {
|
|
|
13
13
|
"cli.all": "(全部)",
|
|
14
14
|
"cli.browserOpenFailed": "无法自动打开浏览器,请手动访问:{{url}}\n",
|
|
15
15
|
"cli.clean.done": "已删除 .niceeval/ 历史运行工件。\n",
|
|
16
|
-
"cli.config.missing":
|
|
16
|
+
"cli.config.missing":
|
|
17
|
+
"找不到 niceeval.config.ts。\n" +
|
|
18
|
+
"修法:\n" +
|
|
19
|
+
" - [init] 运行 `npx niceeval init` 生成 niceeval.config.ts 和 evals/\n" +
|
|
20
|
+
" - [cd] 切到包含 niceeval.config.ts 的项目根再运行\n" +
|
|
21
|
+
" 文档:node_modules/niceeval/docs-site/zh/quickstart.mdx",
|
|
17
22
|
"cli.config.noDefault": "niceeval.config.ts 需要 default export(defineConfig(...))。",
|
|
18
23
|
"cli.dry.header": "\n[dry] {{evals}} 个 eval × {{configs}} 个运行配置:\n",
|
|
19
24
|
"cli.dry.noMatches": "(无匹配)",
|
|
@@ -23,7 +28,8 @@ export const zhCN = {
|
|
|
23
28
|
"runner.budgetUnenforceable":
|
|
24
29
|
"{{budgetKey}} 的 budget:连续多个 attempt 完成后都拿不到成本数据(agent 不上报用量且模型不在价格表)——该 agent 的 budget 无法执行,取消护栏继续跑。\n",
|
|
25
30
|
"judge.modelMissing":
|
|
26
|
-
"judge 未配置模型:在 defineConfig({ judge: { model: \"...\" } })、eval 的 judge 配置或环境变量 NICEEVAL_JUDGE_MODEL 里指定评判模型(没有内置默认模型)
|
|
31
|
+
"judge 未配置模型:在 defineConfig({ judge: { model: \"...\" } })、eval 的 judge 配置或环境变量 NICEEVAL_JUDGE_MODEL 里指定评判模型(没有内置默认模型)。\n" +
|
|
32
|
+
" 文档:node_modules/niceeval/docs-site/zh/guides/scoring-guide.mdx",
|
|
27
33
|
"loaders.yamlMissing":
|
|
28
34
|
"loadYaml(\"{{path}}\") 需要 YAML 解析器:请先 `pnpm add yaml`(或改用 loadJson + JSON 数据集)。",
|
|
29
35
|
"cli.flag.parseError": "{{message}}\n运行 `niceeval --help` 查看用法。\n",
|
|
@@ -52,13 +58,16 @@ export const zhCN = {
|
|
|
52
58
|
"cli.experimentGroup": "组",
|
|
53
59
|
"cli.fallbackCleanupTimeout": "\ngraceful 清理超时,强制清理沙箱…\n",
|
|
54
60
|
"cli.forceCleanupExit": "\n强制清理沙箱并退出…\n",
|
|
55
|
-
"cli.init.done": "
|
|
61
|
+
"cli.init.done": "已就绪:evals/、niceeval.config.ts,以及 AGENTS.md 里的 niceeval agent 指引区块(指向 node_modules/niceeval/docs-site)。\n",
|
|
56
62
|
"cli.interruptCleanup": "\n收到中断,正在清理沙箱容器…(再按一次强制清理并退出)\n",
|
|
57
63
|
"cli.list.header": "发现 {{count}} 个 eval:\n",
|
|
58
64
|
"cli.noAgent": "未指定 agent(用 --agent <name>)。\n",
|
|
59
65
|
"cli.none": "(无)",
|
|
60
66
|
"cli.pressCtrlC": "按 Ctrl+C 退出。\n",
|
|
61
|
-
"cli.
|
|
67
|
+
"cli.resultsPath": "结构化结果:{{path}}(每条结果的 artifactsDir 下是该次 attempt 的 events.json / trace.json / diff.json)\n",
|
|
68
|
+
"cli.run.experimentRequired":
|
|
69
|
+
"运行 eval 必须通过 experiment:用 `niceeval exp [实验组|配置] [eval id 前缀]`。\n" +
|
|
70
|
+
" 文档:node_modules/niceeval/docs-site/zh/guides/write-experiment.mdx\n",
|
|
62
71
|
"cli.run.experimentRequiredHint": "提示:\"{{pattern}}\" 是实验{{kind}},你大概想跑:niceeval exp {{pattern}}\n",
|
|
63
72
|
"cli.run.experimentRequiredKnown": "已发现实验:{{experiments}}\n",
|
|
64
73
|
"cli.sandboxFlagRemoved": "`--sandbox` 不是 CLI flag。请在 experiment(或 niceeval.config.ts 做全项目兜底)里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。\n",
|
|
@@ -67,7 +76,8 @@ export const zhCN = {
|
|
|
67
76
|
"cli.view.incompatible": "{{dir}}: 由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n运行 `{{command}}` 查看这份报告。\n",
|
|
68
77
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
69
78
|
"context.capabilityMissing":
|
|
70
|
-
"agent \"{{agent}}\" 不是沙箱型(defineSandboxAgent 构造),t.{{method}} 这类断言只有沙箱型 agent 可用。换用 defineSandboxAgent 构造的 agent
|
|
79
|
+
"agent \"{{agent}}\" 不是沙箱型(defineSandboxAgent 构造),t.{{method}} 这类断言只有沙箱型 agent 可用。换用 defineSandboxAgent 构造的 agent,或去掉这条断言。\n" +
|
|
80
|
+
" 文档:node_modules/niceeval/docs-site/zh/guides/sandbox-agent.mdx",
|
|
71
81
|
"context.skipEmpty": "skip() 需要一个非空理由。",
|
|
72
82
|
"context.turnFailed": "本轮 send 返回 failed(turn status = failed):{{message}}",
|
|
73
83
|
"context.turnFailedDefault": "本轮 send 返回 failed(turn status = failed)",
|
|
@@ -96,6 +106,7 @@ export const zhCN = {
|
|
|
96
106
|
"judge.httpError": "judge HTTP {{status}}: {{body}}",
|
|
97
107
|
"judge.probeFailed": "judge 预检失败({{model}}): {{error}}",
|
|
98
108
|
"judge.probeMissingKey": "judge 模型 {{model}} 缺少 API key —— 请配置 {{envHint}}",
|
|
109
|
+
"live.more": "… 其余 {{hidden}} 项({{running}} 运行中 · {{waiting}} 等待 · {{done}} 已完成)",
|
|
99
110
|
"live.running": " 正在运行 {{totalRuns}} 次 ({{evals}} eval × {{configs}} 配置, 并发 {{concurrency}}) {{completed}}/{{total}} 完成",
|
|
100
111
|
"live.runningUnknown": " 正在运行… {{completed}}/{{total}} 完成",
|
|
101
112
|
"live.waiting": "排队等待中…",
|
|
@@ -156,7 +167,9 @@ export const zhCN = {
|
|
|
156
167
|
"runner.resumeCarryDetail": " 复用 [{{experiment}}] {{evals}}\n",
|
|
157
168
|
"runner.useRemoteAgent": "使用 remote agent(不创建沙箱)…",
|
|
158
169
|
"sandbox.backendNotImplemented": "{{backend}} sandbox backend not implemented; use docker, vercel, or e2b",
|
|
159
|
-
"sandbox.missingSpec":
|
|
170
|
+
"sandbox.missingSpec":
|
|
171
|
+
"沙箱型 agent 需要一个 sandbox,但没有提供。niceeval 不再自动选默认后端——请在 defineExperiment()/defineConfig() 里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。\n" +
|
|
172
|
+
" 文档:node_modules/niceeval/docs-site/zh/guides/sandbox-backends.mdx",
|
|
160
173
|
"sandbox.dependencyMissing.docker": "Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode",
|
|
161
174
|
"sandbox.dependencyMissing.e2b": "E2B sandbox requires 'e2b'. Install it with: pnpm add e2b",
|
|
162
175
|
"sandbox.dependencyMissing.vercel": "Vercel sandbox requires '@vercel/sandbox'. Install it with: pnpm add @vercel/sandbox",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// bub tape 解析回归锁。
|
|
2
|
+
//
|
|
3
|
+
// 背景(2026-07 实测):模型在同一步里「先说话再调工具」时,bub 的 tape 把伴随文本
|
|
4
|
+
// 记在 tool_call 条目的 payload.content 上(修复前上游直接丢弃,transcript 里
|
|
5
|
+
// 每次 send 后面都看不到 AI 回复 —— 只有最后一步纯文本收尾能出现)。
|
|
6
|
+
// 解析器必须把这段文本还原成 assistant message 事件,且顺序在 action.called 之前。
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import { parseBubTranscript } from "./bub.ts";
|
|
10
|
+
|
|
11
|
+
const line = (obj: object) => JSON.stringify(obj);
|
|
12
|
+
|
|
13
|
+
describe("parseBubTranscript", () => {
|
|
14
|
+
it("tool_call 携带 payload.content 时,先 emit assistant message 再 emit action.called", () => {
|
|
15
|
+
const raw = [
|
|
16
|
+
line({ kind: "message", payload: { role: "user", content: "migrate the routes" } }),
|
|
17
|
+
line({
|
|
18
|
+
kind: "tool_call",
|
|
19
|
+
payload: {
|
|
20
|
+
content: "I'll start by listing the pages directory.",
|
|
21
|
+
calls: [
|
|
22
|
+
{ id: "call_1", type: "function", function: { name: "bash", arguments: '{"cmd":"ls pages"}' } },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
line({ kind: "tool_result", payload: { results: ["index.js\nabout.js"] } }),
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
const { events } = parseBubTranscript(raw);
|
|
30
|
+
const types = events.map((e) => e.type);
|
|
31
|
+
expect(types).toEqual(["message", "message", "action.called", "action.result"]);
|
|
32
|
+
|
|
33
|
+
const assistant = events[1];
|
|
34
|
+
expect(assistant).toMatchObject({
|
|
35
|
+
type: "message",
|
|
36
|
+
role: "assistant",
|
|
37
|
+
text: "I'll start by listing the pages directory.",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const call = events[2];
|
|
41
|
+
expect(call).toMatchObject({ type: "action.called", callId: "call_1", name: "bash", tool: "shell" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("tool_call 无 content(存量 tape)不产生幻影 assistant message", () => {
|
|
45
|
+
const raw = [
|
|
46
|
+
line({ kind: "message", payload: { role: "user", content: "do it" } }),
|
|
47
|
+
line({
|
|
48
|
+
kind: "tool_call",
|
|
49
|
+
payload: { calls: [{ id: "call_1", function: { name: "fs_read", arguments: '{"path":"a.ts"}' } }] },
|
|
50
|
+
}),
|
|
51
|
+
line({ kind: "tool_result", payload: { results: ["file body"] } }),
|
|
52
|
+
].join("\n");
|
|
53
|
+
|
|
54
|
+
const { events } = parseBubTranscript(raw);
|
|
55
|
+
expect(events.map((e) => e.type)).toEqual(["message", "action.called", "action.result"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("tool_call 的 content 为块数组时也能抽出文本", () => {
|
|
59
|
+
const raw = line({
|
|
60
|
+
kind: "tool_call",
|
|
61
|
+
payload: {
|
|
62
|
+
content: [{ type: "text", text: "Checking the config first." }],
|
|
63
|
+
calls: [{ id: "call_1", function: { name: "bash", arguments: "{}" } }],
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { events } = parseBubTranscript(raw);
|
|
68
|
+
expect(events[0]).toMatchObject({ type: "message", role: "assistant", text: "Checking the config first." });
|
|
69
|
+
expect(events[1]).toMatchObject({ type: "action.called", callId: "call_1" });
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/o11y/parsers/bub.ts
CHANGED
|
@@ -163,6 +163,11 @@ export function parseBubTranscript(raw: string | undefined): ParsedTranscript {
|
|
|
163
163
|
const text = extractText(get(payload, "content") ?? get(payload, "text"));
|
|
164
164
|
if (text) events.push({ type: "message", role, text });
|
|
165
165
|
} else if (kind === "tool_call") {
|
|
166
|
+
// 模型「先说话再调工具」:伴随文本记在 tool_call 的 payload.content 上
|
|
167
|
+
// (bub 不给它单独的 message 条目)。先还原成 assistant message,再吐 calls,
|
|
168
|
+
// 否则 transcript 里每次 send 后面都看不到 AI 回复。
|
|
169
|
+
const accompanying = extractText(get(payload, "content"));
|
|
170
|
+
if (accompanying) events.push({ type: "message", role: "assistant", text: accompanying });
|
|
166
171
|
const calls = get(payload, "calls");
|
|
167
172
|
const list = Array.isArray(calls) ? calls : [payload];
|
|
168
173
|
lastCallIds = [];
|
package/src/o11y/types.ts
CHANGED
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
|
|
4
4
|
import type { JsonValue, SourceLoc } from "../shared/types.ts";
|
|
5
5
|
|
|
6
|
-
/** 一次运行的 token 用量(沙箱型从 transcript
|
|
6
|
+
/** 一次运行的 token 用量(沙箱型从 transcript/OTel span 的 `gen_ai.usage.*` 属性抠,remote 由 send 的 `Turn.usage` 直接返回)。 */
|
|
7
7
|
export interface Usage {
|
|
8
|
+
/** 输入(prompt)token 数,不含缓存命中部分。 */
|
|
8
9
|
inputTokens: number;
|
|
10
|
+
/** 输出(completion)token 数。 */
|
|
9
11
|
outputTokens: number;
|
|
12
|
+
/** 命中 prompt 缓存、按缓存价读取的 token 数(省略表示该 agent 不上报此项)。 */
|
|
10
13
|
cacheReadTokens?: number;
|
|
14
|
+
/** 写入 prompt 缓存的 token 数(省略表示该 agent 不上报此项)。 */
|
|
11
15
|
cacheWriteTokens?: number;
|
|
16
|
+
/** 本次运行触发的模型请求次数(多轮/重试可能大于 1)。 */
|
|
12
17
|
requests?: number;
|
|
13
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* 网关/adapter 实测的真实美元成本(只能由 `Turn.usage.costUSD` 显式带回,从不从
|
|
20
|
+
* token 用量或 OTel span 反推得到)。存在时优先于按价格表(`defineConfig({ pricing })`)
|
|
21
|
+
* 估算的成本——见 `estimateCost` 的 `usage.costUSD ?? estimateCost(...)` 兜底顺序。
|
|
22
|
+
*/
|
|
14
23
|
costUSD?: number;
|
|
15
24
|
}
|
|
16
25
|
|
|
@@ -28,12 +37,19 @@ export type ToolName =
|
|
|
28
37
|
| "agent_task"
|
|
29
38
|
| "unknown";
|
|
30
39
|
|
|
40
|
+
/** HITL 停轮请求的结构化描述,供 `t.requireInputRequest(filter)` / `t.respondAll` 按条件匹配。 */
|
|
31
41
|
export interface InputRequest {
|
|
42
|
+
/** 请求的唯一标识;多个请求并停时,`InputResponse.requestId` 靠它对位。 */
|
|
32
43
|
readonly id?: string;
|
|
44
|
+
/** 模型提出的原始问题/文本(用于 `stringMatches` 过滤)。 */
|
|
33
45
|
readonly prompt?: string;
|
|
46
|
+
/** adapter 自定义的人类可读展示文案,与 `prompt` 二选一或并存,用于 UI/日志展示及过滤匹配。 */
|
|
34
47
|
readonly display?: string;
|
|
48
|
+
/** 请求关联的动作类型(如某个待批准的工具调用名),供过滤用。 */
|
|
35
49
|
readonly action?: string;
|
|
50
|
+
/** 请求携带的结构化输入(如待批准命令的参数)。 */
|
|
36
51
|
readonly input?: JsonValue;
|
|
52
|
+
/** 若请求提供了预设选项(如批准/拒绝),逐项列出;`id` 对应 `InputResponse.optionId`。 */
|
|
37
53
|
readonly options?: readonly { id: string; label?: string }[];
|
|
38
54
|
}
|
|
39
55
|
|
|
@@ -42,19 +58,28 @@ export interface InputRequest {
|
|
|
42
58
|
* 各 agent 五花八门的原始 transcript 映射成 StreamEvent[];映射完,整套断言免费。
|
|
43
59
|
*/
|
|
44
60
|
export type StreamEvent =
|
|
61
|
+
/** 一条文本消息(assistant 回复或 user 输入);`loc` 是可选的源码位置,用于把消息叠回 eval 源码。 */
|
|
45
62
|
| { type: "message"; role: "assistant" | "user"; text: string; loc?: SourceLoc }
|
|
63
|
+
/** 发起一次工具/动作调用;`tool` 是归一化后的规范工具名,原始名保留在 DerivedFacts.ToolCall.originalName。 */
|
|
46
64
|
| { type: "action.called"; callId: string; name: string; input: JsonValue; tool?: ToolName }
|
|
65
|
+
/** 一次工具/动作调用的结果,按 `callId` 与对应的 `action.called` 对位。 */
|
|
47
66
|
| {
|
|
48
67
|
type: "action.result";
|
|
49
68
|
callId: string;
|
|
50
69
|
output?: JsonValue;
|
|
51
70
|
status: "completed" | "failed" | "rejected";
|
|
52
71
|
}
|
|
72
|
+
/** 发起一次子 agent 调用(如 Task 工具、远程 sub-agent);`remoteUrl` 仅远程子 agent 有。 */
|
|
53
73
|
| { type: "subagent.called"; callId: string; name: string; remoteUrl?: string }
|
|
74
|
+
/** 一次子 agent 调用的结果,按 `callId` 与对应的 `subagent.called` 对位。 */
|
|
54
75
|
| { type: "subagent.completed"; callId: string; output?: JsonValue; status: "completed" | "failed" }
|
|
76
|
+
/** 模型停下来向人请求输入(HITL);具体请求内容见 InputRequest。 */
|
|
55
77
|
| { type: "input.requested"; request: InputRequest }
|
|
78
|
+
/** 模型的思考/推理文本(非最终回复)。 */
|
|
56
79
|
| { type: "thinking"; text: string }
|
|
80
|
+
/** 上下文被压缩/摘要(如超长会话截断历史);`reason` 是可选的压缩原因说明。 */
|
|
57
81
|
| { type: "compaction"; reason?: string }
|
|
82
|
+
/** 运行中出现的错误。 */
|
|
58
83
|
| { type: "error"; message: string };
|
|
59
84
|
|
|
60
85
|
/** core 从事件流折叠出的结构化事实(deriveRunFacts)。 */
|
|
@@ -20,12 +20,15 @@ function producerVersion(): Promise<string | undefined> {
|
|
|
20
20
|
return producerVersionPromise;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** 一个 attempt 的工件子目录(相对 run 根):<evalId>/<agent>/<model
|
|
23
|
+
/** 一个 attempt 的工件子目录(相对 run 根):<evalId>/<agent>/<model>[/<experiment>]/a<attempt>。 */
|
|
24
24
|
function attemptDir(r: EvalResult): string {
|
|
25
25
|
const safe = (s: string) => s.replace(/[^\w.@-]/g, "_");
|
|
26
26
|
// evalId 里的 / 保留作目录层级,其余危险字符替换。
|
|
27
27
|
const id = r.id.replace(/[^\w./@-]/g, "_");
|
|
28
|
-
|
|
28
|
+
// experiment 段:两个实验可以同 agent 同 model、只差 flags(feature A/B),没有这一段
|
|
29
|
+
// 它们的工件会互相覆盖。experimentId 里的 / 不作层级(压成 _),整个实验一格。
|
|
30
|
+
const exp = r.experimentId ? `/${safe(r.experimentId)}` : "";
|
|
31
|
+
return `${id}/${safe(r.agent)}/${safe(r.model ?? "default")}${exp}/a${r.attempt}`;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/** summary.json 用的瘦身结果:去掉大数组(events/trace/o11y/diff/sources),换成目录引用 + 存在标记。 */
|
|
@@ -51,7 +54,10 @@ function slimResult(r: EvalResult): EvalResult {
|
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
/** Artifacts 报告器额外暴露输出目录:CLI 在 run 结束时打出 summary.json 路径给 agent 直读。 */
|
|
58
|
+
export type ArtifactsReporter = Reporter & { outputDir(): string };
|
|
59
|
+
|
|
60
|
+
export function Artifacts(root = ".niceeval"): ArtifactsReporter {
|
|
55
61
|
let outputDir = "";
|
|
56
62
|
const ensureDir = async (): Promise<void> => {
|
|
57
63
|
if (!outputDir) outputDir = join(root, safeTimestamp(new Date()));
|
|
@@ -59,6 +65,8 @@ export function Artifacts(root = ".niceeval"): Reporter {
|
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
return {
|
|
68
|
+
outputDir: () => outputDir,
|
|
69
|
+
|
|
62
70
|
async onRunStart() {
|
|
63
71
|
outputDir = join(root, safeTimestamp(new Date()));
|
|
64
72
|
await mkdir(outputDir, { recursive: true });
|
|
@@ -102,20 +102,57 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
102
102
|
return `\x1B[2K${hdr}`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// 终端放不下全部行时,光标回跳会被屏幕顶端截断,导致每帧往下追加整表。
|
|
106
|
+
// 所以按终端高度截断:优先显示运行中的行,放不下的折叠成一行摘要。
|
|
107
|
+
function frameLines(frame: number): string[] {
|
|
108
|
+
const lines = [renderHeader()];
|
|
109
|
+
const termRows = process.stderr.rows || 30;
|
|
110
|
+
// 预留:1 行表头 + 1 行防止末尾换行触发滚动
|
|
111
|
+
const budget = Math.max(1, termRows - 2);
|
|
112
|
+
|
|
113
|
+
if (keyOrder.length <= budget) {
|
|
114
|
+
for (const k of keyOrder) lines.push(renderRow(stateMap.get(k)!, frame));
|
|
115
|
+
return lines;
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
const running: string[] = [];
|
|
119
|
+
const waiting: string[] = [];
|
|
120
|
+
const done: string[] = [];
|
|
121
|
+
for (const k of keyOrder) {
|
|
122
|
+
const s = stateMap.get(k)!;
|
|
123
|
+
if (s.completed >= s.total) done.push(k);
|
|
124
|
+
else if (s.started) running.push(k);
|
|
125
|
+
else waiting.push(k);
|
|
126
|
+
}
|
|
127
|
+
// 选出要显示的 key(运行中 > 等待 > 已完成),但按原始顺序渲染,避免行来回跳动
|
|
128
|
+
const shown = new Set([...running, ...waiting, ...done].slice(0, budget - 1));
|
|
113
129
|
for (const k of keyOrder) {
|
|
114
|
-
|
|
130
|
+
if (shown.has(k)) lines.push(renderRow(stateMap.get(k)!, frame));
|
|
115
131
|
}
|
|
132
|
+
lines.push(
|
|
133
|
+
`\x1B[2K ${t("live.more", {
|
|
134
|
+
hidden: keyOrder.length - shown.size,
|
|
135
|
+
running: running.filter((k) => !shown.has(k)).length,
|
|
136
|
+
waiting: waiting.filter((k) => !shown.has(k)).length,
|
|
137
|
+
done: done.filter((k) => !shown.has(k)).length,
|
|
138
|
+
})}`,
|
|
139
|
+
);
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
116
142
|
|
|
143
|
+
function draw(frame: number) {
|
|
144
|
+
if (!process.stderr.isTTY) return;
|
|
145
|
+
|
|
146
|
+
const lines = frameLines(frame);
|
|
147
|
+
let out = drawnLines > 0 ? `\x1B[${drawnLines}A` : "";
|
|
148
|
+
out += lines.join("\n") + "\n";
|
|
149
|
+
// 本帧比上帧短(行完成后折叠、终端拉高)时,清掉下方残留的旧行
|
|
150
|
+
const extra = drawnLines - lines.length;
|
|
151
|
+
if (extra > 0) {
|
|
152
|
+
out += "\x1B[2K\n".repeat(extra) + `\x1B[${extra}A`;
|
|
153
|
+
}
|
|
117
154
|
process.stderr.write(out);
|
|
118
|
-
drawnLines =
|
|
155
|
+
drawnLines = lines.length;
|
|
119
156
|
}
|
|
120
157
|
|
|
121
158
|
function clearDisplay() {
|
package/src/runner/run.ts
CHANGED
|
@@ -9,9 +9,9 @@ import { t } from "../i18n/index.ts";
|
|
|
9
9
|
import { cacheKey, computeFingerprint } from "./fingerprint.ts";
|
|
10
10
|
import { OtelReceiverPool } from "../o11y/otlp/turn-otel.ts";
|
|
11
11
|
import { runAttemptEffect } from "./attempt.ts";
|
|
12
|
-
import { runReporter, emitReporterEvent, summarize } from "./report.ts";
|
|
13
|
-
import type { Agent, EvalResult, JudgeConfig, RunShape, RunSummary } from "../types.ts";
|
|
14
|
-
import type { Attempt, RunOptions } from "./types.ts";
|
|
12
|
+
import { runReporter, emitReporterEvent, scopeReporter, summarize } from "./report.ts";
|
|
13
|
+
import type { Agent, EvalResult, JudgeConfig, Reporter, RunShape, RunSummary } from "../types.ts";
|
|
14
|
+
import type { AgentRun, Attempt, RunOptions } from "./types.ts";
|
|
15
15
|
|
|
16
16
|
export type { AgentRun, RunOptions } from "./types.ts";
|
|
17
17
|
|
|
@@ -102,7 +102,11 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
102
102
|
for (let i = 0; i < run.runs; i++) {
|
|
103
103
|
for (const evalDef of evals) {
|
|
104
104
|
if (run.experimentId && priorRunKeys.has(`${run.experimentId}|${evalDef.id}`)) continue;
|
|
105
|
-
|
|
105
|
+
// key 标识「同一个运行配置下的同一条 eval」,earlyExit 的跳过/abort 只应作用于
|
|
106
|
+
// 同 key 的重试轮。experimentId 必须进 key:两个实验可以同 agent 同 model、只差
|
|
107
|
+
// flags(feature A/B 正是这种形状),漏掉它会让先过的实验把其它实验的同名 eval
|
|
108
|
+
// 整个跳掉——花了钱还丢结果。
|
|
109
|
+
const key = `${run.experimentId ?? ""}|${run.agent.name}|${run.model ?? ""}|${evalDef.id}`;
|
|
106
110
|
attempts.push({
|
|
107
111
|
evalDef,
|
|
108
112
|
run,
|
|
@@ -159,11 +163,37 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
159
163
|
totalRuns: attempts.length,
|
|
160
164
|
maxConcurrency: opts.maxConcurrency,
|
|
161
165
|
};
|
|
162
|
-
|
|
166
|
+
// eval 级 reporters:实例只观测引用它的 eval(经 scopeReporter 过滤转发)。
|
|
167
|
+
// 已经挂在全局 reporters 里的同一实例不重复挂;同一实例被多个 eval 引用时合并观测集
|
|
168
|
+
// (共享一个目的地,如同一个 Braintrust 实验)。本次没有任何被观测 eval 要跑时整个跳过。
|
|
169
|
+
const scopedSets = new Map<Reporter, Set<string>>();
|
|
170
|
+
for (const e of opts.evals) {
|
|
171
|
+
for (const r of e.reporters ?? []) {
|
|
172
|
+
if (opts.reporters.includes(r)) continue;
|
|
173
|
+
let ids = scopedSets.get(r);
|
|
174
|
+
if (!ids) scopedSets.set(r, (ids = new Set()));
|
|
175
|
+
ids.add(e.id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const reporters: Reporter[] = [...opts.reporters];
|
|
179
|
+
for (const [r, ids] of scopedSets) {
|
|
180
|
+
const scopedRuns = attempts.filter((a) => ids.has(a.evalDef.id)).length;
|
|
181
|
+
if (scopedRuns === 0) continue;
|
|
182
|
+
reporters.push(
|
|
183
|
+
scopeReporter(r, ids, {
|
|
184
|
+
evals: [...ids].filter((id) => runningIds.has(id)).length,
|
|
185
|
+
configs: opts.agentRuns.length,
|
|
186
|
+
totalRuns: scopedRuns,
|
|
187
|
+
maxConcurrency: opts.maxConcurrency,
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const r of reporters) {
|
|
163
193
|
// reporter 只是结果消费方:单个 reporter 抛错记 diagnostic,不能让整次调度崩(P2)。
|
|
164
194
|
await runReporter("onRunStart", () => r.onRunStart?.(runningEvals, firstAgent as Agent, shape));
|
|
165
195
|
}
|
|
166
|
-
await emitReporterEvent(
|
|
196
|
+
await emitReporterEvent(reporters, {
|
|
167
197
|
type: "run:start",
|
|
168
198
|
evals: runningEvals,
|
|
169
199
|
agent: firstAgent as Agent,
|
|
@@ -209,6 +239,18 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
209
239
|
// 未显式指定时跟 maxConcurrency 走——各 backend 的推荐值已在 cli 层写进 maxConcurrency 默认值。
|
|
210
240
|
const sandboxSem = Effect.runSync(Effect.makeSemaphore(opts.maxConcurrency));
|
|
211
241
|
|
|
242
|
+
// 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。
|
|
243
|
+
// 实验级信号量让「有共享状态、必须串行」的实验(如跨 eval 累积记忆,maxConcurrency: 1)
|
|
244
|
+
// 只在自己内部排队,同批其它实验照常并发——旧行为是 CLI 取所有选中实验的最小值钳全局,
|
|
245
|
+
// 一个串行实验会把整批基线拖成串行。等于全局上限的实验级值不建闸(与全局闸重复)。
|
|
246
|
+
const globalSem = Effect.runSync(Effect.makeSemaphore(opts.maxConcurrency));
|
|
247
|
+
const runSems = new Map<AgentRun, Effect.Semaphore>();
|
|
248
|
+
for (const run of opts.agentRuns) {
|
|
249
|
+
if (run.maxConcurrency !== undefined && run.maxConcurrency < opts.maxConcurrency) {
|
|
250
|
+
runSems.set(run, Effect.runSync(Effect.makeSemaphore(Math.max(1, run.maxConcurrency))));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
212
254
|
// 非沙箱 tracing agent 的共享 OTLP 接收池:被测应用是长驻进程,端点不能随
|
|
213
255
|
// attempt 换 —— receiver 粒度跟被测进程走(每 agent 一个,整个 run 复用),run 结束回收。
|
|
214
256
|
if (!opts.otelPool) {
|
|
@@ -224,8 +266,11 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
224
266
|
}
|
|
225
267
|
}
|
|
226
268
|
|
|
227
|
-
// 有界并发调度:
|
|
228
|
-
//
|
|
269
|
+
// 有界并发调度:forEach 本身 unbounded(每个 attempt 立刻有自己的 fiber),真正的
|
|
270
|
+
// 并发上限由上面两级信号量把守——执行体先过实验级闸(若有)再占全局 permit 才开跑。
|
|
271
|
+
// 获取定序恒为 runSem → globalSem,无环等待;实验级闸的持有者在等全局 permit 时
|
|
272
|
+
// 不占别的实验的并发位(并发位就是 globalSem 的 permit,不再是 forEach 的 fiber 槽)。
|
|
273
|
+
// runAttemptEffect 只把「执行错误」收进 EvalResult.error(不 fail),
|
|
229
274
|
// 但中断(Ctrl+C / kill)照常向上传播 —— 所以一条挂掉不会中断其它 attempt,而中断能停掉全部。
|
|
230
275
|
//
|
|
231
276
|
// signal:把 opts.signal 喂给 run → abort 触发根 fiber 中断 → forEach 中断所有子 fiber
|
|
@@ -240,14 +285,15 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
240
285
|
const exit = await Effect.runPromiseExit(
|
|
241
286
|
Effect.forEach(
|
|
242
287
|
attempts,
|
|
243
|
-
(a) =>
|
|
244
|
-
Effect.gen(function* () {
|
|
288
|
+
(a) => {
|
|
289
|
+
const body = Effect.gen(function* () {
|
|
245
290
|
// 早停:同 key 已通过,或已 errored(重跑只会重复同一个框架错误)且开了 earlyExit
|
|
246
|
-
// → 跳过未启动的 attempt
|
|
291
|
+
// → 跳过未启动的 attempt。检查必须在拿到 permit 之后(fiber 是 unbounded 一次性
|
|
292
|
+
// 全建的,建时结果还没出来);跳过路径短暂占一个 permit,可忽略。
|
|
247
293
|
if (a.run.earlyExit && (passedKeys.has(a.key) || erroredKeys.has(a.key))) {
|
|
248
294
|
yield* reportMutex.withPermits(1)(
|
|
249
295
|
Effect.promise(() =>
|
|
250
|
-
emitReporterEvent(
|
|
296
|
+
emitReporterEvent(reporters, {
|
|
251
297
|
type: "run:earlyExit",
|
|
252
298
|
evalId: a.evalDef.id,
|
|
253
299
|
experimentId: a.run.experimentId,
|
|
@@ -270,7 +316,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
270
316
|
budgetReported.add(budgetKey);
|
|
271
317
|
yield* reportMutex.withPermits(1)(
|
|
272
318
|
Effect.promise(() =>
|
|
273
|
-
emitReporterEvent(
|
|
319
|
+
emitReporterEvent(reporters, { type: "run:budgetExceeded", budget, spent: s.spent }),
|
|
274
320
|
),
|
|
275
321
|
);
|
|
276
322
|
}
|
|
@@ -305,7 +351,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
305
351
|
|
|
306
352
|
yield* reportMutex.withPermits(1)(
|
|
307
353
|
Effect.promise(() =>
|
|
308
|
-
emitReporterEvent(
|
|
354
|
+
emitReporterEvent(reporters, {
|
|
309
355
|
type: "eval:start",
|
|
310
356
|
eval: { id: a.evalDef.id },
|
|
311
357
|
agent: a.run.agent,
|
|
@@ -354,7 +400,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
354
400
|
// 停掉后续 attempt(P2)。
|
|
355
401
|
Effect.promise(() =>
|
|
356
402
|
Promise.all(
|
|
357
|
-
|
|
403
|
+
reporters.map((r) =>
|
|
358
404
|
runReporter("onEvalComplete", () => r.onEvalComplete?.(result)),
|
|
359
405
|
),
|
|
360
406
|
),
|
|
@@ -364,10 +410,14 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
364
410
|
// attempt 各自触发的 eval:complete 会绕开 permit=1 直接并发跑,和文档承诺的
|
|
365
411
|
// 「报告回调串行化」不一致。
|
|
366
412
|
yield* reportMutex.withPermits(1)(
|
|
367
|
-
Effect.promise(() => emitReporterEvent(
|
|
413
|
+
Effect.promise(() => emitReporterEvent(reporters, { type: "eval:complete", result })),
|
|
368
414
|
);
|
|
369
|
-
})
|
|
370
|
-
|
|
415
|
+
});
|
|
416
|
+
const gated = globalSem.withPermits(1)(body);
|
|
417
|
+
const runSem = runSems.get(a.run);
|
|
418
|
+
return runSem ? runSem.withPermits(1)(gated) : gated;
|
|
419
|
+
},
|
|
420
|
+
{ concurrency: "unbounded", discard: true },
|
|
371
421
|
).pipe(
|
|
372
422
|
// 中断(用户 Ctrl+C):finalizer 已在中断过程中跑完(容器已停),这里只是把它咽下,
|
|
373
423
|
// 好让流程走到 summarize / onRunComplete,用已完成的 results 出一份部分汇总,而不是抛栈。
|
|
@@ -403,10 +453,10 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
403
453
|
);
|
|
404
454
|
|
|
405
455
|
const summary = summarize(allResults, firstAgent?.name ?? "", startedAt, Date.now() - t0, opts.config.name);
|
|
406
|
-
await emitReporterEvent(
|
|
407
|
-
for (const r of
|
|
456
|
+
await emitReporterEvent(reporters, { type: "run:summary", summary });
|
|
457
|
+
for (const r of reporters) {
|
|
408
458
|
await runReporter("onRunComplete", () => r.onRunComplete?.(summary));
|
|
409
459
|
}
|
|
410
|
-
await emitReporterEvent(
|
|
460
|
+
await emitReporterEvent(reporters, { type: "run:saved", summary });
|
|
411
461
|
return summary;
|
|
412
462
|
}
|