niceeval 0.6.1 → 0.6.2
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/dist/agents/types.d.ts +67 -5
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- 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 +3 -3
- package/dist/report/react/fixtures.js +21 -14
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- package/dist/results/types.d.ts +11 -0
- 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 +1 -0
- package/dist/shared/aggregate.js +3 -3
- 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/concepts/adapter.mdx +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- 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/expect.mdx +26 -1
- package/package.json +5 -1
- 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 +11 -0
- 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/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/types.ts +68 -5
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- 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 +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- 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 +34 -5
- package/src/report/built-in-user-parity.test.tsx +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- 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 +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +8 -6
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- 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 +242 -36
- 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 +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- 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 +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- 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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// 纯 reducer:RunFeedbackEvent → RunFeedbackState。计数、active slot、cost 累计、
|
|
2
|
+
// failure/diagnostic 去重全部只在这里计算 —— 三种 profile 的 renderer(后续阶段实现)只读
|
|
3
|
+
// RunFeedbackState,不各自维护第二份推导,也不解析 message 里的人类文案(结构化字段都在
|
|
4
|
+
// DiagnosticNotice.data / FailureNotice 的具名字段上,见 ../types.ts 的类型注释)。
|
|
5
|
+
//
|
|
6
|
+
// `total = reused + running + queued + completed` 在处理每一个事件之后都成立 —— 见
|
|
7
|
+
// reducer.test.ts 的表驱动用例,每一步都断言这个不变量,不只在流程末尾断言一次。
|
|
8
|
+
//
|
|
9
|
+
// reducer 本身不读 Date.now()、不碰 process.stdout/stderr、不知道 profile 是 human/agent/ci ——
|
|
10
|
+
// 纯函数 (state, event) => state,方便脱离真实 runner/terminal 单测。
|
|
11
|
+
|
|
12
|
+
import type { DiagnosticNotice, FailureNotice, RunFeedbackEvent, RunFeedbackState } from "../types.ts";
|
|
13
|
+
import { encodeAttemptKey } from "../types.ts";
|
|
14
|
+
|
|
15
|
+
/** reducer 的起始状态:一个尚未收到任何事件的 run。 */
|
|
16
|
+
export function createInitialRunFeedbackState(): RunFeedbackState {
|
|
17
|
+
return {
|
|
18
|
+
total: 0,
|
|
19
|
+
reused: 0,
|
|
20
|
+
running: 0,
|
|
21
|
+
queued: 0,
|
|
22
|
+
completed: 0,
|
|
23
|
+
earlyExitSkipped: 0,
|
|
24
|
+
elapsedMs: 0,
|
|
25
|
+
active: new Map(),
|
|
26
|
+
failures: [],
|
|
27
|
+
freshFailureCount: 0,
|
|
28
|
+
diagnostics: [],
|
|
29
|
+
kept: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function reduceRunFeedback(state: RunFeedbackState, event: RunFeedbackEvent): RunFeedbackState {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "plan": {
|
|
36
|
+
const total = event.plan.shape.totalRuns;
|
|
37
|
+
const reused = event.plan.reused;
|
|
38
|
+
// plan 是一次 run 的起点:重置 active/failures/diagnostics,即便 reducer 被复用于
|
|
39
|
+
// 多次 run 也不会把上一次的残留状态带进新 run(正常用法下 plan 本来就应是第一个事件)。
|
|
40
|
+
return {
|
|
41
|
+
...state,
|
|
42
|
+
total,
|
|
43
|
+
reused,
|
|
44
|
+
// 全部非携入 attempt 在此刻就已知会被派发,先计入 queued;后续 attempt:start
|
|
45
|
+
// 逐个把它们移进 running,不必等每条 attempt:queued 事件才累加(见类型注释)。
|
|
46
|
+
queued: Math.max(0, total - reused),
|
|
47
|
+
running: 0,
|
|
48
|
+
completed: 0,
|
|
49
|
+
active: new Map(),
|
|
50
|
+
failures: (event.plan.reusedFailures ?? []).map((failure) => ({ ...failure, at: event.at })),
|
|
51
|
+
freshFailureCount: 0,
|
|
52
|
+
diagnostics: [],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case "tick":
|
|
57
|
+
return { ...state, elapsedMs: event.elapsedMs };
|
|
58
|
+
|
|
59
|
+
case "attempt:queued":
|
|
60
|
+
// 计数已经在 "plan" 时一次性算好(见上),这个事件目前只是为后续阶段预留「单条 attempt
|
|
61
|
+
// 进入排队」的挂点(如展示排队位置),对 RunFeedbackState 不产生任何变化。
|
|
62
|
+
return state;
|
|
63
|
+
|
|
64
|
+
case "attempt:start": {
|
|
65
|
+
const key = encodeAttemptKey(event.identity);
|
|
66
|
+
const active = new Map(state.active);
|
|
67
|
+
active.set(key, {
|
|
68
|
+
identity: event.identity,
|
|
69
|
+
who: event.who,
|
|
70
|
+
phase: event.phase,
|
|
71
|
+
phaseStartedAt: event.at,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
...state,
|
|
75
|
+
queued: state.queued - 1,
|
|
76
|
+
running: state.running + 1,
|
|
77
|
+
active,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "attempt:phase": {
|
|
82
|
+
const key = encodeAttemptKey(event.identity);
|
|
83
|
+
const existing = state.active.get(key);
|
|
84
|
+
if (!existing) return state; // 防御:识别不到的 attempt 静默忽略,不让 renderer 崩
|
|
85
|
+
const active = new Map(state.active);
|
|
86
|
+
// 进入新 phase 清空旧 detail —— 次要文本是绑定到具体 phase 的(如 running 阶段的
|
|
87
|
+
// "tool: shell"),不该原样带进下一个 phase 显示。
|
|
88
|
+
active.set(key, { ...existing, phase: event.phase, phaseStartedAt: event.at, detail: undefined });
|
|
89
|
+
return { ...state, active };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "attempt:progress": {
|
|
93
|
+
const key = encodeAttemptKey(event.identity);
|
|
94
|
+
const existing = state.active.get(key);
|
|
95
|
+
if (!existing) return state;
|
|
96
|
+
const active = new Map(state.active);
|
|
97
|
+
active.set(key, { ...existing, detail: event.detail });
|
|
98
|
+
return { ...state, active };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case "attempt:complete": {
|
|
102
|
+
const key = encodeAttemptKey(event.identity);
|
|
103
|
+
const active = new Map(state.active);
|
|
104
|
+
active.delete(key);
|
|
105
|
+
const estimatedCostUSD =
|
|
106
|
+
event.estimatedCostUSD === undefined
|
|
107
|
+
? state.estimatedCostUSD
|
|
108
|
+
: (state.estimatedCostUSD ?? 0) + event.estimatedCostUSD;
|
|
109
|
+
const newTokenCount =
|
|
110
|
+
event.tokenCount === undefined
|
|
111
|
+
? state.newTokenCount
|
|
112
|
+
: (state.newTokenCount ?? 0) + event.tokenCount;
|
|
113
|
+
return {
|
|
114
|
+
...state,
|
|
115
|
+
running: state.running - 1,
|
|
116
|
+
completed: state.completed + 1,
|
|
117
|
+
active,
|
|
118
|
+
newTokenCount,
|
|
119
|
+
estimatedCostUSD,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "attempt:early-exit":
|
|
124
|
+
// 首过即停下已知 verdict 的省略次数:折进 completed(结论已经确定,不再需要派发),
|
|
125
|
+
// 不产生 failures/diagnostics —— 这不是一次失败或异常,只是省下的重复验证
|
|
126
|
+
// (真正「未完整覆盖」的信号来自 budget-exhausted / fail-fast diagnostic,不是这里)。
|
|
127
|
+
return {
|
|
128
|
+
...state,
|
|
129
|
+
queued: state.queued - 1,
|
|
130
|
+
completed: state.completed + 1,
|
|
131
|
+
earlyExitSkipped: state.earlyExitSkipped + 1,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
case "failure": {
|
|
135
|
+
const isFresh = !state.failures.some((failure) => failure.locator === event.locator);
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
failures: upsertFailure(state.failures, {
|
|
139
|
+
at: event.at,
|
|
140
|
+
locator: event.locator,
|
|
141
|
+
identity: event.identity,
|
|
142
|
+
who: event.who,
|
|
143
|
+
verdict: event.verdict,
|
|
144
|
+
reason: event.reason,
|
|
145
|
+
...(event.assertion !== undefined ? { assertion: event.assertion } : {}),
|
|
146
|
+
...(event.phase !== undefined ? { phase: event.phase } : {}),
|
|
147
|
+
}),
|
|
148
|
+
freshFailureCount: state.freshFailureCount + (isFresh ? 1 : 0),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "diagnostic":
|
|
153
|
+
return {
|
|
154
|
+
...state,
|
|
155
|
+
diagnostics: upsertDiagnostic(state.diagnostics, {
|
|
156
|
+
at: event.at,
|
|
157
|
+
key: event.key,
|
|
158
|
+
severity: event.severity,
|
|
159
|
+
message: event.message,
|
|
160
|
+
identity: event.identity,
|
|
161
|
+
data: event.data,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
case "budget-exhausted":
|
|
166
|
+
// 约定:emitter 对每一个因 budget 到顶而不派发的 attempt 各发一次这个事件(与
|
|
167
|
+
// attempt:early-exit 同构),所以每次触发在这里折进 completed 一次 —— 不去信任
|
|
168
|
+
// event.unstarted 的绝对值来算「这次要挪多少」(那需要 reducer 额外记住上一次的值,
|
|
169
|
+
// 破坏纯 (state, event) => state 的最小状态原则)。event.unstarted / event.spent 仍然
|
|
170
|
+
// 整体写进 diagnostic 的 data,供 agent/ci 直接读取当次快照值;真正的去重计数由
|
|
171
|
+
// upsertDiagnostic 的 count 字段给出,天然等于「目前为止因 budget 未派发的次数」。
|
|
172
|
+
return {
|
|
173
|
+
...state,
|
|
174
|
+
queued: state.queued - 1,
|
|
175
|
+
completed: state.completed + 1,
|
|
176
|
+
diagnostics: upsertDiagnostic(state.diagnostics, {
|
|
177
|
+
at: event.at,
|
|
178
|
+
key: `budget-exhausted:${event.experimentId}`,
|
|
179
|
+
severity: "warning",
|
|
180
|
+
message: `budget exhausted for ${event.experimentId}`,
|
|
181
|
+
data: { experimentId: event.experimentId, spent: event.spent, unstarted: event.unstarted },
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
case "kept":
|
|
186
|
+
// 留存授予的永久通知:run 摘要后各 profile 追加输出(见 docs/feature/sandbox/cli.md)。
|
|
187
|
+
return {
|
|
188
|
+
...state,
|
|
189
|
+
kept: [
|
|
190
|
+
...state.kept,
|
|
191
|
+
{
|
|
192
|
+
at: event.at,
|
|
193
|
+
locator: event.locator,
|
|
194
|
+
identity: event.identity,
|
|
195
|
+
who: event.who,
|
|
196
|
+
verdict: event.verdict,
|
|
197
|
+
provider: event.provider,
|
|
198
|
+
sandboxId: event.sandboxId,
|
|
199
|
+
...(event.enter !== undefined ? { enter: event.enter } : {}),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
case "interrupted":
|
|
205
|
+
return {
|
|
206
|
+
...state,
|
|
207
|
+
diagnostics: upsertDiagnostic(state.diagnostics, {
|
|
208
|
+
at: event.at,
|
|
209
|
+
key: "interrupted",
|
|
210
|
+
severity: "warning",
|
|
211
|
+
message: "run interrupted",
|
|
212
|
+
}),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
case "reporter-error":
|
|
216
|
+
return {
|
|
217
|
+
...state,
|
|
218
|
+
diagnostics: upsertDiagnostic(state.diagnostics, {
|
|
219
|
+
at: event.at,
|
|
220
|
+
key: `reporter-error:${event.reporter}`,
|
|
221
|
+
severity: event.required ? "error" : "warning",
|
|
222
|
+
message: `reporter "${event.reporter}" failed: ${event.message}`,
|
|
223
|
+
data: { reporter: event.reporter, required: event.required },
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
case "summary":
|
|
228
|
+
case "saved":
|
|
229
|
+
// 终局通知:发出时 counts/active 已经由前面的 attempt 事件更新到位,状态本身不再变化——
|
|
230
|
+
// 这两个事件的 payload(summary/completion/paths)由 coordinator 直接使用,不经 reducer
|
|
231
|
+
// 折叠进 RunFeedbackState(RunFeedbackState 是「当前进行中」的 dashboard 状态,不是终局报告)。
|
|
232
|
+
return state;
|
|
233
|
+
|
|
234
|
+
default: {
|
|
235
|
+
// 穷尽性检查:新增 RunFeedbackEvent 变体时,这里会在编译期报错提醒补上对应分支。
|
|
236
|
+
const exhaustive: never = event;
|
|
237
|
+
return exhaustive;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function upsertFailure(failures: readonly FailureNotice[], notice: FailureNotice): readonly FailureNotice[] {
|
|
243
|
+
// 同一 locator 理论上不该出现两次;真出现时按最新一次覆盖(幂等),而不是重复追加同一条失败。
|
|
244
|
+
const idx = failures.findIndex((f) => f.locator === notice.locator);
|
|
245
|
+
if (idx === -1) return [...failures, notice];
|
|
246
|
+
const next = failures.slice();
|
|
247
|
+
next[idx] = notice;
|
|
248
|
+
return next;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function upsertDiagnostic(
|
|
252
|
+
diagnostics: readonly DiagnosticNotice[],
|
|
253
|
+
input: Omit<DiagnosticNotice, "count">,
|
|
254
|
+
): readonly DiagnosticNotice[] {
|
|
255
|
+
const idx = diagnostics.findIndex((d) => d.key === input.key);
|
|
256
|
+
if (idx === -1) return [...diagnostics, { ...input, count: 1 }];
|
|
257
|
+
const next = diagnostics.slice();
|
|
258
|
+
next[idx] = { ...input, count: next[idx].count + 1 };
|
|
259
|
+
return next;
|
|
260
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// FeedbackRenderer:human / agent / ci 三种 profile(见 docs/feature/experiments/cli.md)各实现
|
|
2
|
+
// 一份的插件契约。FeedbackCoordinator(coordinator.ts)是这份契约唯一的调用方 —— renderer 不
|
|
3
|
+
// 知道、也不需要知道事件从哪来、该不该去重、什么时候该 tick,这些全部由 coordinator 保证好
|
|
4
|
+
// 了才调用这里;renderer 只管「拿到一个已经确定要展示的 typed 事件 / 已经更新好的状态,
|
|
5
|
+
// 该画成什么样」。
|
|
6
|
+
//
|
|
7
|
+
// 后续阶段(D=human、E=agent、F=ci)各写一个模块实现这个接口,不需要再回来改这个文件。
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AttemptLifecycleEvent,
|
|
11
|
+
DurableFeedbackEvent,
|
|
12
|
+
FeedbackTickEvent,
|
|
13
|
+
RunFeedbackState,
|
|
14
|
+
} from "../types.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 方法命名对应 docs 里「coordinator 负责:撤下动态区域 → 永久写一行 → 在下方重建」的三步:
|
|
18
|
+
* clearDynamic → appendDurable → redrawDynamic。coordinator 保证三步按序、原子执行(内部单
|
|
19
|
+
* 队列串行处理一个事件的三步,不会被下一个事件插入打断);agent/ci 没有「动态区域」概念,
|
|
20
|
+
* 两个 clear/redraw 钩子留空不实现即可 —— coordinator 探测到未实现就跳过,不会因为可选方法
|
|
21
|
+
* 缺失而报错。
|
|
22
|
+
*/
|
|
23
|
+
export interface FeedbackRenderer {
|
|
24
|
+
/**
|
|
25
|
+
* 一条永久事件落地(见 `DurableFeedbackEvent`):human 打一行、agent 按 envelope 追加、ci 按
|
|
26
|
+
* stdout 事件追加。三种 profile 都必须实现 —— 这是最基本的职责,即便某个 profile 选择对
|
|
27
|
+
* 某个 `event.type` 不输出任何可见内容(如 agent 收到 "saved" 但把路径信息留到最终 handoff
|
|
28
|
+
* 才汇总展示),也要显式接住、不抛错。
|
|
29
|
+
*
|
|
30
|
+
* 同一个逻辑事件(如两次 `key` 相同的 "diagnostic")可能被调用多次 —— coordinator 只保证
|
|
31
|
+
* `RunFeedbackState.diagnostics` 里的去重计数正确,不代为决定「该不该重复打印」;renderer
|
|
32
|
+
* 自己决定重复出现时是刷新一行还是静默吸收计数(human dashboard 通常选后者,见 cli.md
|
|
33
|
+
* 「同一 dedupeKey 并发出现时只留一条并显示次数」—— 在只能追加、不能回改 scrollback 的
|
|
34
|
+
* 终端模型下,这意味着首次之后的调用大多是no-op,只靠 state 里的 count 保证信息不丢)。
|
|
35
|
+
*/
|
|
36
|
+
appendDurable(event: DurableFeedbackEvent, state: RunFeedbackState): void | Promise<void>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 即将落地一条永久事件 / 一条 activity 之前,若当前画着动态区域(human dashboard)先撤下。
|
|
40
|
+
* 返回后 coordinator 紧接着调用 appendDurable(或 activity),再(若仍处于「运行中」阶段)
|
|
41
|
+
* 调用 redrawDynamic —— 三者被 coordinator 视为一个原子操作,renderer 不需要自己处理并发/
|
|
42
|
+
* 交错。agent/ci 不实现即可。**必须幂等**:当前没有画任何动态内容时调用也必须安全(coordinator
|
|
43
|
+
* 在 stopDynamic() 收尾时会无条件再调用一次)。
|
|
44
|
+
*/
|
|
45
|
+
clearDynamic?(): void | Promise<void>;
|
|
46
|
+
|
|
47
|
+
/** appendDurable(或 activity)之后,若动态区域还应该存在,在这里重建(human dashboard 专用)。
|
|
48
|
+
* run 收尾阶段(coordinator.stopDynamic() 之后)不会再调用这个钩子 —— 见 coordinator.ts
|
|
49
|
+
* 的「结束后不允许再重建动态区域」。 */
|
|
50
|
+
redrawDynamic?(state: RunFeedbackState): void | Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 不进入 `RunFeedbackState`、不去重的瞬时活动文本(如 docker 镜像拉取进度、vercel session
|
|
54
|
+
* rotate 成功通知)—— A2 阶段(scoped `ScopedFeedback.progress`)落地前的过渡出口,详见
|
|
55
|
+
* `sink.ts` 的 `reportActivity`。coordinator 仍按 clearDynamic → activity → redrawDynamic
|
|
56
|
+
* 的顺序调用;不实现就等价于「provisioning retry/backoff 不逐次输出」——agent/ci 的目标行为
|
|
57
|
+
* (见 cli.md 信息分级表)天然由「不实现这个可选方法」满足,不需要 renderer 自己判断 profile。
|
|
58
|
+
*/
|
|
59
|
+
activity?(text: string, state: RunFeedbackState): void | Promise<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 运行级时钟 tick(见 `FeedbackTickEvent`):human 用它判断要不要重画 dashboard(自行做
|
|
63
|
+
* 4fps/1Hz 节流 —— coordinator 只保证「这是一次重画机会」,不承诺 tick 间隔恰好等于某个
|
|
64
|
+
* profile 需要的帧率);agent/ci 用它判断距上一次永久事件是否已超过空闲阈值(30s / 60s,
|
|
65
|
+
* coordinator 不知道这两个数字,那是各 renderer 自己维护的常量),超过则自行追加一条
|
|
66
|
+
* heartbeat(通过自己的输出通道,不是再回调 coordinator——heartbeat 不是需要去重/计入
|
|
67
|
+
* RunFeedbackState 的永久事件)。
|
|
68
|
+
*/
|
|
69
|
+
onTick?(event: FeedbackTickEvent, state: RunFeedbackState): void | Promise<void>;
|
|
70
|
+
|
|
71
|
+
/** attempt 生命周期事件(见 `AttemptLifecycleEvent`):human 用它驱动 active slot 的内容;
|
|
72
|
+
* agent/ci 通常不需要实现 —— 它们不逐条展示 active 状态(见 cli.md 的信息分级表)。 */
|
|
73
|
+
onLifecycle?(event: AttemptLifecycleEvent, state: RunFeedbackState): void | Promise<void>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* coordinator 关闭时的最后一次机会 —— 在 `finish()` 内部,已经完成 stopDynamic() 且已经把
|
|
77
|
+
* "summary"/"saved" 两个永久事件送进 appendDurable 之后才调用。之后保证不会再收到任何调用。
|
|
78
|
+
* 适合释放 renderer 自己持有的资源(如未 unref 的句柄);不适合再写任何输出 —— 该写的已经
|
|
79
|
+
* 通过 appendDurable 写完。
|
|
80
|
+
*/
|
|
81
|
+
close?(): void | Promise<void>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// run 激活期间,底层模块(sandbox provider、budget 记账、reporter 兜底、中断处理……)不得
|
|
2
|
+
// 直接写 stdout/stderr(见 docs/feature/experiments/cli.md「输出流和落盘节奏」与本文件对应的
|
|
3
|
+
// PLAN 第 2 节「一个 run 内只有一个终端协调者」)。它们改调用这里的 reportXxx() 系列函数。
|
|
4
|
+
//
|
|
5
|
+
// - 有活跃 FeedbackCoordinator 时(coordinator.start() 之后、finish() 之前,见 cli.ts 的
|
|
6
|
+
// run:start 接线),转发给它 —— coordinator 负责去重、clear→append→redraw 排序、按 profile
|
|
7
|
+
// 分派给 renderer。
|
|
8
|
+
// - 没有活跃 coordinator 时(run:start 之前的 bootstrap 错误、finish() 之后、或不经过
|
|
9
|
+
// cli.ts 的直接库调用),透传回 `src/tty-line.ts` 的 bootstrap 出口。
|
|
10
|
+
//
|
|
11
|
+
// 活跃 coordinator 用一个栈而不是单个全局变量:允许嵌套/连续的 runEvals() 调用(如测试里
|
|
12
|
+
// 同进程跑多个 run,或未来某种「run 之内再跑一个子 run」的场景)各自持有自己的活跃身份,
|
|
13
|
+
// 不会互相覆盖对方的 sink。用显式栈(而不是旧 `tty-line.ts` 那套「广播给所有监听者」的
|
|
14
|
+
// 订阅模式)是因为 reportXxx() 需要一个*唯一*目的地来做去重决策,不能让多个监听者都收到
|
|
15
|
+
// 同一条消息。
|
|
16
|
+
|
|
17
|
+
import { writeStderrLine } from "../../tty-line.ts";
|
|
18
|
+
import { t } from "../../i18n/index.ts";
|
|
19
|
+
import type { AttemptLifecycleEvent, AttemptRef, FailureDetail } from "../types.ts";
|
|
20
|
+
import type { Verdict } from "../../scoring/types.ts";
|
|
21
|
+
import type { JsonValue } from "../../shared/types.ts";
|
|
22
|
+
import type { AttemptLocator } from "../../results/locator.ts";
|
|
23
|
+
|
|
24
|
+
/** `sink.diagnostic()` 的输入 —— 与 `DurableFeedbackEvent` 的 "diagnostic" 变体字段一致,
|
|
25
|
+
* 只是省略 `type`/`at`(由 coordinator 补上)。 */
|
|
26
|
+
export interface DiagnosticInput {
|
|
27
|
+
/** 稳定去重 key —— 同一种 warning/error 用同一个 key(见 cli.md「同一 dedupeKey 并发出现时
|
|
28
|
+
* 只留一条并显示次数」),不要把可变的实例细节(如具体 sandbox id)编进 key 本身,
|
|
29
|
+
* 那些细节放 `data`。 */
|
|
30
|
+
key: string;
|
|
31
|
+
severity: "warning" | "error";
|
|
32
|
+
/** 一句话人类可读摘要;renderer 的 appendDurable 直接展示,不需要再解析。 */
|
|
33
|
+
message: string;
|
|
34
|
+
identity?: AttemptRef;
|
|
35
|
+
data?: Readonly<Record<string, JsonValue>>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `sink.failure()` 的输入 —— 与 `DurableFeedbackEvent` 的 "failure" 变体字段一致,只省略
|
|
39
|
+
* `type`/`at`(由 coordinator 补上)。`locator` 只有在 attempt 挂靠 experiment 时才存在
|
|
40
|
+
* (见 `results/locator.ts` 的 `encodeAttemptLocator`);调用方(run.ts)只在拿到 locator 之后
|
|
41
|
+
* 才应该调用这个函数——没有 locator 的裸 run 不产出这类永久失败通知。 */
|
|
42
|
+
export type FailureInput = FailureDetail;
|
|
43
|
+
|
|
44
|
+
/** `sink.budgetExhausted()` 的输入 —— 与 `DurableFeedbackEvent` 的 "budget-exhausted" 变体字段
|
|
45
|
+
* 一致,只省略 `type`/`at`。调用方(run.ts)对每一个因预算到顶而不派发的 attempt 各调一次
|
|
46
|
+
* (与 `AttemptLifecycleEvent` 的 "attempt:early-exit" 同构);`unstarted` 是调用方自己维护的、
|
|
47
|
+
* 发出这条时的累计未派发数,不是 reducer 能推导的值(见 reducer.ts 对应事件的注释)。 */
|
|
48
|
+
export interface BudgetExhaustedInput {
|
|
49
|
+
experimentId: string;
|
|
50
|
+
spent: number;
|
|
51
|
+
unstarted: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `sink.kept()` 的输入 —— 与 `DurableFeedbackEvent` 的 "kept" 变体字段一致,省略 type/at。 */
|
|
55
|
+
export interface KeptInput {
|
|
56
|
+
locator: AttemptLocator;
|
|
57
|
+
identity: AttemptRef;
|
|
58
|
+
who: string;
|
|
59
|
+
verdict: Verdict;
|
|
60
|
+
provider: string;
|
|
61
|
+
sandboxId: string;
|
|
62
|
+
enter?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `sink.ts` façade 函数实际转发到的最小接口 —— `FeedbackCoordinator`(coordinator.ts)实现它。
|
|
66
|
+
* 定义在这里(而不是从 coordinator.ts 导入)是为了让 sink.ts 不必在运行时依赖 coordinator.ts,
|
|
67
|
+
* 避免两个模块互相 import 造成的循环依赖 —— coordinator.ts 反过来 `import type` 这个接口。 */
|
|
68
|
+
export interface FeedbackSink {
|
|
69
|
+
activity(text: string): void;
|
|
70
|
+
diagnostic(input: DiagnosticInput): void;
|
|
71
|
+
interrupted(): void;
|
|
72
|
+
reporterError(input: { reporter: string; required: boolean; message: string }): void;
|
|
73
|
+
/** 一次失败/errored attempt 的永久通知(见 `FailureInput`)。 */
|
|
74
|
+
failure(input: FailureInput): void;
|
|
75
|
+
/** 一个因预算到顶而未被派发的 attempt(见 `BudgetExhaustedInput`)。 */
|
|
76
|
+
budgetExhausted(input: BudgetExhaustedInput): void;
|
|
77
|
+
/** 一次留存授予(--keep-sandbox);见 `KeptInput` 与 docs/feature/sandbox/cli.md。 */
|
|
78
|
+
kept(input: KeptInput): void;
|
|
79
|
+
/** attempt 生命周期事件(queued/start/phase/progress/complete/early-exit),见
|
|
80
|
+
* `AttemptLifecycleEvent`。只驱动 human dashboard 的 active slot,不落 RunSummary/结果文件,
|
|
81
|
+
* 所以没有活跃 coordinator 时(见 `reportAttemptLifecycle`)静默丢弃是安全的 —— 这类信息
|
|
82
|
+
* 本身就只服务「正在画着的 dashboard」,不是必须留痕的诊断。 */
|
|
83
|
+
lifecycle(event: AttemptLifecycleEvent): void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const activeStack: FeedbackSink[] = [];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* coordinator 进入「活跃」阶段时调用一次(见 coordinator.ts 的 start()),把自己注册为当前
|
|
90
|
+
* reportXxx() 调用的目的地。返回的函数用于退出活跃状态(见 coordinator.ts 的 finish())——
|
|
91
|
+
* 调用一次即失效,重复调用是安全的 no-op。
|
|
92
|
+
*/
|
|
93
|
+
export function activateFeedbackSink(sink: FeedbackSink): () => void {
|
|
94
|
+
activeStack.push(sink);
|
|
95
|
+
let active = true;
|
|
96
|
+
return () => {
|
|
97
|
+
if (!active) return;
|
|
98
|
+
active = false;
|
|
99
|
+
const idx = activeStack.lastIndexOf(sink);
|
|
100
|
+
if (idx !== -1) activeStack.splice(idx, 1);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function current(): FeedbackSink | undefined {
|
|
105
|
+
return activeStack[activeStack.length - 1];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 仅供测试:断言没有遗留未退出的活跃 coordinator(避免一个测试忘记 finish() 污染下一个)。 */
|
|
109
|
+
export function activeFeedbackSinkCount(): number {
|
|
110
|
+
return activeStack.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 不需要去重、不进入 RunFeedbackState 的瞬时活动文本(docker 镜像拉取进度、vercel session
|
|
114
|
+
* rotate 成功通知……)。没有活跃 coordinator 时退回一行 stderr,与迁移前的裸写视觉效果一致。 */
|
|
115
|
+
export function reportActivity(text: string): void {
|
|
116
|
+
const sink = current();
|
|
117
|
+
if (sink) {
|
|
118
|
+
sink.activity(text);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
writeStderrLine(text.endsWith("\n") ? text : `${text}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 需要去重、要出现在三种 profile 永久事件流里的一条 warning/error。 */
|
|
125
|
+
export function reportDiagnostic(input: DiagnosticInput): void {
|
|
126
|
+
const sink = current();
|
|
127
|
+
if (sink) {
|
|
128
|
+
sink.diagnostic(input);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
writeStderrLine(input.message.endsWith("\n") ? input.message : `${input.message}\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 一次失败/errored attempt 的永久通知(见 `FailureInput`)。与 `reportDiagnostic` 同级别的
|
|
135
|
+
* 「必须留痕」信号 —— 没有活跃 coordinator 时退回一行 stderr,不像 `reportAttemptLifecycle`
|
|
136
|
+
* 那样静默丢弃(定位一次真实失败不该因为没接 coordinator 就彻底没有出口)。 */
|
|
137
|
+
export function reportFailure(input: FailureInput): void {
|
|
138
|
+
const sink = current();
|
|
139
|
+
if (sink) {
|
|
140
|
+
sink.failure(input);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
writeStderrLine(`${input.who} ${input.verdict}: ${input.reason}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 一个因预算到顶而未被派发的 attempt(见 `BudgetExhaustedInput`)。没有活跃 coordinator 时的
|
|
147
|
+
* 兜底文案与 coordinator.ts 的 `fallbackTextFor` 对 "budget-exhausted" 事件的格式化保持一致。 */
|
|
148
|
+
/** 一次留存授予的永久通知(--keep-sandbox);没有活跃 coordinator 时退回一行 stderr。 */
|
|
149
|
+
export function reportKept(input: KeptInput): void {
|
|
150
|
+
const sink = current();
|
|
151
|
+
if (sink) {
|
|
152
|
+
sink.kept(input);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
writeStderrLine(`kept sandbox ${input.sandboxId} (${input.provider}) — ${input.identity.evalId} #${input.identity.attempt} ${input.verdict}\n`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function reportBudgetExhausted(input: BudgetExhaustedInput): void {
|
|
159
|
+
const sink = current();
|
|
160
|
+
if (sink) {
|
|
161
|
+
sink.budgetExhausted(input);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
writeStderrLine(`budget exhausted for ${input.experimentId} (spent ${input.spent}, unstarted ${input.unstarted})\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 用户中断(Ctrl+C)。没有活跃 coordinator 时的兜底文案与迁移前的 `runner.interrupted` 完全
|
|
168
|
+
* 相同 —— 调用方(run.ts)不再需要自己持有这段 i18n 文案。 */
|
|
169
|
+
export function reportInterrupted(): void {
|
|
170
|
+
const sink = current();
|
|
171
|
+
if (sink) {
|
|
172
|
+
sink.interrupted();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
writeStderrLine(t("runner.interrupted"));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* runner 自己驱动的 attempt 生命周期投影(见 docs/feature/experiments/cli.md「Attempt 阶段」)——
|
|
180
|
+
* `src/runner/attempt.ts`(phase/progress)与 `src/runner/run.ts`(start/complete/early-exit)是
|
|
181
|
+
* 目前仅有的两个调用方。没有活跃 coordinator 时(今天:CLI 还没接入 --output)静默 no-op ——
|
|
182
|
+
* 与其它 reportXxx() 不同,这里不退回 `writeStderrLine` 兜底:这类事件只服务尚不存在的
|
|
183
|
+
* dashboard,不是需要留痕的诊断,现有可观察行为(直到 CLI 接入真正的 coordinator 之前)因此
|
|
184
|
+
* 完全不变。
|
|
185
|
+
*/
|
|
186
|
+
export function reportAttemptLifecycle(event: AttemptLifecycleEvent): void {
|
|
187
|
+
current()?.lifecycle(event);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 一个 reporter 的某次回调抛错(见 `runner/report.ts` 的 `runReporter`)。`reporter`/`required`
|
|
191
|
+
* 来自调用方注册这个 reporter 时的 `ReporterRegistration`(见其字段注释)——默认 artifacts、
|
|
192
|
+
* 显式 `--json`/`--junit` 传 `required: true`,用户 `config.reporters`/`EvalDef.reporters`
|
|
193
|
+
* 传 `required: false`。coordinator 把它折进 `RunFeedbackState.diagnostics`(reducer 按
|
|
194
|
+
* `reporter-error:<reporter>` 去重),调用方(`cli.ts` 的 `assembleRunCompletion`)据此把
|
|
195
|
+
* `required` 为真的失败折进 `RunCompletion.reporterErrors`,让 completion/CI 退出码判红。 */
|
|
196
|
+
export function reportReporterError(input: { reporter: string; required: boolean; message: string }): void {
|
|
197
|
+
const sink = current();
|
|
198
|
+
if (sink) {
|
|
199
|
+
sink.reporterError(input);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
writeStderrLine(t("runner.reporterDiagnostic", { stage: input.reporter, message: input.message }));
|
|
203
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// 测试专用:确定性假 FeedbackIO。coordinator.test.ts 与后续阶段(D/E/F renderer 测试)共用,
|
|
2
|
+
// 好让「同一种 fake IO 的搭建方式」只维护一份 —— 不在每个测试文件里各自 monkey-patch
|
|
3
|
+
// `process.stdout`/`process.stderr`/`Date.now`/`setInterval`。
|
|
4
|
+
//
|
|
5
|
+
// 只在测试导入(`from "./testing.ts"`),不进 index.ts 的生产条形码出口。
|
|
6
|
+
|
|
7
|
+
import type { FeedbackClock, FeedbackIO, FeedbackStream, FeedbackTimerHandle } from "./io.ts";
|
|
8
|
+
|
|
9
|
+
export interface FakeFeedbackStream extends FeedbackStream {
|
|
10
|
+
/** 按写入顺序累积的完整历史,供断言「写了几次、写了什么」。 */
|
|
11
|
+
readonly writes: readonly string[];
|
|
12
|
+
isTTY: boolean;
|
|
13
|
+
columns: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createFakeStream(overrides: Partial<Pick<FakeFeedbackStream, "isTTY" | "columns" | "rows">> = {}): FakeFeedbackStream {
|
|
18
|
+
const writes: string[] = [];
|
|
19
|
+
return {
|
|
20
|
+
isTTY: overrides.isTTY ?? false,
|
|
21
|
+
columns: overrides.columns ?? 100,
|
|
22
|
+
rows: overrides.rows ?? 30,
|
|
23
|
+
writes,
|
|
24
|
+
write(text) {
|
|
25
|
+
writes.push(text);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FakeFeedbackIO {
|
|
31
|
+
readonly io: FeedbackIO;
|
|
32
|
+
readonly stdout: FakeFeedbackStream;
|
|
33
|
+
readonly stderr: FakeFeedbackStream;
|
|
34
|
+
/** 手动推进时钟 ms 毫秒;推进途中到期的 setInterval 回调按到期顺序依次同步触发
|
|
35
|
+
* (每次到期都重新入队下一次到期,不会因为一次 advance 跨越多个周期而漏触发)。 */
|
|
36
|
+
advance(ms: number): void;
|
|
37
|
+
/** 当前仍在运行(未被 clearInterval)的定时器数量,断言 stopDynamic()/finish() 之后
|
|
38
|
+
* 没有遗留定时器时用。 */
|
|
39
|
+
activeTimerCount(): number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FakeTimer {
|
|
43
|
+
id: number;
|
|
44
|
+
fn: () => void;
|
|
45
|
+
intervalMs: number;
|
|
46
|
+
nextFireAt: number;
|
|
47
|
+
cleared: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 创建一份完全确定性的假 IO;env 默认空对象(profile.ts 的 CI 检测据此判定「非 CI」)。 */
|
|
51
|
+
export function createFakeFeedbackIO(
|
|
52
|
+
opts: {
|
|
53
|
+
stdout?: Partial<Pick<FakeFeedbackStream, "isTTY" | "columns" | "rows">>;
|
|
54
|
+
stderr?: Partial<Pick<FakeFeedbackStream, "isTTY" | "columns" | "rows">>;
|
|
55
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
56
|
+
startAt?: number;
|
|
57
|
+
} = {},
|
|
58
|
+
): FakeFeedbackIO {
|
|
59
|
+
const stdout = createFakeStream(opts.stdout);
|
|
60
|
+
const stderr = createFakeStream(opts.stderr);
|
|
61
|
+
let now = opts.startAt ?? 0;
|
|
62
|
+
let nextId = 1;
|
|
63
|
+
const timers = new Map<number, FakeTimer>();
|
|
64
|
+
|
|
65
|
+
const clock: FeedbackClock = {
|
|
66
|
+
now: () => now,
|
|
67
|
+
setInterval: (fn, ms) => {
|
|
68
|
+
const id = nextId++;
|
|
69
|
+
timers.set(id, { id, fn, intervalMs: ms, nextFireAt: now + ms, cleared: false });
|
|
70
|
+
return { id } as unknown as FeedbackTimerHandle;
|
|
71
|
+
},
|
|
72
|
+
clearInterval: (handle) => {
|
|
73
|
+
const id = (handle as unknown as { id: number }).id;
|
|
74
|
+
const timer = timers.get(id);
|
|
75
|
+
if (timer) timer.cleared = true;
|
|
76
|
+
timers.delete(id);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const io: FeedbackIO = { stdout, stderr, env: opts.env ?? {}, clock };
|
|
81
|
+
|
|
82
|
+
function advance(ms: number): void {
|
|
83
|
+
const target = now + ms;
|
|
84
|
+
// 按到期时间逐个触发,支持一次 advance 跨越同一个定时器的多个周期,也支持多个不同
|
|
85
|
+
// 定时器交错到期 —— 每次只处理「当前最早到期」的那一个,触发后立即重新计算下一次到期。
|
|
86
|
+
for (;;) {
|
|
87
|
+
let earliest: FakeTimer | undefined;
|
|
88
|
+
for (const timer of timers.values()) {
|
|
89
|
+
if (timer.cleared) continue;
|
|
90
|
+
if (timer.nextFireAt > target) continue;
|
|
91
|
+
if (!earliest || timer.nextFireAt < earliest.nextFireAt) earliest = timer;
|
|
92
|
+
}
|
|
93
|
+
if (!earliest) break;
|
|
94
|
+
now = earliest.nextFireAt;
|
|
95
|
+
earliest.nextFireAt += earliest.intervalMs;
|
|
96
|
+
earliest.fn();
|
|
97
|
+
}
|
|
98
|
+
now = target;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function activeTimerCount(): number {
|
|
102
|
+
return [...timers.values()].filter((t) => !t.cleared).length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { io, stdout, stderr, advance, activeTimerCount };
|
|
106
|
+
}
|