niceeval 0.4.6 → 0.5.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/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +10 -2
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +104 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// show 的文本渲染:单 eval 详情、--history 时间轴、三个证据切面(transcript / trace / diff)。
|
|
2
|
+
// 输出形态照 docs-site/zh/guides/viewing-results.mdx 的示例块;长内容一律截断,
|
|
3
|
+
// 但截断永远如实标注剩余数量和原始工件路径 —— 输出对上下文窗口友好,事实源留在盘上。
|
|
4
|
+
// 全部纯函数(时间经 now 显式传入),证据数据由调用方 await 好了递进来。
|
|
5
|
+
|
|
6
|
+
import { join, relative } from "node:path";
|
|
7
|
+
import type { AssertionResult, ResultOutcome, StreamEvent, TraceSpan } from "../types.ts";
|
|
8
|
+
import type { DiffData } from "../types.ts";
|
|
9
|
+
import type { AttemptHandle, Selection, Snapshot } from "../results/index.ts";
|
|
10
|
+
import { foldEvalOutcome } from "../shared/outcome.ts";
|
|
11
|
+
import { attemptCostUSD } from "../report/metrics.ts";
|
|
12
|
+
import { formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD } from "../report/format.ts";
|
|
13
|
+
import { indentBlock, padDisplay, renderAlignedRows, wrapDisplay } from "../report/text/layout.ts";
|
|
14
|
+
import type { EvalHistoryRow, ExperimentHistoryRow } from "./compose.ts";
|
|
15
|
+
|
|
16
|
+
const MISSING = "—";
|
|
17
|
+
|
|
18
|
+
// ───────────────────────── 时间与小件 ─────────────────────────
|
|
19
|
+
|
|
20
|
+
/** 判决时间的相对标注("just now" / "41s ago" / "2h ago");未来时刻按 just now 兜底。 */
|
|
21
|
+
export function relativeAgo(iso: string | undefined, now: number): string {
|
|
22
|
+
if (!iso) return "";
|
|
23
|
+
const ms = now - Date.parse(iso);
|
|
24
|
+
if (!Number.isFinite(ms) || ms < 10_000) return "just now";
|
|
25
|
+
const seconds = Math.floor(ms / 1000);
|
|
26
|
+
if (seconds < 120) return `${seconds}s ago`;
|
|
27
|
+
const minutes = Math.floor(seconds / 60);
|
|
28
|
+
if (minutes < 120) return `${minutes}m ago`;
|
|
29
|
+
const hours = Math.floor(minutes / 60);
|
|
30
|
+
if (hours < 48) return `${hours}h ago`;
|
|
31
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 时间轴行的时刻:ISO 截到分钟,冒号换 -(与 run 目录名的清洗一致)。 */
|
|
35
|
+
export function timelineStamp(iso: string): string {
|
|
36
|
+
return iso.slice(0, 16).replace(/:/g, "-");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function outcomeMark(outcome: ResultOutcome): string {
|
|
40
|
+
if (outcome === "passed") return "✓";
|
|
41
|
+
if (outcome === "skipped") return "-";
|
|
42
|
+
return "✗";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function attemptsLabel(n: number): string {
|
|
46
|
+
return `${n} ${n === 1 ? "attempt" : "attempts"}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** attempt 的展示序号:落盘 attempt 从 0 计,人看 1 计(工件目录后缀 __2 = attempt 3)。 */
|
|
50
|
+
export function displayAttemptNumber(attempt: AttemptHandle): number {
|
|
51
|
+
return attempt.result.attempt + 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** attempt 工件目录的展示路径:尽量给相对 cwd 的短路径,出了 cwd 给绝对路径。 */
|
|
55
|
+
export function attemptArtifactsPath(attempt: AttemptHandle, cwd: string): string | undefined {
|
|
56
|
+
const r = attempt.result;
|
|
57
|
+
let abs: string | undefined;
|
|
58
|
+
if (r.artifactsDir) abs = join(attempt.runDir.dir, r.artifactsDir);
|
|
59
|
+
else if (r.artifactBase) abs = join(attempt.runDir.dir, "..", r.artifactBase);
|
|
60
|
+
if (!abs) return undefined;
|
|
61
|
+
const rel = relative(cwd, abs);
|
|
62
|
+
return rel.startsWith("..") ? abs : rel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ───────────────────────── attempt 挑选 ─────────────────────────
|
|
66
|
+
|
|
67
|
+
/** 选集内某道题的全部 attempt(合成选集里每实验只剩最新判决)。 */
|
|
68
|
+
export function attemptsOfEval(snapshots: Snapshot[], evalId: string): AttemptHandle[] {
|
|
69
|
+
const out: AttemptHandle[] = [];
|
|
70
|
+
for (const snapshot of snapshots) {
|
|
71
|
+
const ev = snapshot.evals.find((e) => e.id === evalId);
|
|
72
|
+
if (ev) out.push(...ev.attempts);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 证据切面与详情块默认挑最新一次失败的 attempt;没有失败挑最新一次。
|
|
79
|
+
* --attempt 收人看的 1 计序号;--experiment 已在选集合成时收窄。
|
|
80
|
+
*/
|
|
81
|
+
export function pickDetailAttempt(
|
|
82
|
+
attempts: AttemptHandle[],
|
|
83
|
+
attemptNumber?: number,
|
|
84
|
+
): AttemptHandle | undefined {
|
|
85
|
+
if (attempts.length === 0) return undefined;
|
|
86
|
+
const byTime = [...attempts].sort((a, b) =>
|
|
87
|
+
(a.result.startedAt ?? "").localeCompare(b.result.startedAt ?? "") || a.result.attempt - b.result.attempt,
|
|
88
|
+
);
|
|
89
|
+
if (attemptNumber !== undefined) {
|
|
90
|
+
const wanted = byTime.filter((a) => a.result.attempt === attemptNumber - 1);
|
|
91
|
+
return wanted.at(-1);
|
|
92
|
+
}
|
|
93
|
+
const failing = byTime.filter((a) => a.result.outcome === "failed" || a.result.outcome === "errored");
|
|
94
|
+
return failing.at(-1) ?? byTime.at(-1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ───────────────────────── 单 eval 详情 ─────────────────────────
|
|
98
|
+
|
|
99
|
+
function scoreText(score: number): string {
|
|
100
|
+
return formatPlainNumber(Math.round(score * 100) / 100);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 断言行:✓/✗ + severity + name;gate 失败带 detail,soft 恒带 score/1(失败再补 detail)。 */
|
|
104
|
+
export function assertionLine(a: AssertionResult): string {
|
|
105
|
+
const head = `${a.passed ? "✓" : "✗"} ${a.severity} ${a.name}`;
|
|
106
|
+
if (a.severity === "soft") {
|
|
107
|
+
const detail = !a.passed && a.detail ? `: ${a.detail}` : "";
|
|
108
|
+
return `${head} — ${scoreText(a.score)}/1${detail}`;
|
|
109
|
+
}
|
|
110
|
+
if (!a.passed) return a.detail ? `${head} — ${a.detail}` : `${head} — score ${scoreText(a.score)}`;
|
|
111
|
+
return head;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 详情块头部:`attempt 3 · compare/codex-gpt-5.4 · failed · 41s · 12.3k tokens · $0.04`。 */
|
|
115
|
+
export function attemptHeader(attempt: AttemptHandle): string {
|
|
116
|
+
const r = attempt.result;
|
|
117
|
+
const parts = [
|
|
118
|
+
`attempt ${displayAttemptNumber(attempt)}`,
|
|
119
|
+
attempt.experimentId,
|
|
120
|
+
r.outcome,
|
|
121
|
+
formatDurationMs(r.durationMs),
|
|
122
|
+
];
|
|
123
|
+
if (r.usage) parts.push(`${formatMetricValue(r.usage.inputTokens + r.usage.outputTokens)} tokens`);
|
|
124
|
+
const cost = attemptCostUSD(r);
|
|
125
|
+
if (cost !== null) parts.push(formatUSD(cost));
|
|
126
|
+
return parts.join(" · ");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface EvalDetailOptions {
|
|
130
|
+
evalId: string;
|
|
131
|
+
snapshots: Snapshot[];
|
|
132
|
+
/** 详情块展示的 attempt(pickDetailAttempt 的产物);无 attempt 时省略详情块。 */
|
|
133
|
+
detail?: AttemptHandle;
|
|
134
|
+
cwd: string;
|
|
135
|
+
now: number;
|
|
136
|
+
width: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `niceeval show <eval id>`:各 experiment 的判决行 + 默认 attempt 的断言明细。 */
|
|
140
|
+
export function evalDetailText(opts: EvalDetailOptions): string {
|
|
141
|
+
const { evalId, snapshots, detail, cwd, now, width } = opts;
|
|
142
|
+
const blocks: string[] = [];
|
|
143
|
+
|
|
144
|
+
const description = snapshots
|
|
145
|
+
.flatMap((s) => s.evals.filter((e) => e.id === evalId))
|
|
146
|
+
.flatMap((e) => e.attempts)
|
|
147
|
+
.map((a) => a.result.description)
|
|
148
|
+
.find((d) => d !== undefined);
|
|
149
|
+
blocks.push(description ? `${evalId} — ${description}` : evalId);
|
|
150
|
+
|
|
151
|
+
// 每 experiment 一行:折叠判决、attempt 数、最新 attempt 的耗时、总成本、判决时间
|
|
152
|
+
const rows: string[][] = [];
|
|
153
|
+
for (const snapshot of snapshots) {
|
|
154
|
+
const ev = snapshot.evals.find((e) => e.id === evalId);
|
|
155
|
+
if (!ev || ev.attempts.length === 0) continue;
|
|
156
|
+
const outcome = foldEvalOutcome(ev.attempts.map((a) => a.result));
|
|
157
|
+
const latest = ev.attempts.reduce((a, b) => (b.result.attempt >= a.result.attempt ? b : a));
|
|
158
|
+
let cost: number | null = null;
|
|
159
|
+
for (const attempt of ev.attempts) {
|
|
160
|
+
const c = attemptCostUSD(attempt.result);
|
|
161
|
+
if (c !== null) cost = (cost ?? 0) + c;
|
|
162
|
+
}
|
|
163
|
+
rows.push([
|
|
164
|
+
snapshot.experimentId,
|
|
165
|
+
`${outcomeMark(outcome)} ${outcome}`,
|
|
166
|
+
attemptsLabel(ev.attempts.length),
|
|
167
|
+
formatDurationMs(latest.result.durationMs),
|
|
168
|
+
cost === null ? MISSING : formatUSD(cost),
|
|
169
|
+
`(${relativeAgo(latest.result.startedAt ?? snapshot.startedAt, now)})`,
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
if (rows.length > 0) blocks.push(renderAlignedRows(rows));
|
|
173
|
+
|
|
174
|
+
if (detail) {
|
|
175
|
+
const lines: string[] = [attemptHeader(detail)];
|
|
176
|
+
for (const a of detail.result.assertions) {
|
|
177
|
+
lines.push(indentBlock(wrapDisplay(assertionLine(a), width - 2).join("\n"), " "));
|
|
178
|
+
}
|
|
179
|
+
if (detail.result.error !== undefined) {
|
|
180
|
+
lines.push(indentBlock(wrapDisplay(`error: ${detail.result.error}`, width - 2).join("\n"), " "));
|
|
181
|
+
}
|
|
182
|
+
if (detail.result.skipReason !== undefined) {
|
|
183
|
+
lines.push(indentBlock(wrapDisplay(`skipped: ${detail.result.skipReason}`, width - 2).join("\n"), " "));
|
|
184
|
+
}
|
|
185
|
+
blocks.push(lines.join("\n"));
|
|
186
|
+
|
|
187
|
+
const tail: string[] = [];
|
|
188
|
+
const artifacts = attemptArtifactsPath(detail, cwd);
|
|
189
|
+
if (artifacts) tail.push(`artifacts: ${artifacts}/`);
|
|
190
|
+
tail.push(`next: niceeval show ${evalId} --transcript | --trace | --diff`);
|
|
191
|
+
blocks.push(tail.join("\n"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return blocks.join("\n\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ───────────────────────── --history ─────────────────────────
|
|
198
|
+
|
|
199
|
+
/** 单 eval 时间轴:`compare/codex-gpt-5.4 · 5 runs · passed 2/5` + 每次真实执行一行。 */
|
|
200
|
+
export function evalHistoryText(opts: {
|
|
201
|
+
experimentId: string;
|
|
202
|
+
/** 多 eval 前缀时块头带上 eval id;单 eval 与 mdx 示例一致不带。 */
|
|
203
|
+
evalId?: string;
|
|
204
|
+
rows: EvalHistoryRow[];
|
|
205
|
+
}): string {
|
|
206
|
+
const { experimentId, evalId, rows } = opts;
|
|
207
|
+
const passed = rows.filter((r) => r.outcome === "passed").length;
|
|
208
|
+
const head = [
|
|
209
|
+
...(evalId ? [evalId] : []),
|
|
210
|
+
experimentId,
|
|
211
|
+
`${rows.length} ${rows.length === 1 ? "run" : "runs"}`,
|
|
212
|
+
`passed ${passed}/${rows.length}`,
|
|
213
|
+
].join(" · ");
|
|
214
|
+
if (rows.length === 0) return head;
|
|
215
|
+
const table = renderAlignedRows(
|
|
216
|
+
rows.map((r) => [
|
|
217
|
+
timelineStamp(r.startedAt),
|
|
218
|
+
`${outcomeMark(r.outcome)} ${r.outcome}`,
|
|
219
|
+
attemptsLabel(r.attempts),
|
|
220
|
+
r.costUSD === null ? MISSING : formatUSD(r.costUSD),
|
|
221
|
+
r.failedAssertion ?? (r.error ? `error: ${r.error}` : ""),
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
return `${head}\n\n${indentBlock(table, " ")}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 实验级 per-run 通过率序列(裸 `show --history`)。 */
|
|
228
|
+
export function experimentHistoryText(experimentId: string, rows: ExperimentHistoryRow[]): string {
|
|
229
|
+
const head = `${experimentId} · ${rows.length} ${rows.length === 1 ? "run" : "runs"}`;
|
|
230
|
+
if (rows.length === 0) return head;
|
|
231
|
+
const table = renderAlignedRows(
|
|
232
|
+
rows.map((r) => [
|
|
233
|
+
timelineStamp(r.startedAt),
|
|
234
|
+
`${r.passedEvals}/${r.totalEvals} passed`,
|
|
235
|
+
r.costUSD === null ? MISSING : formatUSD(r.costUSD),
|
|
236
|
+
]),
|
|
237
|
+
);
|
|
238
|
+
return `${head}\n\n${indentBlock(table, " ")}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ───────────────────────── 证据切面:transcript ─────────────────────────
|
|
242
|
+
|
|
243
|
+
const MAX_EVENTS = 80;
|
|
244
|
+
const MAX_TEXT = 600;
|
|
245
|
+
|
|
246
|
+
function clip(text: string, max = MAX_TEXT): string {
|
|
247
|
+
if (text.length <= max) return text;
|
|
248
|
+
return `${text.slice(0, max)}… (+${text.length - max} more chars)`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function jsonPreview(value: unknown, max = 200): string {
|
|
252
|
+
let text: string;
|
|
253
|
+
try {
|
|
254
|
+
text = JSON.stringify(value) ?? "";
|
|
255
|
+
} catch {
|
|
256
|
+
text = String(value);
|
|
257
|
+
}
|
|
258
|
+
return clip(text, max);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const ROLE_PAD = 12;
|
|
262
|
+
|
|
263
|
+
function eventLines(event: StreamEvent, width: number): string[] {
|
|
264
|
+
const body = (tag: string, text: string): string[] => {
|
|
265
|
+
const wrapped = wrapDisplay(clip(text), Math.max(20, width - ROLE_PAD));
|
|
266
|
+
return wrapped.map((line, i) => (i === 0 ? padDisplay(tag, ROLE_PAD) + line : " ".repeat(ROLE_PAD) + line));
|
|
267
|
+
};
|
|
268
|
+
switch (event.type) {
|
|
269
|
+
case "message":
|
|
270
|
+
return body(`[${event.role}]`, event.text);
|
|
271
|
+
case "thinking":
|
|
272
|
+
return body("[thinking]", clip(event.text, 200));
|
|
273
|
+
case "action.called":
|
|
274
|
+
return body("[tool]", `${event.name}(${jsonPreview(event.input)})`);
|
|
275
|
+
case "action.result":
|
|
276
|
+
return body("", `→ ${event.status}${event.output !== undefined ? `: ${jsonPreview(event.output)}` : ""}`);
|
|
277
|
+
case "subagent.called":
|
|
278
|
+
return body("[subagent]", event.name);
|
|
279
|
+
case "subagent.completed":
|
|
280
|
+
return body("", `→ ${event.status}`);
|
|
281
|
+
case "input.requested":
|
|
282
|
+
return body("[input]", event.request.prompt ?? event.request.display ?? "(input requested)");
|
|
283
|
+
case "compaction":
|
|
284
|
+
return body("[compaction]", event.reason ?? "context compacted");
|
|
285
|
+
case "error":
|
|
286
|
+
return body("[error]", event.message);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function transcriptText(opts: {
|
|
291
|
+
header: string;
|
|
292
|
+
events: StreamEvent[] | null;
|
|
293
|
+
artifactPath?: string;
|
|
294
|
+
width: number;
|
|
295
|
+
}): string {
|
|
296
|
+
const { header, events, artifactPath, width } = opts;
|
|
297
|
+
const source = artifactPath ? join(artifactPath, "events.json") : undefined;
|
|
298
|
+
if (!events || events.length === 0) {
|
|
299
|
+
return `${header}\n\n(no events recorded for this attempt${source ? ` · expected: ${source}` : ""})`;
|
|
300
|
+
}
|
|
301
|
+
const shown = events.slice(0, MAX_EVENTS);
|
|
302
|
+
const lines = shown.flatMap((event) => eventLines(event, width));
|
|
303
|
+
const toolCalls = events.filter((e) => e.type === "action.called").length;
|
|
304
|
+
const footer = [
|
|
305
|
+
`${events.length} ${events.length === 1 ? "event" : "events"}`,
|
|
306
|
+
toolCalls === 0 ? "no tool calls" : `${toolCalls} tool calls`,
|
|
307
|
+
...(events.length > shown.length ? [`${events.length - shown.length} more events not shown`] : []),
|
|
308
|
+
...(source ? [`full stream: ${source}`] : []),
|
|
309
|
+
].join(" · ");
|
|
310
|
+
return `${header}\n\n${lines.join("\n")}\n\n(${footer})`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ───────────────────────── 证据切面:trace ─────────────────────────
|
|
314
|
+
|
|
315
|
+
const MAX_SPANS = 40;
|
|
316
|
+
const TRACE_BAR_WIDTH = 20;
|
|
317
|
+
|
|
318
|
+
/** span 类目与 web 版同一套标准化口径(SpanKind),不读原生 span 名;other 保留原名如实展示。 */
|
|
319
|
+
function spanLabel(span: TraceSpan): string {
|
|
320
|
+
switch (span.kind) {
|
|
321
|
+
case "turn":
|
|
322
|
+
return "agent run";
|
|
323
|
+
case "model":
|
|
324
|
+
return "inference";
|
|
325
|
+
case "tool": {
|
|
326
|
+
const tool = span.attributes?.["tool_name"];
|
|
327
|
+
return typeof tool === "string" ? `execute_tool ${tool}` : "execute_tool";
|
|
328
|
+
}
|
|
329
|
+
case "agent":
|
|
330
|
+
return "invoke_agent";
|
|
331
|
+
default:
|
|
332
|
+
return span.name || "other";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function traceBar(startMs: number, endMs: number, windowStart: number, windowEnd: number): string {
|
|
337
|
+
const total = Math.max(1, windowEnd - windowStart);
|
|
338
|
+
const from = Math.max(0, Math.min(TRACE_BAR_WIDTH - 1, Math.floor(((startMs - windowStart) / total) * TRACE_BAR_WIDTH)));
|
|
339
|
+
const to = Math.max(from + 1, Math.min(TRACE_BAR_WIDTH, Math.ceil(((endMs - windowStart) / total) * TRACE_BAR_WIDTH)));
|
|
340
|
+
return `▕${"░".repeat(from)}${"█".repeat(to - from)}${"░".repeat(TRACE_BAR_WIDTH - to)}▏`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function traceText(opts: {
|
|
344
|
+
header: string;
|
|
345
|
+
spans: TraceSpan[] | null;
|
|
346
|
+
artifactPath?: string;
|
|
347
|
+
width: number;
|
|
348
|
+
}): string {
|
|
349
|
+
const { header, spans, artifactPath } = opts;
|
|
350
|
+
const source = artifactPath ? join(artifactPath, "trace.json") : undefined;
|
|
351
|
+
if (!spans || spans.length === 0) {
|
|
352
|
+
return `${header}\n\n(no trace recorded for this attempt${source ? ` · expected: ${source}` : ""})`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const windowStart = Math.min(...spans.map((s) => s.startMs));
|
|
356
|
+
const windowEnd = Math.max(...spans.map((s) => s.endMs));
|
|
357
|
+
const ids = new Set(spans.map((s) => s.spanId));
|
|
358
|
+
const children = new Map<string, TraceSpan[]>();
|
|
359
|
+
const roots: TraceSpan[] = [];
|
|
360
|
+
for (const span of spans) {
|
|
361
|
+
if (span.parentSpanId && ids.has(span.parentSpanId)) {
|
|
362
|
+
const list = children.get(span.parentSpanId) ?? [];
|
|
363
|
+
list.push(span);
|
|
364
|
+
children.set(span.parentSpanId, list);
|
|
365
|
+
} else {
|
|
366
|
+
roots.push(span);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const byStart = (a: TraceSpan, b: TraceSpan) => a.startMs - b.startMs;
|
|
370
|
+
roots.sort(byStart);
|
|
371
|
+
for (const list of children.values()) list.sort(byStart);
|
|
372
|
+
|
|
373
|
+
const rows: { label: string; span: TraceSpan }[] = [];
|
|
374
|
+
const walk = (span: TraceSpan, prefix: string, childPrefix: string) => {
|
|
375
|
+
rows.push({ label: prefix + spanLabel(span), span });
|
|
376
|
+
const kids = children.get(span.spanId) ?? [];
|
|
377
|
+
kids.forEach((kid, i) => {
|
|
378
|
+
const last = i === kids.length - 1;
|
|
379
|
+
walk(kid, childPrefix + (last ? "└─ " : "├─ "), childPrefix + (last ? " " : "│ "));
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
for (const root of roots) walk(root, "", "");
|
|
383
|
+
|
|
384
|
+
const shown = rows.slice(0, MAX_SPANS);
|
|
385
|
+
const labelWidth = Math.max(...shown.map((r) => r.label.length));
|
|
386
|
+
const lines = shown.map(
|
|
387
|
+
(r) =>
|
|
388
|
+
`${padDisplay(r.label, labelWidth)} ${traceBar(r.span.startMs, r.span.endMs, windowStart, windowEnd)} ${formatDurationMs(
|
|
389
|
+
r.span.endMs - r.span.startMs,
|
|
390
|
+
)}${r.span.status === "error" ? " ✗" : ""}`,
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const toolSpans = spans.filter((s) => s.kind === "tool").length;
|
|
394
|
+
const footer = [
|
|
395
|
+
`${spans.length} ${spans.length === 1 ? "span" : "spans"}`,
|
|
396
|
+
toolSpans === 0 ? "no execute_tool spans" : `${toolSpans} execute_tool spans`,
|
|
397
|
+
...(rows.length > shown.length ? [`${rows.length - shown.length} more spans not shown`] : []),
|
|
398
|
+
...(source ? [`full trace: ${source}`] : []),
|
|
399
|
+
].join(" · ");
|
|
400
|
+
return `${header}\n\n${lines.join("\n")}\n\n(${footer})`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ───────────────────────── 证据切面:diff ─────────────────────────
|
|
404
|
+
|
|
405
|
+
const MAX_DIFF_LINES = 200;
|
|
406
|
+
|
|
407
|
+
export function diffText(opts: {
|
|
408
|
+
header: string;
|
|
409
|
+
diff: DiffData | null;
|
|
410
|
+
artifactPath?: string;
|
|
411
|
+
/** --diff=<路径>:看单个文件的完整内容。 */
|
|
412
|
+
file?: string;
|
|
413
|
+
}): string {
|
|
414
|
+
const { header, diff, artifactPath, file } = opts;
|
|
415
|
+
const source = artifactPath ? join(artifactPath, "diff.json") : undefined;
|
|
416
|
+
if (!diff) {
|
|
417
|
+
return `${header}\n\n(no diff recorded for this attempt${source ? ` · expected: ${source}` : ""})`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (file !== undefined) {
|
|
421
|
+
const content = diff.generatedFiles[file];
|
|
422
|
+
if (content === undefined) {
|
|
423
|
+
if (diff.deletedFiles.includes(file)) return `${header}\n\nD ${file} (deleted by the agent)`;
|
|
424
|
+
const known = [...Object.keys(diff.generatedFiles), ...diff.deletedFiles];
|
|
425
|
+
return `${header}\n\nFile "${file}" is not in this attempt's diff. Files: ${known.join(", ") || "(none)"}`;
|
|
426
|
+
}
|
|
427
|
+
const lines = content.split("\n");
|
|
428
|
+
const shown = lines.slice(0, MAX_DIFF_LINES);
|
|
429
|
+
const footer = [
|
|
430
|
+
`${lines.length} ${lines.length === 1 ? "line" : "lines"}`,
|
|
431
|
+
...(lines.length > shown.length ? [`${lines.length - shown.length} more lines not shown`] : []),
|
|
432
|
+
...(source ? [`full diff: ${source}`] : []),
|
|
433
|
+
].join(" · ");
|
|
434
|
+
return `${header}\n\n${file}\n${shown.join("\n")}\n\n(${footer})`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const generated = Object.entries(diff.generatedFiles).sort(([a], [b]) => a.localeCompare(b));
|
|
438
|
+
const deleted = [...diff.deletedFiles].sort();
|
|
439
|
+
if (generated.length === 0 && deleted.length === 0) {
|
|
440
|
+
return `${header}\n\n(no file changes recorded${source ? ` · full diff: ${source}` : ""})`;
|
|
441
|
+
}
|
|
442
|
+
// 落盘的 diff 只有改后全文(git name-status + readFile),没有基线:
|
|
443
|
+
// 行数是文件现大小,不硬编 +/- 增删行;A/M 无从区分,统一 M(created or modified)。
|
|
444
|
+
const rows = [
|
|
445
|
+
...generated.map(([path, content]) => {
|
|
446
|
+
const lines = content.split("\n").length;
|
|
447
|
+
return ["M", path, `${lines} ${lines === 1 ? "line" : "lines"}`];
|
|
448
|
+
}),
|
|
449
|
+
...deleted.map((path) => ["D", path, "(deleted)"]),
|
|
450
|
+
];
|
|
451
|
+
const footer = [
|
|
452
|
+
`${rows.length} ${rows.length === 1 ? "file" : "files"}`,
|
|
453
|
+
...(source ? [`full diff: ${source}`] : []),
|
|
454
|
+
].join(" · ");
|
|
455
|
+
return `${header}\n\n${renderAlignedRows(rows)}\n\n(${footer})`;
|
|
456
|
+
}
|