niceeval 0.1.1 → 0.2.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/README.md +8 -8
- package/README.zh.md +13 -13
- package/docs/README.md +119 -0
- package/docs/adapters/README.md +60 -0
- package/docs/adapters/authoring.md +179 -0
- package/docs/adapters/coding-agent-skills-plugins.md +324 -0
- package/docs/adapters/collection.md +128 -0
- package/docs/adapters/contract.md +263 -0
- package/docs/adapters/reference/agent-eval.md +215 -0
- package/docs/adapters/reference/agent-loop-apis.md +69 -0
- package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
- package/docs/adapters/reference/eve-protocol.md +127 -0
- package/docs/adapters/reference/otel-genai.md +107 -0
- package/docs/adapters/reference/otel-instrumentation.md +65 -0
- package/docs/adapters/targets.md +99 -0
- package/docs/architecture.md +140 -0
- package/docs/assertions.md +388 -0
- package/docs/capabilities-by-construction.md +48 -0
- package/docs/cli.md +171 -0
- package/docs/concepts.md +106 -0
- package/docs/e2e-ci.md +295 -0
- package/docs/eval-authoring.md +319 -0
- package/docs/experiments.md +154 -0
- package/docs/getting-started.md +224 -0
- package/docs/multi-agent.md +145 -0
- package/docs/observability.md +333 -0
- package/docs/origin-integration.md +195 -0
- package/docs/references.md +35 -0
- package/docs/results-format.md +228 -0
- package/docs/runner.md +104 -0
- package/docs/sandbox.md +276 -0
- package/docs/scoring.md +119 -0
- package/docs/source-map.md +106 -0
- package/docs/tier-sync.md +177 -0
- package/docs/view.md +157 -0
- package/docs/vision.md +88 -0
- package/package.json +39 -5
- package/src/agents/ai-sdk-otel.test.ts +22 -0
- package/src/agents/ai-sdk-otel.ts +62 -0
- package/src/agents/ai-sdk.test.ts +462 -0
- package/src/agents/ai-sdk.ts +575 -0
- package/src/agents/bub.ts +30 -37
- package/src/agents/claude-code.ts +7 -4
- package/src/agents/codex.ts +15 -10
- package/src/agents/index.ts +48 -1
- package/src/agents/sdk-streams.test.ts +145 -0
- package/src/agents/sdk-streams.ts +457 -0
- package/src/agents/shared.ts +77 -39
- package/src/agents/streaming.test.ts +152 -0
- package/src/agents/streaming.ts +195 -0
- package/src/agents/types.ts +218 -0
- package/src/agents/ui-message-stream.test.ts +241 -0
- package/src/agents/ui-message-stream.ts +368 -0
- package/src/cli.ts +124 -77
- package/src/context/context.test.ts +203 -7
- package/src/context/context.ts +158 -49
- package/src/context/session.test.ts +96 -0
- package/src/context/session.ts +119 -9
- package/src/context/types.ts +205 -0
- package/src/define.ts +4 -14
- package/src/expect/index.ts +8 -71
- package/src/i18n/core.ts +28 -0
- package/src/i18n/en.ts +47 -5
- package/src/i18n/index.ts +5 -17
- package/src/i18n/zh-CN.ts +47 -5
- package/src/index.ts +12 -0
- package/src/loaders/index.ts +8 -39
- package/src/o11y/otlp/canonical.ts +7 -6
- package/src/o11y/otlp/mappers/codex.ts +10 -8
- package/src/o11y/otlp/mappers/index.ts +9 -21
- package/src/o11y/otlp/receiver.ts +25 -7
- package/src/o11y/otlp/sandbox-receiver.ts +57 -21
- package/src/o11y/otlp/turn-otel.test.ts +110 -0
- package/src/o11y/otlp/turn-otel.ts +125 -0
- package/src/o11y/parsers/bub.ts +13 -11
- package/src/o11y/parsers/claude-code.ts +27 -51
- package/src/o11y/parsers/codex.ts +29 -46
- package/src/o11y/parsers/index.ts +5 -14
- package/src/o11y/tool-names.test.ts +61 -0
- package/src/o11y/tool-names.ts +74 -0
- package/src/o11y/types.ts +135 -0
- package/src/runner/attempt.ts +456 -0
- package/src/runner/fingerprint.ts +59 -0
- package/src/runner/remote-sandbox.ts +53 -0
- package/src/runner/report.ts +53 -0
- package/src/runner/reporters/artifacts.ts +25 -4
- package/src/runner/reporters/console.ts +2 -8
- package/src/runner/reporters/live.ts +2 -8
- package/src/runner/reporters/shared.ts +15 -0
- package/src/runner/reporters/table.ts +10 -62
- package/src/runner/run.ts +114 -619
- package/src/runner/types.ts +237 -0
- package/src/sandbox/checkpoint.ts +15 -13
- package/src/sandbox/docker-stream.ts +87 -0
- package/src/sandbox/docker.ts +42 -120
- package/src/sandbox/e2b.ts +24 -75
- package/src/sandbox/local-files.ts +33 -0
- package/src/sandbox/paths.test.ts +85 -0
- package/src/sandbox/paths.ts +45 -0
- package/src/sandbox/resolve.ts +10 -34
- package/src/sandbox/shell.ts +17 -0
- package/src/sandbox/source-files.ts +42 -2
- package/src/sandbox/types.ts +158 -0
- package/src/sandbox/vercel.ts +55 -71
- package/src/scoring/collector.ts +3 -2
- package/src/scoring/judge.ts +72 -146
- package/src/scoring/match.ts +79 -0
- package/src/scoring/scoped.ts +66 -18
- package/src/scoring/types.ts +76 -0
- package/src/shared/aggregate.ts +48 -0
- package/src/shared/format.ts +20 -0
- package/src/shared/outcome.ts +48 -0
- package/src/shared/types.ts +37 -0
- package/src/types.ts +20 -911
- package/src/view/aggregate.ts +103 -0
- package/src/view/app/App.tsx +26 -5
- package/src/view/app/components/AttemptModal.tsx +12 -3
- package/src/view/app/components/CodeView.tsx +16 -19
- package/src/view/app/components/CopyControls.tsx +4 -4
- package/src/view/app/components/ExperimentTable.tsx +5 -5
- package/src/view/app/i18n.ts +31 -7
- package/src/view/app/lib/format.ts +17 -20
- package/src/view/app/lib/outcome.ts +4 -16
- package/src/view/app/lib/transcript-data.tsx +20 -4
- package/src/view/app/main.tsx +3 -5
- package/src/view/app/pages/RunsPage.tsx +2 -2
- package/src/view/app/pages/TracesPage.tsx +2 -2
- package/src/view/app/shared.ts +2 -1
- package/src/view/app/types.ts +9 -44
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +24 -401
- package/src/view/loader.ts +234 -0
- package/src/view/server.ts +136 -0
- package/src/view/shared/types.ts +64 -0
- package/src/view/styles.css +60 -9
package/src/scoring/scoped.ts
CHANGED
|
@@ -49,8 +49,39 @@ function toolMatches(tc: ToolCall, name: string, match?: ToolMatch): boolean {
|
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// ── evidence:把调用的出入参带回断言结果,view 展开可见,不用翻原始事件流 ──
|
|
53
|
+
|
|
54
|
+
function briefJson(value: unknown, max = 800): string {
|
|
55
|
+
let s: string;
|
|
56
|
+
try {
|
|
57
|
+
s = JSON.stringify(value) ?? String(value);
|
|
58
|
+
} catch {
|
|
59
|
+
s = String(value);
|
|
60
|
+
}
|
|
61
|
+
return s.length > max ? s.slice(0, max) + "…" : s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function describeCalls(calls: readonly ToolCall[]): string | undefined {
|
|
65
|
+
if (calls.length === 0) return undefined;
|
|
66
|
+
return calls
|
|
67
|
+
.map((tc) => {
|
|
68
|
+
const name = tc.originalName && tc.originalName !== tc.name ? tc.originalName : tc.name;
|
|
69
|
+
const lines = [`${name} [${tc.status}]`, ` input: ${briefJson(tc.input)}`];
|
|
70
|
+
if (tc.output !== undefined) lines.push(` output: ${briefJson(tc.output)}`);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
})
|
|
73
|
+
.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function describeSubagents(calls: readonly SubagentCall[]): string | undefined {
|
|
77
|
+
if (calls.length === 0) return undefined;
|
|
78
|
+
return calls
|
|
79
|
+
.map((s) => {
|
|
80
|
+
const lines = [`${s.name} [${s.status}]${s.remoteUrl ? ` ${s.remoteUrl}` : ""}`];
|
|
81
|
+
if (s.output !== undefined) lines.push(` output: ${briefJson(s.output)}`);
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
})
|
|
84
|
+
.join("\n");
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
function subagentMatches(call: SubagentCall, name: string, match?: SubagentMatch): boolean {
|
|
@@ -89,7 +120,8 @@ export function messageIncludes(token: string | RegExp): Spec {
|
|
|
89
120
|
.map((e) => e.text)
|
|
90
121
|
.join("\n");
|
|
91
122
|
const ok = token instanceof RegExp ? token.test(text) : text.includes(token);
|
|
92
|
-
|
|
123
|
+
// 失败时把实际被扫的助手文本带回来(和 t.check 的口径一致);命中时行色已说明一切。
|
|
124
|
+
return ok ? 1 : { score: 0, evidence: text ? (text.length > 4000 ? text.slice(0, 4000) + "…" : text) : undefined };
|
|
93
125
|
},
|
|
94
126
|
};
|
|
95
127
|
}
|
|
@@ -99,9 +131,13 @@ export function calledTool(name: string, match?: ToolMatch): Spec {
|
|
|
99
131
|
name: `calledTool(${name})`,
|
|
100
132
|
severity: "gate",
|
|
101
133
|
evaluate: (ctx) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
const matched = ctx.facts.toolCalls.filter((tc) => toolMatches(tc, name, match));
|
|
135
|
+
const n = matched.length;
|
|
136
|
+
const score = match?.count !== undefined ? (n === match.count ? 1 : 0) : n >= 1 ? 1 : 0;
|
|
137
|
+
// 命中给命中调用的出入参;没命中给同名调用(条件不满足的近失);再没有就列出实际调过的工具。
|
|
138
|
+
const sameName = ctx.facts.toolCalls.filter((tc) => tc.name === name || tc.originalName === name);
|
|
139
|
+
const shown = matched.length ? matched : sameName.length ? sameName : ctx.facts.toolCalls;
|
|
140
|
+
return { score, evidence: describeCalls(shown) };
|
|
105
141
|
},
|
|
106
142
|
};
|
|
107
143
|
}
|
|
@@ -110,7 +146,10 @@ export function notCalledTool(name: string, match?: ToolMatch): Spec {
|
|
|
110
146
|
return {
|
|
111
147
|
name: `notCalledTool(${name})`,
|
|
112
148
|
severity: "gate",
|
|
113
|
-
evaluate: (ctx) =>
|
|
149
|
+
evaluate: (ctx) => {
|
|
150
|
+
const matched = ctx.facts.toolCalls.filter((tc) => toolMatches(tc, name, match));
|
|
151
|
+
return { score: matched.length === 0 ? 1 : 0, evidence: describeCalls(matched) };
|
|
152
|
+
},
|
|
114
153
|
};
|
|
115
154
|
}
|
|
116
155
|
|
|
@@ -123,7 +162,8 @@ export function toolOrder(names: string[]): Spec {
|
|
|
123
162
|
for (const tc of ctx.facts.toolCalls) {
|
|
124
163
|
if (i < names.length && (tc.name === names[i] || tc.originalName === names[i])) i++;
|
|
125
164
|
}
|
|
126
|
-
|
|
165
|
+
const actual = ctx.facts.toolCalls.map((tc) => tc.originalName ?? tc.name).join(" → ");
|
|
166
|
+
return { score: i === names.length ? 1 : 0, evidence: actual || undefined };
|
|
127
167
|
},
|
|
128
168
|
};
|
|
129
169
|
}
|
|
@@ -132,7 +172,10 @@ export function usedNoTools(): Spec {
|
|
|
132
172
|
return {
|
|
133
173
|
name: "usedNoTools",
|
|
134
174
|
severity: "gate",
|
|
135
|
-
evaluate: (ctx) => (
|
|
175
|
+
evaluate: (ctx) => ({
|
|
176
|
+
score: ctx.facts.toolCalls.length === 0 ? 1 : 0,
|
|
177
|
+
evidence: describeCalls(ctx.facts.toolCalls),
|
|
178
|
+
}),
|
|
136
179
|
};
|
|
137
180
|
}
|
|
138
181
|
|
|
@@ -140,7 +183,10 @@ export function maxToolCalls(max: number): Spec {
|
|
|
140
183
|
return {
|
|
141
184
|
name: `maxToolCalls(${max})`,
|
|
142
185
|
severity: "gate",
|
|
143
|
-
evaluate: (ctx) => (
|
|
186
|
+
evaluate: (ctx) => ({
|
|
187
|
+
score: ctx.facts.toolCalls.length <= max ? 1 : 0,
|
|
188
|
+
evidence: describeCalls(ctx.facts.toolCalls),
|
|
189
|
+
}),
|
|
144
190
|
};
|
|
145
191
|
}
|
|
146
192
|
|
|
@@ -153,9 +199,10 @@ export function noFailedActions(): Spec {
|
|
|
153
199
|
name: "noFailedActions",
|
|
154
200
|
severity: "gate",
|
|
155
201
|
evaluate: (ctx) => {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
202
|
+
const failedTools = ctx.facts.toolCalls.filter((tc) => tc.status === "failed");
|
|
203
|
+
const failedSubs = ctx.facts.subagentCalls.filter((s) => s.status === "failed");
|
|
204
|
+
const evidence = [describeCalls(failedTools), describeSubagents(failedSubs)].filter(Boolean).join("\n") || undefined;
|
|
205
|
+
return { score: failedTools.length || failedSubs.length ? 0 : 1, evidence };
|
|
159
206
|
},
|
|
160
207
|
};
|
|
161
208
|
}
|
|
@@ -165,9 +212,10 @@ export function calledSubagent(name: string, match?: SubagentMatch): Spec {
|
|
|
165
212
|
name: `calledSubagent(${name})`,
|
|
166
213
|
severity: "gate",
|
|
167
214
|
evaluate: (ctx) => {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
215
|
+
const matched = ctx.facts.subagentCalls.filter((call) => subagentMatches(call, name, match));
|
|
216
|
+
const n = matched.length;
|
|
217
|
+
const score = match?.count !== undefined ? (n === match.count ? 1 : 0) : n >= 1 ? 1 : 0;
|
|
218
|
+
return { score, evidence: describeSubagents(matched.length ? matched : ctx.facts.subagentCalls) };
|
|
171
219
|
},
|
|
172
220
|
};
|
|
173
221
|
}
|
|
@@ -258,8 +306,8 @@ export function noFailedShellCommands(): Spec {
|
|
|
258
306
|
name: "noFailedShellCommands",
|
|
259
307
|
severity: "gate",
|
|
260
308
|
evaluate: (ctx) => {
|
|
261
|
-
const failed = ctx.facts.toolCalls.
|
|
262
|
-
return failed ? 0 : 1;
|
|
309
|
+
const failed = ctx.facts.toolCalls.filter((tc) => tc.name === "shell" && tc.status === "failed");
|
|
310
|
+
return { score: failed.length ? 0 : 1, evidence: describeCalls(failed) };
|
|
263
311
|
},
|
|
264
312
|
};
|
|
265
313
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// scoring 域类型:值级断言(expect 匹配器)、断言记录与结果、评分上下文、judge 配置。
|
|
2
|
+
|
|
3
|
+
import type { Severity, SourceLoc } from "../shared/types.ts";
|
|
4
|
+
import type { DerivedFacts, StreamEvent, Usage } from "../o11y/types.ts";
|
|
5
|
+
|
|
6
|
+
/** 值级断言(expect 匹配器)。纯函数 score + 可链式改严重级 / 阈值。 */
|
|
7
|
+
export interface ValueAssertion {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly severity: Severity;
|
|
10
|
+
readonly threshold?: number;
|
|
11
|
+
score(value: unknown): number | Promise<number>;
|
|
12
|
+
gate(threshold?: number): ValueAssertion;
|
|
13
|
+
atLeast(threshold: number): ValueAssertion;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 收集到 collector 里的一条断言记录(评估前)。 */
|
|
17
|
+
export interface AssertionSpec {
|
|
18
|
+
name: string;
|
|
19
|
+
severity: Severity;
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/** 延迟评估:final 时拿到完整运行结果再算分。 */
|
|
22
|
+
evaluate(ctx: ScoringContext): Promise<number> | number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 断言评估完的结果(进判决 / 报告)。 */
|
|
26
|
+
export interface AssertionResult {
|
|
27
|
+
name: string;
|
|
28
|
+
severity: Severity;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
score: number;
|
|
31
|
+
passed: boolean;
|
|
32
|
+
detail?: string;
|
|
33
|
+
/** 这条分数是看着什么材料算出来的(judge 收到的输入,或 t.check 失败时实际被检查的值)。view 展开排查「为什么是这个分」,默认不展示。 */
|
|
34
|
+
evidence?: string;
|
|
35
|
+
/** 所属分组(t.group 标题)。纯报告用,不影响 passed/score。 */
|
|
36
|
+
group?: string;
|
|
37
|
+
/** 断言在 eval 源码里的调用点(栈回溯抠出);view 把判决叠回这一行。 */
|
|
38
|
+
loc?: SourceLoc;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** eval 作者拿到的可链式句柄(t.judge.autoevals.closedQA(...).atLeast(0.7))。 */
|
|
42
|
+
export interface AssertionHandle {
|
|
43
|
+
atLeast(threshold: number): AssertionHandle;
|
|
44
|
+
gate(threshold?: number): AssertionHandle;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** scoped / judge 断言在 final 评估时拿到的运行结果。 */
|
|
48
|
+
export interface ScoringContext {
|
|
49
|
+
readonly events: readonly StreamEvent[];
|
|
50
|
+
readonly facts: DerivedFacts;
|
|
51
|
+
readonly diff: DiffData;
|
|
52
|
+
readonly scripts: Record<string, ScriptResult>;
|
|
53
|
+
readonly usage: Usage;
|
|
54
|
+
readonly status: "completed" | "failed" | "waiting";
|
|
55
|
+
/** 读沙箱里某文件的最终内容(judge / file 断言用)。 */
|
|
56
|
+
readFile(path: string): Promise<string | undefined>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ScriptResult {
|
|
60
|
+
success: boolean;
|
|
61
|
+
output: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DiffData {
|
|
65
|
+
generatedFiles: Record<string, string>;
|
|
66
|
+
deletedFiles: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ResultOutcome = "passed" | "failed" | "errored" | "skipped";
|
|
70
|
+
|
|
71
|
+
export interface JudgeConfig {
|
|
72
|
+
model: string;
|
|
73
|
+
/** OpenAI 兼容 base url + key 来源;省略则从 env 探测(见 scoring/judge.ts)。 */
|
|
74
|
+
baseUrl?: string;
|
|
75
|
+
apiKeyEnv?: string;
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// CLI 表格(runner/reporters/table.ts)与 view 榜单(view/aggregate.ts)共用的聚合小工具。
|
|
2
|
+
// 实验标签推导、token/成本求和、outcome 排序各只有一份 —— 否则同一个实验在终端和网页上
|
|
3
|
+
// 会显示成两个名字 / 两组数。保持环境无关(纯函数,只 type import)。
|
|
4
|
+
|
|
5
|
+
import type { Usage } from "../o11y/types.ts";
|
|
6
|
+
import type { EvalResult, ExperimentRunInfo } from "../runner/types.ts";
|
|
7
|
+
import type { ResultOutcome } from "../scoring/types.ts";
|
|
8
|
+
|
|
9
|
+
/** 明细行排序:失败最靠前(failed > errored > skipped > passed 的紧急程度)。 */
|
|
10
|
+
export const OUTCOME_ORDER: Record<ResultOutcome, number> = {
|
|
11
|
+
failed: 0,
|
|
12
|
+
errored: 1,
|
|
13
|
+
skipped: 2,
|
|
14
|
+
passed: 3,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function totalTokens(items: Array<Usage | undefined>): number {
|
|
18
|
+
return items.reduce((n, u) => n + (u?.inputTokens ?? 0) + (u?.outputTokens ?? 0), 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 求和,但「全都没报」时返回 undefined(区别于真 0 成本)。 */
|
|
22
|
+
export function sumMaybe(items: Array<number | undefined>): number | undefined {
|
|
23
|
+
const known = items.filter((n): n is number => n !== undefined);
|
|
24
|
+
return known.length ? known.reduce((sum, n) => sum + n, 0) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function avg(items: number[]): number {
|
|
28
|
+
return items.length ? items.reduce((sum, n) => sum + n, 0) / items.length : 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 实验 id 的展示名:取路径最后一段(exp 分组用目录表达)。 */
|
|
32
|
+
export function displayExperimentName(id: string | undefined): string | undefined {
|
|
33
|
+
if (!id) return undefined;
|
|
34
|
+
return id.split("/").filter(Boolean).at(-1) ?? id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 无 experimentId 时的兜底标签。 */
|
|
38
|
+
export function fallbackExperimentLabel(result: {
|
|
39
|
+
experiment?: ExperimentRunInfo;
|
|
40
|
+
agent: string;
|
|
41
|
+
model?: string;
|
|
42
|
+
}): string {
|
|
43
|
+
if (result.experiment?.id) return displayExperimentName(result.experiment.id) ?? result.experiment.id;
|
|
44
|
+
if (result.model) return `${result.agent}/${result.model}`;
|
|
45
|
+
return result.agent || "ad hoc run";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type { EvalResult };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// server 与前端共用的数值格式化;两边展示口径(取整/单位切换阈值)必须一致,所以单独成模块。
|
|
2
|
+
// 保持环境无关:不 import node/browser API。日期格式化不在这里 —— 它按前端当前 locale 做,
|
|
3
|
+
// 留在 app/lib/format.ts(见 formatDateTime)。
|
|
4
|
+
|
|
5
|
+
export function formatPercent(value?: number): string {
|
|
6
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "0%";
|
|
7
|
+
return Math.round(value * 100) + "%";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatDuration(ms?: number): string {
|
|
11
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
12
|
+
if (ms >= 60_000) return (ms / 60_000).toFixed(1) + "m";
|
|
13
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + "s";
|
|
14
|
+
return Math.round(ms) + "ms";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatCost(value?: number): string {
|
|
18
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "$0";
|
|
19
|
+
return "$" + value.toFixed(value < 1 ? 3 : 2);
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// server(aggregate.ts)与前端(app/lib/outcome.ts)共用的判决折叠口径。
|
|
2
|
+
// 必须保持环境无关且只依赖 type import,vite 前端会直接打包它。
|
|
3
|
+
// 单独成模块的原因:折叠/计票口径两边必须逐字一致,否则折叠行状态会和 KPI / 成功率对不上。
|
|
4
|
+
|
|
5
|
+
import type { ResultOutcome } from "../types.ts";
|
|
6
|
+
|
|
7
|
+
/** 折叠/计票只需要 outcome 字段;server 传 EvalResult,前端传 ViewResult 都满足。 */
|
|
8
|
+
export interface OutcomeLike {
|
|
9
|
+
outcome: ResultOutcome;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 把同一个 eval 的多轮 attempt 折叠成单一判决:任一轮通过 → 该 eval 通过(对齐 earlyExit
|
|
14
|
+
* 「先过一次即停」语义),否则按 failed > errored > skipped 取最严重的一个。
|
|
15
|
+
*/
|
|
16
|
+
export function foldEvalOutcome(attempts: OutcomeLike[]): ResultOutcome {
|
|
17
|
+
const outcomes = attempts.map((a) => a.outcome);
|
|
18
|
+
if (outcomes.some((o) => o === "passed")) return "passed";
|
|
19
|
+
if (outcomes.some((o) => o === "failed")) return "failed";
|
|
20
|
+
if (outcomes.some((o) => o === "errored")) return "errored";
|
|
21
|
+
return "skipped";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EvalLevelStats {
|
|
25
|
+
/** 去重后的 eval 数(成功率分母的口径)。 */
|
|
26
|
+
evals: number;
|
|
27
|
+
passed: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
errored: number;
|
|
30
|
+
skipped: number;
|
|
31
|
+
passRate: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 通过率与 passed/failed 一律按 eval 计票,不按 attempt:每个 eval 不管跑几轮都只占一票,先把它
|
|
36
|
+
* 的多轮折叠成单一判决再计数。否则 runs>1 时同一 eval 的 N 次 attempt 各算一票 —— 尤其 earlyExit
|
|
37
|
+
* 开时通过的 eval 只留 1 次、失败的 eval 跑满 N 次,失败 eval 被重复计入分母,把通过率拉低
|
|
38
|
+
* (见 docs/runner.md、docs/scoring.md)。keyOf 决定「一个 eval」的粒度:单实验按 eval id,
|
|
39
|
+
* 跨实验组按 experimentId|eval id。
|
|
40
|
+
*/
|
|
41
|
+
export function evalLevelStats<T extends OutcomeLike>(results: T[], keyOf: (r: T) => string): EvalLevelStats {
|
|
42
|
+
const byEval = new Map<string, T[]>();
|
|
43
|
+
for (const r of results) byEval.set(keyOf(r), [...(byEval.get(keyOf(r)) ?? []), r]);
|
|
44
|
+
const counts = { passed: 0, failed: 0, errored: 0, skipped: 0 };
|
|
45
|
+
for (const group of byEval.values()) counts[foldEvalOutcome(group)] += 1;
|
|
46
|
+
const ran = counts.passed + counts.failed + counts.errored; // skipped 不进分母
|
|
47
|
+
return { evals: byEval.size, ...counts, passRate: ran ? counts.passed / ran : 0 };
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 真正跨域的原子类型:序列化 / 严重级 / 源码位置 / 生命周期。
|
|
2
|
+
// 各域的类型住在各自目录的 types.ts(o11y / sandbox / agents / scoring / context / runner),
|
|
3
|
+
// src/types.ts 是聚合 facade —— 模块代码统一从那里 import,不必记住每个类型的家。
|
|
4
|
+
|
|
5
|
+
export type JsonValue =
|
|
6
|
+
| string
|
|
7
|
+
| number
|
|
8
|
+
| boolean
|
|
9
|
+
| null
|
|
10
|
+
| JsonValue[]
|
|
11
|
+
| { [key: string]: JsonValue };
|
|
12
|
+
|
|
13
|
+
export type Severity = "gate" | "soft";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* eval 源码里一次调用的位置(`t.send` / 各断言),运行期从栈回溯抠出来(见 src/source-loc.ts)。
|
|
17
|
+
* view 据此把运行结果叠回真实源码行(github-diff 式代码视图)。`file` 为相对项目根的路径。
|
|
18
|
+
*/
|
|
19
|
+
export interface SourceLoc {
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
column?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 随结果回传的一份 eval 源码(相对项目根的路径 + 文本),供 view 渲染代码视图。 */
|
|
26
|
+
export interface SourceArtifact {
|
|
27
|
+
path: string;
|
|
28
|
+
content: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Cleanup = () => Promise<void> | void;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 可本地化文案:纯字符串,或按 locale 代码(如 "en"、"zh-CN")映射多语言。
|
|
35
|
+
* view 按当前界面语言挑一条,挑不到回退到 en / 第一条。
|
|
36
|
+
*/
|
|
37
|
+
export type LocalizedText = string | Record<string, string>;
|