niceeval 0.6.1 → 0.7.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.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +69 -7
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +57 -3
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +32 -24
- package/dist/report/aggregate.js +158 -50
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +93 -160
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +87 -81
- package/dist/report/compute.js +597 -417
- package/dist/report/flag.d.ts +32 -6
- package/dist/report/flag.js +92 -4
- package/dist/report/format.d.ts +19 -11
- package/dist/report/format.js +30 -13
- package/dist/report/index.d.ts +16 -16
- package/dist/report/index.js +20 -21
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +57 -33
- package/dist/report/locale.js +122 -56
- package/dist/report/metrics.d.ts +23 -4
- package/dist/report/metrics.js +110 -25
- package/dist/report/primitives.d.ts +48 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +9 -7
- package/dist/report/react/AttemptList.js +17 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +4 -4
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +10 -0
- package/dist/report/react/ExperimentComparison.js +12 -0
- package/dist/report/react/ExperimentList.d.ts +4 -3
- package/dist/report/react/ExperimentList.js +17 -18
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +59 -28
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +110 -147
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -16
- package/dist/report/report.js +259 -28
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +253 -184
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +38 -7
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +247 -284
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +49 -14
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +4 -2
- package/dist/shared/aggregate.js +8 -7
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
- package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
- package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
- package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
- package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
- package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
- package/docs-site/zh/how-to/publish-report.mdx +105 -0
- package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
- package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
- package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
- package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
- package/docs-site/zh/index.mdx +24 -26
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +32 -5
- package/docs-site/zh/reference/capabilities.mdx +8 -8
- package/docs-site/zh/reference/cli.mdx +40 -12
- package/docs-site/zh/reference/define-agent.mdx +58 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
- package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
- package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
- package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
- package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +10 -2
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +13 -2
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +71 -8
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +446 -124
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +162 -30
- package/src/context/session.test.ts +2 -1
- package/src/context/session.ts +115 -7
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +81 -17
- package/src/i18n/zh-CN.ts +80 -17
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +178 -61
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -279
- package/src/report/compute.ts +723 -491
- package/src/report/dual-render.test.tsx +741 -1024
- package/src/report/flag.ts +104 -12
- package/src/report/format.ts +32 -12
- package/src/report/index.ts +119 -46
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +136 -65
- package/src/report/metrics.ts +108 -25
- package/src/report/primitives.tsx +196 -45
- package/src/report/react/AttemptList.tsx +30 -43
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +73 -0
- package/src/report/react/ExperimentList.tsx +50 -32
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +86 -34
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +89 -5
- package/src/report/react/fixtures.ts +114 -154
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +138 -158
- package/src/report/react/styles.css +243 -82
- package/src/report/report.test.ts +779 -841
- package/src/report/report.ts +423 -41
- package/src/report/text/faces.ts +290 -193
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +44 -7
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +261 -271
- package/src/report/web.ts +63 -20
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +13 -11
- package/src/results/attempt-evidence.ts +20 -13
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +150 -60
- package/src/results/host-equivalence.test.ts +34 -20
- package/src/results/index.ts +12 -4
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +15 -5
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +89 -54
- package/src/results/select.ts +104 -34
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +43 -14
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +243 -37
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +8 -6
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +127 -56
- package/src/show/render.ts +662 -131
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +320 -54
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +423 -11
- package/src/view/app/components/CodeView.tsx +41 -14
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +37 -17
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +10 -3
- package/src/view/data.ts +155 -49
- package/src/view/index.ts +56 -41
- package/src/view/server.ts +37 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +227 -0
- package/src/view/view-report.test.ts +167 -62
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
- package/dist/report/built-ins/experiment-comparison.js +0 -13
- package/dist/report/built-ins/index.d.ts +0 -1
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/docs-site/zh/guides/publish-report.mdx +0 -91
- package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
- package/src/report/built-in-user-parity.test.tsx +0 -640
- package/src/report/built-ins/experiment-comparison.tsx +0 -19
- package/src/report/built-ins/index.ts +0 -2
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// Human profile renderer(见 docs/feature/experiments/cli.md「人在终端里怎么用」)。
|
|
2
|
+
//
|
|
3
|
+
// 两个变体,由 `io.stderr.isTTY` 在构造时选一次(profile 是消费者模型,TTY 只是传输能力 ——
|
|
4
|
+
// 显式 `--output human` 在非 TTY 下仍是 human,只是退化成纯追加文案,不悄悄变成 agent 语义):
|
|
5
|
+
//
|
|
6
|
+
// - TTY:动态 dashboard(命令/elapsed/守恒计数/cost/active slots)覆盖重画,永久事件走
|
|
7
|
+
// clear → append → redraw(coordinator 保证顺序,这里只需正确实现三个钩子)。
|
|
8
|
+
// - 非 TTY:零 ANSI 的追加流 —— 只有 start(plan 永久事件天然充当)、永久事件、以及连续 30
|
|
9
|
+
// 秒无永久事件时的一条 heartbeat;不追踪 active slot,不重画。
|
|
10
|
+
//
|
|
11
|
+
// 两个变体共用同一份「永久事件 → 文本行」的纯函数(renderDurableLines 及其子函数),保证
|
|
12
|
+
// 完成页/失败行/诊断行的实际文案在两种模式下完全一致,只有「要不要用 ANSI 维护一块动态区域」
|
|
13
|
+
// 不同 —— 不是两套平行的文案实现。
|
|
14
|
+
//
|
|
15
|
+
// 完成页(summary/saved 两个永久事件)不再调用 `./reporters/table.ts` 的 `renderRunReport()`
|
|
16
|
+
// 大表:失败优先摘要 + locator + show/view 下一步 + 折叠后的快照路径,完整对比留给
|
|
17
|
+
// `niceeval show` / `niceeval view`(见 docs 的「人看的结束反馈」)。
|
|
18
|
+
|
|
19
|
+
import { t } from "../../i18n/index.ts";
|
|
20
|
+
import { verdictSymbol } from "../reporters/shared.ts";
|
|
21
|
+
import { formatCost } from "../../shared/format.ts";
|
|
22
|
+
import { assertionSummaryLines } from "../../scoring/display.ts";
|
|
23
|
+
import { encodeAttemptKey } from "../types.ts";
|
|
24
|
+
import type {
|
|
25
|
+
ActiveAttempt,
|
|
26
|
+
AttemptKey,
|
|
27
|
+
LifecyclePhase,
|
|
28
|
+
DurableFeedbackEvent,
|
|
29
|
+
RunFeedbackPlan,
|
|
30
|
+
RunFeedbackState,
|
|
31
|
+
} from "../types.ts";
|
|
32
|
+
import type { FeedbackRenderer } from "./renderer.ts";
|
|
33
|
+
import type { FeedbackIO } from "./io.ts";
|
|
34
|
+
|
|
35
|
+
/** 失败/errored 默认展开上限(见 cli.md「'立即追加'也必须有上限」表:human 前 10 条)。 */
|
|
36
|
+
const HUMAN_FAILURE_CAP = 10;
|
|
37
|
+
/** 快照结果路径超过这个数量才折叠成「前 N 个 + … 还有 M 个」,不是 cli.md 的强制数字 ——
|
|
38
|
+
* docs 的两个完成页示例(FAILED / PASSED)对同样 5 条路径给了两种不同的排版,契约本身只要求
|
|
39
|
+
* 「多时折叠,不逐行刷满几十个」,这里选一个单一、可预测的算法同时满足两边。 */
|
|
40
|
+
const RESULTS_PATH_CAP = 3;
|
|
41
|
+
/** 非 TTY human 退化流的空闲 heartbeat 阈值(见 cli.md「什么动态更新,什么逐条追加」表)。 */
|
|
42
|
+
const NON_TTY_HEARTBEAT_IDLE_MS = 30_000;
|
|
43
|
+
/** dashboard 高度预留:避免最后一行触发终端自动滚动(与 live.ts 旧实现的 `rows - 2` 同一动机,
|
|
44
|
+
* 这里只需要给「下一帧」留出一行余地,不需要额外的表头/尾行预留)。 */
|
|
45
|
+
const DASHBOARD_ROW_RESERVE = 1;
|
|
46
|
+
|
|
47
|
+
export interface HumanRendererOptions {
|
|
48
|
+
io: FeedbackIO;
|
|
49
|
+
/** dashboard 首行的命令名(如 "niceeval exp compare");CLI 层按 argv 拼好传入 —— renderer
|
|
50
|
+
* 不解析 argv,不重新发明「这次跑的是什么命令」。 */
|
|
51
|
+
command: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 按 `io.stderr.isTTY` 派发:构造时选一次,运行期不再切换。 */
|
|
55
|
+
export function createHumanRenderer(options: HumanRendererOptions): FeedbackRenderer {
|
|
56
|
+
const { io, command } = options;
|
|
57
|
+
return io.stderr.isTTY ? createDashboardRenderer(io, command) : createPlainRenderer(io);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ───────────────────────── 共享:永久事件 → 文本行(纯函数,两种模式同一份文案) ─────────────────────────
|
|
61
|
+
|
|
62
|
+
/** 一条永久事件 → 待写入的整行文本(不含结尾换行,调用方统一 join("\n") + "\n")。
|
|
63
|
+
* 空数组表示这个事件类型在 human 下没有可见内容(目前没有这种情形,保留以防未来扩展)。 */
|
|
64
|
+
export function renderDurableLines(event: DurableFeedbackEvent, state: RunFeedbackState): string[] {
|
|
65
|
+
switch (event.type) {
|
|
66
|
+
case "plan":
|
|
67
|
+
return buildPlanLines(event.plan);
|
|
68
|
+
case "failure": {
|
|
69
|
+
// 立即追加也要遵守展开上限(见 cli.md「'立即追加'也必须有上限,防止失败风暴重新淹没
|
|
70
|
+
// 输出」)。reducer 已经把这一条计入 state.failures(emit() 先 reduce 再入队),所以
|
|
71
|
+
// freshFailureCount 就是「本次新发生且算上这一条」的累计数;plan 静态注入的复用失败
|
|
72
|
+
// 不消耗流式上限。越过上限
|
|
73
|
+
// 的第一条给一次 suppressed 提示(让人立刻知道开始折叠了);再往后的每一条都静默 ——
|
|
74
|
+
// 不然「追加一次」会变成每条失败都重复一遍「还剩多少条」,完成页的 FAILURES 区块才是
|
|
75
|
+
// 最终准确总数的权威来源。
|
|
76
|
+
const count = state.freshFailureCount;
|
|
77
|
+
if (count <= HUMAN_FAILURE_CAP) return [buildFailureLine(event)];
|
|
78
|
+
if (count === HUMAN_FAILURE_CAP + 1) {
|
|
79
|
+
return [t("feedback.human.suppressedFailures", { count: 1 })];
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
case "diagnostic":
|
|
84
|
+
return buildDiagnosticLines(event, state);
|
|
85
|
+
case "budget-exhausted":
|
|
86
|
+
return [
|
|
87
|
+
`! ${t("feedback.human.budgetExhausted", {
|
|
88
|
+
experimentId: event.experimentId,
|
|
89
|
+
spent: event.spent.toFixed(2),
|
|
90
|
+
unstarted: event.unstarted,
|
|
91
|
+
})}`,
|
|
92
|
+
];
|
|
93
|
+
case "interrupted":
|
|
94
|
+
return [t("runner.interrupted").trimEnd()];
|
|
95
|
+
case "reporter-error":
|
|
96
|
+
return [t("runner.reporterDiagnostic", { stage: event.reporter, message: event.message }).trimEnd()];
|
|
97
|
+
case "kept":
|
|
98
|
+
// 留存授予单条不即时打印;run 摘要后由 buildSummaryLines 汇总成 Kept sandboxes 块
|
|
99
|
+
// (见 docs/feature/sandbox/cli.md「run 收尾输出」)。
|
|
100
|
+
return [];
|
|
101
|
+
case "summary":
|
|
102
|
+
return buildSummaryLines(event, state);
|
|
103
|
+
case "saved":
|
|
104
|
+
return buildSavedLines(event);
|
|
105
|
+
default: {
|
|
106
|
+
// 穷尽性检查:新增 DurableFeedbackEvent 变体时这里编译期报错提醒补上对应分支。
|
|
107
|
+
const exhaustive: never = event;
|
|
108
|
+
return exhaustive;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 把一条永久事件的渲染行写到正确的流(见 docs/feature/experiments/cli.md「输出流和落盘
|
|
114
|
+
* 节奏」的 stream 边界表:human 的 `stdout` 只留给"最终摘要与结果路径",也就是 "summary" /
|
|
115
|
+
* "saved" 这两个事件;计划、失败、诊断等其它永久事件与 dashboard 本身都在 `stderr`)。
|
|
116
|
+
* TTY/非 TTY 两个变体共用这一份判断,不各自重复一遍分支。 */
|
|
117
|
+
function writeDurable(io: FeedbackIO, event: DurableFeedbackEvent, state: RunFeedbackState): void {
|
|
118
|
+
const lines = renderDurableLines(event, state);
|
|
119
|
+
if (lines.length === 0) return;
|
|
120
|
+
const text = `${lines.join("\n")}\n`;
|
|
121
|
+
if (event.type === "summary" || event.type === "saved") io.stdout.write(text);
|
|
122
|
+
else io.stderr.write(text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildPlanLines(plan: RunFeedbackPlan): string[] {
|
|
126
|
+
const lines = [
|
|
127
|
+
t("feedback.human.plan", {
|
|
128
|
+
total: plan.shape.totalRuns,
|
|
129
|
+
evals: plan.shape.evals,
|
|
130
|
+
configs: plan.shape.configs,
|
|
131
|
+
concurrency: plan.shape.maxConcurrency,
|
|
132
|
+
}),
|
|
133
|
+
];
|
|
134
|
+
if (plan.reused > 0) {
|
|
135
|
+
lines.push(t("feedback.human.reuse", {
|
|
136
|
+
reused: plan.reused,
|
|
137
|
+
total: plan.shape.totalRuns,
|
|
138
|
+
toRun: Math.max(0, plan.shape.totalRuns - plan.reused),
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildFailureLine(event: DurableFeedbackEvent & { type: "failure" }): string {
|
|
145
|
+
const phaseSuffix = event.phase ? ` · ${phaseLabel(event.phase)}` : "";
|
|
146
|
+
const summary = event.assertion ? assertionSummaryLines(event.assertion) : [event.reason];
|
|
147
|
+
const body = summary.map((line, index) => `${index === 0 ? " " : " "}${line}`).join("\n");
|
|
148
|
+
return `${verdictSymbol(event.verdict)} ${event.locator} ${event.identity.evalId} [${event.who}]${phaseSuffix}\n${body}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildDiagnosticLines(event: DurableFeedbackEvent & { type: "diagnostic" }, state: RunFeedbackState): string[] {
|
|
152
|
+
// count 从 state.diagnostics 读(reducer 已经按 key 去重累加),不在这里自己维护第二份计数。
|
|
153
|
+
const count = state.diagnostics.find((d) => d.key === event.key)?.count ?? 1;
|
|
154
|
+
const sym = event.severity === "error" ? "✗" : "!";
|
|
155
|
+
const suffix = count > 1 ? ` (${count} attempts)` : "";
|
|
156
|
+
return [`${sym} ${event.key}${suffix}`, ` ${event.message}`];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildSummaryLines(event: DurableFeedbackEvent & { type: "summary" }, state: RunFeedbackState): string[] {
|
|
160
|
+
const { summary, completion } = event;
|
|
161
|
+
const fullReuse = state.total > 0 && state.total === state.reused;
|
|
162
|
+
// required reporter(默认 artifacts、显式 --json/--junit)写失败必须让这行判红——它不是
|
|
163
|
+
// CompletionStatus 的第四个值(那个枚举只有 complete/incomplete/interrupted 三态),但和
|
|
164
|
+
// ci.ts 的 resultStatusWord() 同一个判断顺序:不能让人看到一句会被误读成"全绿"的 PASSED,
|
|
165
|
+
// 而进程实际以非零退出(见 computeCiExitCode 对 reporterErrors 的同一条判断)。
|
|
166
|
+
const verdictWord =
|
|
167
|
+
completion.status === "interrupted"
|
|
168
|
+
? t("feedback.human.resultInterrupted")
|
|
169
|
+
: completion.status === "incomplete"
|
|
170
|
+
? t("feedback.human.resultIncomplete")
|
|
171
|
+
: summary.failed > 0 || summary.errored > 0 || completion.reporterErrors.some((e) => e.required)
|
|
172
|
+
? t("feedback.human.resultFailed")
|
|
173
|
+
: t("feedback.human.resultPassed");
|
|
174
|
+
|
|
175
|
+
const lines: string[] = [
|
|
176
|
+
`${verdictWord} ${t(fullReuse ? "feedback.human.summaryAllReusedLine" : "feedback.human.summaryLine", {
|
|
177
|
+
passed: summary.passed,
|
|
178
|
+
failed: summary.failed,
|
|
179
|
+
errored: summary.errored,
|
|
180
|
+
reused: state.reused,
|
|
181
|
+
})}`,
|
|
182
|
+
` ${formatSummaryDetail(summary.durationMs, state)}`,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// 全通过时(state.failures 为空)不留空 FAILURES 区块。fresh 失败来自 durable event,carry
|
|
186
|
+
// 失败由 plan 静态注入;reducer 把两者按 locator 收进同一清单,这里不从 RunSummary 再造。
|
|
187
|
+
if (state.failures.length > 0) {
|
|
188
|
+
lines.push("", t("feedback.human.failuresHeader"));
|
|
189
|
+
const shown = state.failures.slice(0, HUMAN_FAILURE_CAP);
|
|
190
|
+
for (const f of shown) lines.push(buildFailureLine({ ...f, type: "failure" }));
|
|
191
|
+
if (state.failures.length > HUMAN_FAILURE_CAP) {
|
|
192
|
+
lines.push(t("feedback.human.suppressedFailures", { count: state.failures.length - HUMAN_FAILURE_CAP }));
|
|
193
|
+
}
|
|
194
|
+
// 下钻命令只给第一条失败做示范(cli.md 的完成页示例只有一条失败时展示了一组;多条失败
|
|
195
|
+
// 时逐条重复三行命令会让「有界摘要」变成新的刷屏源,和「不逐条输出」的原则冲突)。
|
|
196
|
+
const first = shown[0];
|
|
197
|
+
if (first) {
|
|
198
|
+
lines.push("", t("feedback.human.inspect", { locator: first.locator }));
|
|
199
|
+
lines.push(t("feedback.human.evalHint", { locator: first.locator }));
|
|
200
|
+
lines.push(t("feedback.human.trace", { locator: first.locator }));
|
|
201
|
+
lines.push(t("feedback.human.diffHint", { locator: first.locator }));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 留存授予块(--keep-sandbox,见 docs/feature/sandbox/cli.md「run 收尾输出」):
|
|
206
|
+
// 每条给 locator(接 niceeval show)、provider 与实例 id、进入现场的命令。
|
|
207
|
+
if (state.kept.length > 0) {
|
|
208
|
+
lines.push("", `Kept sandboxes (${state.kept.length})`);
|
|
209
|
+
for (const k of state.kept) {
|
|
210
|
+
lines.push(` ${k.locator} ${k.identity.evalId} #${k.identity.attempt} ${k.verdict} ${k.provider} · ${k.sandboxId}`);
|
|
211
|
+
lines.push(` enter: niceeval sandbox enter ${k.sandboxId.slice(0, 12)}`);
|
|
212
|
+
}
|
|
213
|
+
lines.push(`Stop them with: niceeval sandbox stop --all`);
|
|
214
|
+
}
|
|
215
|
+
return lines;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildSavedLines(event: DurableFeedbackEvent & { type: "saved" }): string[] {
|
|
219
|
+
const paths = event.paths;
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
const group = deriveResultGroup(paths);
|
|
222
|
+
if (group) lines.push(t("feedback.human.compare", { group }));
|
|
223
|
+
if (paths.length === 0) return lines;
|
|
224
|
+
if (paths.length === 1) {
|
|
225
|
+
lines.push(`${t("feedback.human.resultsHeader")} ${paths[0]}`);
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
lines.push(t("feedback.human.resultsHeader"));
|
|
229
|
+
for (const p of paths.slice(0, RESULTS_PATH_CAP)) lines.push(` ${p}`);
|
|
230
|
+
if (paths.length > RESULTS_PATH_CAP) {
|
|
231
|
+
lines.push(` ${t("feedback.human.resultsMore", { count: paths.length - RESULTS_PATH_CAP })}`);
|
|
232
|
+
}
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 快照路径的共同「组」段(如 `.niceeval/compare/bub-e2b/<snapshot>` → `compare`)。
|
|
237
|
+
* 多个 experiment 组混在同一次 invocation 里(不带 group 前缀的 `niceeval exp` 全量运行)时
|
|
238
|
+
* 组名不唯一,不猜一个可能就 wrong 的 view 目标,直接省略 Compare 行 —— Results 路径仍然完整。 */
|
|
239
|
+
function deriveResultGroup(paths: readonly string[]): string | undefined {
|
|
240
|
+
const groups = new Set<string>();
|
|
241
|
+
for (const p of paths) {
|
|
242
|
+
const seg = p.split("/")[1];
|
|
243
|
+
if (seg) groups.add(seg);
|
|
244
|
+
}
|
|
245
|
+
return groups.size === 1 ? [...groups][0] : undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatSummaryDetail(durationMs: number, state: RunFeedbackState): string {
|
|
249
|
+
const parts = [formatElapsed(durationMs)];
|
|
250
|
+
const fullReuse = state.total > 0 && state.total === state.reused;
|
|
251
|
+
if (fullReuse) {
|
|
252
|
+
parts.push("0 new tok", "$0.00");
|
|
253
|
+
return parts.join(" · ");
|
|
254
|
+
}
|
|
255
|
+
if (state.newTokenCount !== undefined) parts.push(`${formatTokenCount(state.newTokenCount)} new tok`);
|
|
256
|
+
const cost = formatCost(state.estimatedCostUSD);
|
|
257
|
+
if (cost !== "—") parts.push(cost);
|
|
258
|
+
return parts.join(" · ");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ───────────────────────── 共享:纯格式化 helper ─────────────────────────
|
|
262
|
+
|
|
263
|
+
/** "2m 14s" / "54s" 风格,匹配 cli.md 全部 dashboard/完成页示例;`shared/format.ts` 的
|
|
264
|
+
* `formatDuration` 是 "2.3m"/"120ms" 风格,服务的是 view/表格场景,不是这里要的格式。 */
|
|
265
|
+
export function formatElapsed(ms: number): string {
|
|
266
|
+
const totalS = Math.max(0, Math.floor(ms / 1000));
|
|
267
|
+
const m = Math.floor(totalS / 60);
|
|
268
|
+
const s = totalS % 60;
|
|
269
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** "1.2M tok" / "3.4k" 风格;`table.ts` 的 `formatTokens` 只到 k 档,凑不出 cli.md 完成页
|
|
273
|
+
* 示例里的 "1.2M tok"。 */
|
|
274
|
+
export function formatTokenCount(n: number): string {
|
|
275
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
276
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
277
|
+
return String(n);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** LifecyclePhase → Human 展示列的人读投影(见 docs/feature/experiments/cli.md「Attempt 阶段」);
|
|
281
|
+
* 机器面(agent/ci 的 `phase=` 与落盘)保留精确的点分名,收尾段在 Human 侧合并显示为一档。 */
|
|
282
|
+
function phaseLabel(phase: LifecyclePhase): string {
|
|
283
|
+
switch (phase) {
|
|
284
|
+
case "sandbox.queue":
|
|
285
|
+
return t("feedback.phase.sandboxQueue");
|
|
286
|
+
case "sandbox.create":
|
|
287
|
+
return t("feedback.phase.sandboxCreate");
|
|
288
|
+
case "sandbox.setup":
|
|
289
|
+
return t("feedback.phase.sandboxSetup");
|
|
290
|
+
case "workspace.baseline":
|
|
291
|
+
return t("feedback.phase.workspaceBaseline");
|
|
292
|
+
case "eval.setup":
|
|
293
|
+
return t("feedback.phase.evalSetup");
|
|
294
|
+
case "agent.setup":
|
|
295
|
+
return t("feedback.phase.agentSetup");
|
|
296
|
+
case "telemetry.configure":
|
|
297
|
+
return t("feedback.phase.telemetryConfigure");
|
|
298
|
+
case "eval.run":
|
|
299
|
+
case "agent.run": // 嵌套成员:Human 展示不切换顶层阶段
|
|
300
|
+
return t("feedback.phase.evalRun");
|
|
301
|
+
case "workspace.diff":
|
|
302
|
+
return t("feedback.phase.workspaceDiff");
|
|
303
|
+
case "scoring.evaluate":
|
|
304
|
+
return t("feedback.phase.scoring");
|
|
305
|
+
case "telemetry.collect":
|
|
306
|
+
return t("feedback.phase.telemetryCollect");
|
|
307
|
+
case "eval.teardown":
|
|
308
|
+
case "agent.teardown":
|
|
309
|
+
case "sandbox.teardown":
|
|
310
|
+
case "sandbox.suspend":
|
|
311
|
+
case "sandbox.stop":
|
|
312
|
+
return t("feedback.phase.teardown");
|
|
313
|
+
default: {
|
|
314
|
+
const exhaustive: never = phase;
|
|
315
|
+
return exhaustive;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatCounts(state: RunFeedbackState): string {
|
|
321
|
+
const counts = t("feedback.human.counts", {
|
|
322
|
+
total: state.total,
|
|
323
|
+
reused: state.reused,
|
|
324
|
+
running: state.running,
|
|
325
|
+
queued: state.queued,
|
|
326
|
+
completed: state.completed,
|
|
327
|
+
});
|
|
328
|
+
if (state.estimatedCostUSD === undefined || state.estimatedCostUSD <= 0) return counts;
|
|
329
|
+
return `${counts} ${formatCost(state.estimatedCostUSD)}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 命令名靠左、elapsed 靠右对齐到 `columns`;放不下两端对齐时退化成单空格分隔,并按 `columns`
|
|
333
|
+
* 硬截断 —— 不产生软换行(见 checklist「窄终端…不产生软换行」)。 */
|
|
334
|
+
function formatCommandLine(command: string, elapsedMs: number, columns: number): string {
|
|
335
|
+
const elapsed = formatElapsed(elapsedMs);
|
|
336
|
+
const gap = columns - command.length - elapsed.length;
|
|
337
|
+
if (gap >= 1) return command + " ".repeat(gap) + elapsed;
|
|
338
|
+
const line = `${command} ${elapsed}`;
|
|
339
|
+
return truncateNoWrap(line, columns);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function truncateNoWrap(s: string, columns: number): string {
|
|
343
|
+
if (columns <= 0) return "";
|
|
344
|
+
return s.length <= columns ? s : s.slice(0, Math.max(0, columns - 1)) + "…";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function padTrunc(s: string, width: number): string {
|
|
348
|
+
return s.length > width ? s.slice(0, width) : s.padEnd(width);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ───────────────────────── TTY:动态 dashboard ─────────────────────────
|
|
352
|
+
|
|
353
|
+
function createDashboardRenderer(io: FeedbackIO, command: string): FeedbackRenderer {
|
|
354
|
+
// active slot 的稳定顺序:只在这里追加/删除(attempt:start 追加到末尾,
|
|
355
|
+
// attempt:complete/early-exit 删除),phase/detail 变化不改变顺序或成员 ——
|
|
356
|
+
// 可见 attempt 完成前不会因为别的 attempt 更新而换位(checklist「active slots 稳定」)。
|
|
357
|
+
const activeOrder: AttemptKey[] = [];
|
|
358
|
+
// 上一帧写了多少行(供 \x1B[nA 回跳)与上一帧的完整文本(供「同帧不写」判断)。
|
|
359
|
+
let linesDrawn = 0;
|
|
360
|
+
let lastFrameText: string | undefined;
|
|
361
|
+
|
|
362
|
+
function buildFrameLines(state: RunFeedbackState): string[] {
|
|
363
|
+
// 全量复用没有 active attempt,也没有“本次执行中”状态;plan/reuse 与终局摘要已经完整,
|
|
364
|
+
// 不画一块只有 0 running 的 dashboard。
|
|
365
|
+
if (state.total > 0 && state.total === state.reused) return [];
|
|
366
|
+
const columns = io.stderr.columns;
|
|
367
|
+
const lines: string[] = [formatCommandLine(command, state.elapsedMs, columns), formatCounts(state)];
|
|
368
|
+
if (activeOrder.length === 0) return lines.map((l) => truncateNoWrap(l, columns));
|
|
369
|
+
|
|
370
|
+
lines.push("", t("feedback.human.active"));
|
|
371
|
+
const rowBudget = Math.max(0, io.stderr.rows - lines.length - DASHBOARD_ROW_RESERVE);
|
|
372
|
+
const total = activeOrder.length;
|
|
373
|
+
// 窄/矮终端先减 active slots(减少行数),而不是先压缩单行内容 ——
|
|
374
|
+
// 单行内容的截断在 formatActiveRow 里按 columns 单独处理。
|
|
375
|
+
const showCount = total <= rowBudget ? total : Math.max(0, rowBudget - 1);
|
|
376
|
+
for (let i = 0; i < showCount; i++) {
|
|
377
|
+
const active = state.active.get(activeOrder[i]!);
|
|
378
|
+
if (active) lines.push(formatActiveRow(active, io));
|
|
379
|
+
}
|
|
380
|
+
if (total > showCount) {
|
|
381
|
+
lines.push(t("feedback.human.moreActive", { count: total - showCount }));
|
|
382
|
+
}
|
|
383
|
+
// 宽度是硬上限:formatCommandLine/formatActiveRow 已经按 columns 自己算好了,但守恒计数行、
|
|
384
|
+
// "ACTIVE" 标题、overflow 摘要行都是变长文本(i18n 插值后长度不定,数字多位、experiment 名长
|
|
385
|
+
// 都可能超),这里统一兜底截断,不产生软换行。
|
|
386
|
+
return lines.map((l) => truncateNoWrap(l, columns));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function redraw(state: RunFeedbackState): void {
|
|
390
|
+
const lines = buildFrameLines(state);
|
|
391
|
+
const text = lines.join("\n");
|
|
392
|
+
if (text === lastFrameText) return; // 真实内容没变化,不写(checklist「rendered frame 与上一帧相同则不写」)
|
|
393
|
+
if (lines.length === 0) {
|
|
394
|
+
if (linesDrawn > 0) {
|
|
395
|
+
let out = `\x1B[${linesDrawn}A`;
|
|
396
|
+
out += "\x1B[2K\n".repeat(linesDrawn) + `\x1B[${linesDrawn}A`;
|
|
397
|
+
io.stderr.write(out);
|
|
398
|
+
}
|
|
399
|
+
linesDrawn = 0;
|
|
400
|
+
lastFrameText = text;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let out = linesDrawn > 0 ? `\x1B[${linesDrawn}A` : "";
|
|
404
|
+
out += lines.map((l) => `\x1B[2K${l}`).join("\n") + "\n";
|
|
405
|
+
// 本帧比上帧短(行完成后折叠、终端拉高)时,清掉下方残留的旧行,与 live.ts 旧实现同一手法。
|
|
406
|
+
const extra = linesDrawn - lines.length;
|
|
407
|
+
if (extra > 0) out += "\x1B[2K\n".repeat(extra) + `\x1B[${extra}A`;
|
|
408
|
+
io.stderr.write(out);
|
|
409
|
+
linesDrawn = lines.length;
|
|
410
|
+
lastFrameText = text;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
appendDurable(event, state) {
|
|
415
|
+
writeDurable(io, event, state);
|
|
416
|
+
},
|
|
417
|
+
clearDynamic() {
|
|
418
|
+
if (linesDrawn === 0) return; // 幂等:coordinator 收尾时会无条件再调一次
|
|
419
|
+
let out = `\x1B[${linesDrawn}A`;
|
|
420
|
+
out += "\x1B[2K\n".repeat(linesDrawn) + `\x1B[${linesDrawn}A`;
|
|
421
|
+
io.stderr.write(out);
|
|
422
|
+
linesDrawn = 0;
|
|
423
|
+
lastFrameText = undefined; // 物理终端已清空,下一帧必须真的重写,不能被「同帧」判断吞掉
|
|
424
|
+
},
|
|
425
|
+
redrawDynamic(state) {
|
|
426
|
+
redraw(state);
|
|
427
|
+
},
|
|
428
|
+
onTick(_event, state) {
|
|
429
|
+
// coordinator 的 tick 周期(默认 250ms = 4fps)已经是重画的硬上限 —— 这里每个 tick 最多
|
|
430
|
+
// 重画一次;elapsed 按整秒渲染进 formatCommandLine,连同「同帧不写」,自然满足
|
|
431
|
+
// 「elapsed 最多每秒变化一次」而不需要单独的节流变量。
|
|
432
|
+
redraw(state);
|
|
433
|
+
},
|
|
434
|
+
onLifecycle(event) {
|
|
435
|
+
if (event.type === "attempt:start") {
|
|
436
|
+
const key = encodeAttemptKey(event.identity);
|
|
437
|
+
if (!activeOrder.includes(key)) activeOrder.push(key);
|
|
438
|
+
} else if (event.type === "attempt:complete" || event.type === "attempt:early-exit") {
|
|
439
|
+
const key = encodeAttemptKey(event.identity);
|
|
440
|
+
const idx = activeOrder.indexOf(key);
|
|
441
|
+
if (idx !== -1) activeOrder.splice(idx, 1);
|
|
442
|
+
}
|
|
443
|
+
// attempt:phase / attempt:progress 不改变 activeOrder 成员;下一次 tick 的 redraw()
|
|
444
|
+
// 会从 state.active 读到最新 phase/detail,不需要在这里强制重画(见 checklist
|
|
445
|
+
// 「真实 state 变化合并渲染」—— 逐条 lifecycle 事件不各自触发一次重画)。
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** evalId/who 列宽按可用宽度成比例分配(约 55/45),不是固定 26/18 —— 固定宽度在窄终端下
|
|
451
|
+
* 会让整行早早超出 `columns`(违反「宽度以 columns 为硬上限」),给宽终端又会截得比必要更早。
|
|
452
|
+
* 身份列不能吞掉全部剩余宽度:phase/detail 才是 active 行存在的理由,必须预留可见空间。 */
|
|
453
|
+
function formatActiveRow(active: ActiveAttempt, io: FeedbackIO): string {
|
|
454
|
+
const columns = io.stderr.columns;
|
|
455
|
+
const elapsed = formatElapsed(io.clock.now() - active.phaseStartedAt).padStart(6);
|
|
456
|
+
const sym = "● ";
|
|
457
|
+
const fixedWidth = sym.length + elapsed.length + 6; // 6 = 三处两两分隔空格
|
|
458
|
+
// 之前把 `columns - fixedWidth` 全给了 identity,导致 prefix 恒占满一行,detail
|
|
459
|
+
// budget 恒为 0;lifecycle/progress 已进 reducer,却永远无法画到终端。
|
|
460
|
+
const detailReserve = Math.min(80, Math.max(0, Math.floor(columns * 0.35)));
|
|
461
|
+
const remaining = Math.max(0, columns - fixedWidth - detailReserve);
|
|
462
|
+
const evalWidth = Math.max(0, Math.round(remaining * 0.55));
|
|
463
|
+
const whoWidth = Math.max(0, remaining - evalWidth);
|
|
464
|
+
const evalCol = padTrunc(active.identity.evalId, evalWidth);
|
|
465
|
+
const whoCol = padTrunc(active.who, whoWidth);
|
|
466
|
+
const prefix = `${sym}${evalCol} ${whoCol} ${elapsed} `;
|
|
467
|
+
const budget = Math.max(0, columns - prefix.length);
|
|
468
|
+
const detail = active.detail ? `${phaseLabel(active.phase)}: ${active.detail}` : phaseLabel(active.phase);
|
|
469
|
+
return prefix + detail.slice(0, budget);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ───────────────────────── 非 TTY:human 文案的纯追加流 ─────────────────────────
|
|
473
|
+
|
|
474
|
+
function createPlainRenderer(io: FeedbackIO): FeedbackRenderer {
|
|
475
|
+
// 上一条永久事件的时间戳:heartbeat 只在「连续 30 秒没有永久事件」时才追加一条
|
|
476
|
+
//(见 checklist「显式 human + 非 TTY」),failure/diagnostic 出现后立即重新计时。
|
|
477
|
+
let lastDurableAtMs = 0;
|
|
478
|
+
return {
|
|
479
|
+
appendDurable(event, state) {
|
|
480
|
+
lastDurableAtMs = event.at;
|
|
481
|
+
writeDurable(io, event, state);
|
|
482
|
+
},
|
|
483
|
+
onTick(event, state) {
|
|
484
|
+
if (event.at - lastDurableAtMs < NON_TTY_HEARTBEAT_IDLE_MS) return;
|
|
485
|
+
lastDurableAtMs = event.at;
|
|
486
|
+
io.stderr.write(
|
|
487
|
+
`${t("feedback.human.heartbeat", { elapsed: formatElapsed(state.elapsedMs), counts: formatCounts(state) })}\n`,
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
// 没有 clearDynamic/redrawDynamic/activity/onLifecycle:非 TTY 退化流不维护动态区域,
|
|
491
|
+
// 不展示 active attempt 的逐次阶段变化,也不逐次输出 provisioning retry/backoff ——
|
|
492
|
+
// 这些行为由「不实现对应可选钩子」天然满足,不需要在这里写 profile 分支。
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ───────────────────────── `--dry`(human profile):稳定预览,不经 coordinator ─────────────────────────
|
|
497
|
+
|
|
498
|
+
/** 一个 (config, eval) 组合在 `--dry` 预览里的一行;字段形状与 cli.ts 里已有的 dry 数据一一对应,
|
|
499
|
+
* 只是把 `t("cli.dry.row", ...)` 的拼装从 CLI 分支搬进这里 —— dry 预览同样是「展示」,
|
|
500
|
+
* 不该留在 cli.ts(见 docs/feature/experiments/cli.md 与 plan 对「CLI 只负责解析/构造/退出」的
|
|
501
|
+
* 要求)。dry run 不派发 attempt,没有 `RunFeedbackState` 可言,所以这是独立于
|
|
502
|
+
* `FeedbackRenderer`/coordinator 的纯函数,与 `renderAgentPlanEnvelope`(agent.ts)同一定位。 */
|
|
503
|
+
export interface HumanDryPlanRow {
|
|
504
|
+
who: string;
|
|
505
|
+
/** `run.experimentId` 存在时的后缀,如 `" (exp compare/bub-e2b)"`;否则空串。 */
|
|
506
|
+
experimentSuffix: string;
|
|
507
|
+
evalIds: readonly string[];
|
|
508
|
+
runs: number;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export interface HumanDryPlanInput {
|
|
512
|
+
/** 去重前的候选 eval 数(= discover 到、按 `--tag` 过滤后的 eval 总数,与 `cli.dry.header`
|
|
513
|
+
* 历来的口径一致 —— 不是「实际会跑」的去重数,那个概念属于真正开跑时的 `RunFeedbackPlan`)。 */
|
|
514
|
+
evals: number;
|
|
515
|
+
configs: number;
|
|
516
|
+
rows: readonly HumanDryPlanRow[];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 沿用既有 `cli.dry.header`/`cli.dry.row` 文案(见 src/i18n/{en,zh-CN}.ts),逐行列出每个
|
|
520
|
+
* (config, eval) 组合会匹配到哪些 eval —— 与 CI/agent 的 dry 预览不同,human 不折叠、不设行数
|
|
521
|
+
* 上限:这条路径历来就是给人逐行读的完整清单。 */
|
|
522
|
+
export function renderHumanDryPlan(input: HumanDryPlanInput): string {
|
|
523
|
+
const lines = [t("cli.dry.header", { evals: input.evals, configs: input.configs })];
|
|
524
|
+
for (const row of input.rows) {
|
|
525
|
+
lines.push(
|
|
526
|
+
t("cli.dry.row", {
|
|
527
|
+
who: row.who,
|
|
528
|
+
experiment: row.experimentSuffix,
|
|
529
|
+
evals: row.evalIds.join(", ") || t("cli.dry.noMatches"),
|
|
530
|
+
runs: row.runs,
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return lines.join("");
|
|
535
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `niceeval exp` 的 human / agent / ci 反馈模型:公开入口。
|
|
2
|
+
// 见 docs/feature/experiments/cli.md,承重设计见 plan/exp-output-feedback-models.md 第 2-3 节。
|
|
3
|
+
//
|
|
4
|
+
// - profile.ts —— `--output auto` 的纯环境判定。
|
|
5
|
+
// - reducer.ts —— 事件 → RunFeedbackState 的纯 reducer(与本文件同目录,类型见 ../types.ts)。
|
|
6
|
+
// - renderer.ts —— human/agent/ci 各实现一份的插件契约。
|
|
7
|
+
// - coordinator.ts —— 一个 run 内唯一的终端协调者,renderer 的唯一调用方。
|
|
8
|
+
// - sink.ts —— 供尚未拿到 coordinator 引用的底层模块(sandbox provider、budget 记账、
|
|
9
|
+
// reporter 兜底……)使用的迁移出口。
|
|
10
|
+
// - io.ts —— 可注入的终端 I/O 面;testing.ts(仅测试用,不在这个条形码出口里)提供假实现。
|
|
11
|
+
|
|
12
|
+
export { resolveOutputProfile, isCIEnvironment, type OutputProfileFlag, type ResolveOutputProfileInput } from "./profile.ts";
|
|
13
|
+
export { createInitialRunFeedbackState, reduceRunFeedback } from "./reducer.ts";
|
|
14
|
+
export type { FeedbackRenderer } from "./renderer.ts";
|
|
15
|
+
export {
|
|
16
|
+
createFeedbackCoordinator,
|
|
17
|
+
type FeedbackCoordinator,
|
|
18
|
+
type FeedbackCoordinatorOptions,
|
|
19
|
+
} from "./coordinator.ts";
|
|
20
|
+
export {
|
|
21
|
+
reportActivity,
|
|
22
|
+
reportDiagnostic,
|
|
23
|
+
reportInterrupted,
|
|
24
|
+
reportReporterError,
|
|
25
|
+
reportAttemptLifecycle,
|
|
26
|
+
reportFailure,
|
|
27
|
+
reportBudgetExhausted,
|
|
28
|
+
activateFeedbackSink,
|
|
29
|
+
activeFeedbackSinkCount,
|
|
30
|
+
type DiagnosticInput,
|
|
31
|
+
type FailureInput,
|
|
32
|
+
type BudgetExhaustedInput,
|
|
33
|
+
type FeedbackSink,
|
|
34
|
+
} from "./sink.ts";
|
|
35
|
+
export {
|
|
36
|
+
createNodeFeedbackIO,
|
|
37
|
+
type FeedbackIO,
|
|
38
|
+
type FeedbackStream,
|
|
39
|
+
type FeedbackClock,
|
|
40
|
+
type FeedbackTimerHandle,
|
|
41
|
+
} from "./io.ts";
|
|
42
|
+
export {
|
|
43
|
+
createHumanRenderer,
|
|
44
|
+
renderDurableLines,
|
|
45
|
+
renderHumanDryPlan,
|
|
46
|
+
formatElapsed,
|
|
47
|
+
formatTokenCount,
|
|
48
|
+
type HumanRendererOptions,
|
|
49
|
+
type HumanDryPlanInput,
|
|
50
|
+
type HumanDryPlanRow,
|
|
51
|
+
} from "./human.ts";
|
|
52
|
+
export {
|
|
53
|
+
createAgentRenderer,
|
|
54
|
+
renderAgentPlanEnvelope,
|
|
55
|
+
type AgentRendererOptions,
|
|
56
|
+
type AgentDryPlanInput,
|
|
57
|
+
type AgentPlanRow,
|
|
58
|
+
} from "./agent.ts";
|
|
59
|
+
export {
|
|
60
|
+
createCiRenderer,
|
|
61
|
+
computeCiExitCode,
|
|
62
|
+
renderCiDryPlan,
|
|
63
|
+
type CiRendererOptions,
|
|
64
|
+
type CiDryPlanInput,
|
|
65
|
+
type CiDryPlanRow,
|
|
66
|
+
} from "./ci.ts";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// 可注入的终端 I/O 面:feedback coordinator(coordinator.ts)与三种 profile renderer(后续阶段)
|
|
2
|
+
// 只经这里读写 stdout/stderr、探测 TTY/尺寸、拿时钟与定时器 —— 不直接引用全局 `process`。
|
|
3
|
+
//
|
|
4
|
+
// 这样测试能喂一份完全确定性的假 IO(见 testing.ts 的 createFakeFeedbackIO):不靠 monkey-patch
|
|
5
|
+
// `process.stdout`/`process.stderr`/`Date.now`/`setInterval`,断言也不必和真实终端的时序、
|
|
6
|
+
// 尺寸竞争。生产环境用 createNodeFeedbackIO() 包一层真实 process。
|
|
7
|
+
|
|
8
|
+
/** 一路输出流的最小面:coordinator/renderer 只需要写文本、探测是否 TTY 与当前尺寸。 */
|
|
9
|
+
export interface FeedbackStream {
|
|
10
|
+
write(text: string): void;
|
|
11
|
+
readonly isTTY: boolean;
|
|
12
|
+
/** 非 TTY 或探测不到时的合理兜底见各实现;调用方不需要自己再猜省略值。 */
|
|
13
|
+
readonly columns: number;
|
|
14
|
+
readonly rows: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** setInterval 返回句柄的不透明品牌类型 —— 真实实现是 NodeJS.Timeout,测试实现是数字 id,
|
|
18
|
+
* 调用方永远不检视内部结构,只用来传回 clearInterval。 */
|
|
19
|
+
export type FeedbackTimerHandle = { readonly __brand: "FeedbackTimerHandle" };
|
|
20
|
+
|
|
21
|
+
/** coordinator 需要的最小时钟面:当前时间 + 周期定时器。不用 Date.now()/setInterval 直接调用,
|
|
22
|
+
* 好让 reducer 之外唯一还需要「时间」概念的这一层(tick 节奏、heartbeat 空闲判断)也能被
|
|
23
|
+
* 确定性地驱动,不依赖真实挂钟等待。 */
|
|
24
|
+
export interface FeedbackClock {
|
|
25
|
+
now(): number;
|
|
26
|
+
setInterval(fn: () => void, ms: number): FeedbackTimerHandle;
|
|
27
|
+
clearInterval(handle: FeedbackTimerHandle): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** feedback coordinator 依赖的完整 IO 面。三种 profile renderer(human/agent/ci)也应该经这里
|
|
31
|
+
* 读写终端,不直接 import `process` —— 这是「不要把 process 到处传」与「测试不靠全局
|
|
32
|
+
* monkey-patch」两个目标共用的同一个注入点。 */
|
|
33
|
+
export interface FeedbackIO {
|
|
34
|
+
readonly stdout: FeedbackStream;
|
|
35
|
+
readonly stderr: FeedbackStream;
|
|
36
|
+
/** 只读环境变量面(profile 自动检测的 CI 标记、NICEEVAL_LANG 等都从这里读,不直接碰
|
|
37
|
+
* process.env),同样是为了让 profile.ts 的纯函数可测。 */
|
|
38
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
39
|
+
readonly clock: FeedbackClock;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wrapStream(stream: NodeJS.WriteStream): FeedbackStream {
|
|
43
|
+
return {
|
|
44
|
+
write: (text) => {
|
|
45
|
+
stream.write(text);
|
|
46
|
+
},
|
|
47
|
+
get isTTY() {
|
|
48
|
+
return stream.isTTY === true;
|
|
49
|
+
},
|
|
50
|
+
get columns() {
|
|
51
|
+
return stream.columns || 100;
|
|
52
|
+
},
|
|
53
|
+
get rows() {
|
|
54
|
+
return stream.rows || 30;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 生产实现:包一层真实 `process`。setInterval 句柄 `.unref()`—— tick 定时器本身绝不能是
|
|
60
|
+
* 进程活着的理由,`stopDynamic()`/`finish()` 之外的任何异常退出路径也不会被它拖住。 */
|
|
61
|
+
export function createNodeFeedbackIO(): FeedbackIO {
|
|
62
|
+
return {
|
|
63
|
+
stdout: wrapStream(process.stdout),
|
|
64
|
+
stderr: wrapStream(process.stderr),
|
|
65
|
+
env: process.env,
|
|
66
|
+
clock: {
|
|
67
|
+
now: () => Date.now(),
|
|
68
|
+
setInterval: (fn, ms) => {
|
|
69
|
+
const handle = setInterval(fn, ms);
|
|
70
|
+
handle.unref?.();
|
|
71
|
+
return handle as unknown as FeedbackTimerHandle;
|
|
72
|
+
},
|
|
73
|
+
clearInterval: (handle) => {
|
|
74
|
+
clearInterval(handle as unknown as ReturnType<typeof setInterval>);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|