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,142 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/scoring/cases.md
|
|
2
|
+
// judge 解析与请求材料的单测:端点/凭据/模型解析结果必须进入真实请求,低分过不了 .gate()。
|
|
3
|
+
// fixture judge client = 截获 globalThis.fetch(autoevals 底层 openai client 走全局 fetch),
|
|
4
|
+
// 不起 HTTP server、不 spawn CLI。契约见 docs/feature/scoring/library/judge.md 与
|
|
5
|
+
// docs-site/zh/concepts/judge.mdx 的解析优先级表;用例登记在
|
|
6
|
+
// docs/engineering/unit-tests/scoring/cases.md 的 Judge 分区。
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { AssertionCollector } from "./collector.ts";
|
|
10
|
+
import { buildJudge } from "./judge.ts";
|
|
11
|
+
import { computeVerdict } from "./verdict.ts";
|
|
12
|
+
import { resolveAgentCoverage, completeCoverage } from "./coverage.ts";
|
|
13
|
+
import { emptyDiffData } from "./diff.ts";
|
|
14
|
+
import { deriveRunFacts } from "../o11y/derive.ts";
|
|
15
|
+
import type { JudgeConfig, ScoringContext } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
function ctx(): ScoringContext {
|
|
18
|
+
return {
|
|
19
|
+
events: [],
|
|
20
|
+
facts: deriveRunFacts([]),
|
|
21
|
+
diff: emptyDiffData(),
|
|
22
|
+
scripts: {},
|
|
23
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
24
|
+
status: "completed",
|
|
25
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
26
|
+
readFile: async () => undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CapturedRequest {
|
|
31
|
+
url: string;
|
|
32
|
+
authorization: string | null;
|
|
33
|
+
body: { model?: string; messages?: Array<{ role: string; content: string }> };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 截获全局 fetch:记录请求,回一个 ClosedQA 选 "N"(score 0)的 chat completion。 */
|
|
37
|
+
function stubJudgeFetch(): CapturedRequest[] {
|
|
38
|
+
const captured: CapturedRequest[] = [];
|
|
39
|
+
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
40
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
41
|
+
const headers = new Headers(input instanceof Request && !init?.headers ? input.headers : init?.headers);
|
|
42
|
+
const rawBody = init?.body ?? (input instanceof Request ? await input.text() : undefined);
|
|
43
|
+
captured.push({
|
|
44
|
+
url,
|
|
45
|
+
authorization: headers.get("authorization"),
|
|
46
|
+
body: typeof rawBody === "string" ? JSON.parse(rawBody) : {},
|
|
47
|
+
});
|
|
48
|
+
const payload = {
|
|
49
|
+
id: "chatcmpl-fixture",
|
|
50
|
+
object: "chat.completion",
|
|
51
|
+
created: 0,
|
|
52
|
+
model: "fixture",
|
|
53
|
+
choices: [
|
|
54
|
+
{
|
|
55
|
+
index: 0,
|
|
56
|
+
finish_reason: "tool_calls",
|
|
57
|
+
message: {
|
|
58
|
+
role: "assistant",
|
|
59
|
+
content: null,
|
|
60
|
+
tool_calls: [
|
|
61
|
+
{
|
|
62
|
+
id: "call_1",
|
|
63
|
+
type: "function",
|
|
64
|
+
function: {
|
|
65
|
+
name: "select_choice",
|
|
66
|
+
arguments: JSON.stringify({ choice: "N", reasons: "拒绝识图,答非所问" }),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
|
|
75
|
+
});
|
|
76
|
+
return captured;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function judgeWith(judge: JudgeConfig | undefined) {
|
|
80
|
+
const collector = new AssertionCollector();
|
|
81
|
+
const ns = buildJudge({
|
|
82
|
+
record: (spec) => collector.record(spec),
|
|
83
|
+
judge,
|
|
84
|
+
getOutput: () => "很抱歉,我目前使用的模型不支持图像输入,无法查看你发送的图片。",
|
|
85
|
+
getInput: () => "这张图片里有什么?主要是什么颜色?",
|
|
86
|
+
});
|
|
87
|
+
return { collector, ns };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
vi.unstubAllGlobals();
|
|
92
|
+
vi.unstubAllEnvs();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("judge 端点/凭据/模型解析进入真实请求", () => {
|
|
96
|
+
it("NICEEVAL_JUDGE_BASE/KEY 落在请求 URL 与 Bearer 头;config model 压过 NICEEVAL_JUDGE_MODEL;score 0 过不了 .gate(0.7)", async () => {
|
|
97
|
+
vi.stubEnv("NICEEVAL_JUDGE_BASE", "http://judge.fixture.internal/v1");
|
|
98
|
+
vi.stubEnv("NICEEVAL_JUDGE_KEY", "fixture-key");
|
|
99
|
+
vi.stubEnv("NICEEVAL_JUDGE_MODEL", "env-model");
|
|
100
|
+
const captured = stubJudgeFetch();
|
|
101
|
+
|
|
102
|
+
const { collector, ns } = judgeWith({ model: "config-model" });
|
|
103
|
+
ns.autoevals.closedQA("助手是否描述了这张图片的内容,而不是答非所问?").gate(0.7);
|
|
104
|
+
const [result] = await collector.finalize(ctx());
|
|
105
|
+
|
|
106
|
+
expect(captured).toHaveLength(1);
|
|
107
|
+
expect(captured[0]!.url).toBe("http://judge.fixture.internal/v1/chat/completions");
|
|
108
|
+
expect(captured[0]!.authorization).toBe("Bearer fixture-key");
|
|
109
|
+
expect(captured[0]!.body.model).toBe("config-model");
|
|
110
|
+
// 请求材料:被评的 output 与 rubric 都要真的送到裁判面前。
|
|
111
|
+
const material = JSON.stringify(captured[0]!.body.messages ?? []);
|
|
112
|
+
expect(material).toContain("不支持图像输入");
|
|
113
|
+
expect(material).toContain("助手是否描述了这张图片的内容");
|
|
114
|
+
|
|
115
|
+
// 裁判给 0 分,.gate(0.7) 是硬要求:该条 failed,折叠后整个 attempt failed(与 --strict 无关)。
|
|
116
|
+
expect(result).toMatchObject({ severity: "gate", threshold: 0.7, outcome: "failed", score: 0 });
|
|
117
|
+
expect(computeVerdict({ assertions: [result!] })).toBe("failed");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("单次 { model } 压过 judge config", async () => {
|
|
121
|
+
vi.stubEnv("NICEEVAL_JUDGE_KEY", "fixture-key");
|
|
122
|
+
const captured = stubJudgeFetch();
|
|
123
|
+
|
|
124
|
+
const { collector, ns } = judgeWith({ model: "config-model" });
|
|
125
|
+
ns.autoevals.closedQA("是否切题?", { model: "call-model" });
|
|
126
|
+
await collector.finalize(ctx());
|
|
127
|
+
|
|
128
|
+
expect(captured[0]!.body.model).toBe("call-model");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("config 缺席时回落到 NICEEVAL_JUDGE_MODEL", async () => {
|
|
132
|
+
vi.stubEnv("NICEEVAL_JUDGE_KEY", "fixture-key");
|
|
133
|
+
vi.stubEnv("NICEEVAL_JUDGE_MODEL", "env-model");
|
|
134
|
+
const captured = stubJudgeFetch();
|
|
135
|
+
|
|
136
|
+
const { collector, ns } = judgeWith(undefined);
|
|
137
|
+
ns.autoevals.closedQA("是否切题?");
|
|
138
|
+
await collector.finalize(ctx());
|
|
139
|
+
|
|
140
|
+
expect(captured[0]!.body.model).toBe("env-model");
|
|
141
|
+
});
|
|
142
|
+
});
|
package/src/scoring/judge.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// closedQA / factuality / summarizes 直接用 autoevals 库(braintrust)。
|
|
7
7
|
|
|
8
8
|
import { ClosedQA, Factuality, Summary } from "autoevals";
|
|
9
|
-
import type
|
|
9
|
+
import { unavailable, type EvalScore, type EvalUnavailable } from "./collector.ts";
|
|
10
10
|
import type { AssertionHandle, AutoevalsNamespace, JudgeConfig, JudgeNamespace, ScoringContext } from "../types.ts";
|
|
11
11
|
import { getEnv } from "../util.ts";
|
|
12
12
|
import { t } from "../i18n/index.ts";
|
|
@@ -43,7 +43,7 @@ export interface JudgeDeps {
|
|
|
43
43
|
record(spec: {
|
|
44
44
|
name: string;
|
|
45
45
|
severity: "soft";
|
|
46
|
-
evaluate(ctx: ScoringContext): Promise<EvalScore>;
|
|
46
|
+
evaluate(ctx: ScoringContext): Promise<EvalScore | EvalUnavailable>;
|
|
47
47
|
}): AssertionHandle;
|
|
48
48
|
judge: JudgeConfig | undefined;
|
|
49
49
|
getOutput: () => string;
|
|
@@ -52,17 +52,6 @@ export interface JudgeDeps {
|
|
|
52
52
|
signal?: AbortSignal;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** 没解析到 judge key 时返回的 no-op 命名空间:judge 断言静默跳过(不记录)。 */
|
|
56
|
-
function noOpJudge(): JudgeNamespace {
|
|
57
|
-
const handle: AssertionHandle = {
|
|
58
|
-
atLeast: () => handle,
|
|
59
|
-
gate: () => handle,
|
|
60
|
-
};
|
|
61
|
-
const skip = () => handle;
|
|
62
|
-
const noOpAutoevals: AutoevalsNamespace = { closedQA: skip, factuality: skip, summarizes: skip };
|
|
63
|
-
return { autoevals: noOpAutoevals };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
55
|
/** 预检显式配置的 judge:验证 model + API key 存在,并发最小请求确认端点可达。
|
|
67
56
|
* 返回错误描述字符串,可达则返回 undefined。*/
|
|
68
57
|
export async function probeJudge(judge: JudgeConfig, signal?: AbortSignal): Promise<string | undefined> {
|
|
@@ -99,11 +88,12 @@ export async function probeJudge(judge: JudgeConfig, signal?: AbortSignal): Prom
|
|
|
99
88
|
return undefined;
|
|
100
89
|
}
|
|
101
90
|
|
|
102
|
-
/** 构造 t.judge 命名空间。每个方法 record 一条延迟 soft 断言。
|
|
91
|
+
/** 构造 t.judge 命名空间。每个方法 record 一条延迟 soft 断言。
|
|
92
|
+
* 没解析到模型或 API key 时【不静默、不抛错】:该条断言照常记录,finalize 时落成
|
|
93
|
+
* `outcome: "unavailable"`(带机器可读 reason)——rubric 写了就必须留下记录,评不了的
|
|
94
|
+
* 结论按 Severity 与 Verdict 的折叠规则使 attempt errored(除非作者链 `.optional()`)。 */
|
|
103
95
|
export function buildJudge(deps: JudgeDeps): JudgeNamespace {
|
|
104
96
|
const resolved = resolveJudge(deps.judge);
|
|
105
|
-
// 没 key 就静默跳过 judge —— eval 不必再手动 gate「环境里有没有 judge key」。
|
|
106
|
-
if (!resolved.apiKey) return noOpJudge();
|
|
107
97
|
|
|
108
98
|
const materialFor = async (ctx: ScoringContext, on?: string): Promise<string> => {
|
|
109
99
|
if (on) {
|
|
@@ -124,16 +114,23 @@ export function buildJudge(deps: JudgeDeps): JudgeNamespace {
|
|
|
124
114
|
|
|
125
115
|
// 三个 autoevals 方法只差评分器和材料字段名,共享行为(record spec / 材料构造 /
|
|
126
116
|
// 分数归一 / evidence)单一出处。model 解析:单次 { model } → judge config →
|
|
127
|
-
// NICEEVAL_JUDGE_MODEL
|
|
117
|
+
// NICEEVAL_JUDGE_MODEL;没解析到模型或 key 时该条记 unavailable(带 reason),
|
|
118
|
+
// 绝不静默消失、也不在调用点崩——评不了的折叠交给 Severity 与 Verdict 规则。
|
|
128
119
|
const makeAutoeval =
|
|
129
120
|
(kind: "closedQA" | "factuality" | "summarizes", scorer: Scorer, payloadKey: "criteria" | "expected") =>
|
|
130
121
|
(reference: string, opts?: { on?: string; model?: string }) => {
|
|
131
122
|
const model = opts?.model ?? resolved.model;
|
|
132
|
-
if (!model) throw new Error(t("judge.modelMissing"));
|
|
133
123
|
return deps.record({
|
|
134
124
|
name: `judge:autoevals:${kind}`,
|
|
135
125
|
severity: "soft",
|
|
136
126
|
evaluate: async (ctx) => {
|
|
127
|
+
if (!model) {
|
|
128
|
+
return unavailable("judge-model-unresolved (no model in config, NICEEVAL_JUDGE_MODEL unset)");
|
|
129
|
+
}
|
|
130
|
+
if (!resolved.apiKey) {
|
|
131
|
+
const envHint = deps.judge?.apiKeyEnv ?? "NICEEVAL_JUDGE_KEY / OPENAI_API_KEY";
|
|
132
|
+
return unavailable(`judge-key-unresolved (${envHint} unset)`);
|
|
133
|
+
}
|
|
137
134
|
const output = await materialFor(ctx, opts?.on);
|
|
138
135
|
const result = await scorer({
|
|
139
136
|
input: deps.getInput(),
|
package/src/scoring/scoped.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
// 作用域断言:读标准事件流的派生事实(toolCalls / parked …)、diff、脚本结果。
|
|
2
2
|
// 每个 builder 产一个延迟 Spec,context 负责 record。规则覆盖不到的奇怪断言可直接落 events。
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
//
|
|
4
|
+
// 证据覆盖的三值折叠(见 docs/feature/scoring/architecture/evidence.md):
|
|
5
|
+
// - 正断言:找到匹配即通过(证据存在就是证据);没找到且所需通道非 complete(含 unknown)
|
|
6
|
+
// 记 unavailable——「没采到」不能算成「Agent 没做」;complete 通道上没找到才是 failed。
|
|
7
|
+
// - 负断言:找到反例即 failed(反例是确凿证据);没找到反例且通道非 complete 记 unavailable——
|
|
8
|
+
// 空流证明不了「没发生」。
|
|
9
|
+
// - 上限断言:实测已超限即 failed(partial 只会少采,超限是确凿的);未超限且通道非 complete
|
|
10
|
+
// 记 unavailable——缺证据不能按零聚合。
|
|
11
|
+
|
|
12
|
+
import { unavailable, type EvalUnavailable, type Spec } from "./collector.ts";
|
|
13
|
+
import type { CoverageChannel } from "./coverage.ts";
|
|
14
|
+
import type { ScoringContext, StreamEvent, SubagentCall, SubagentMatch, ToolCall, ToolMatch } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
// ── 覆盖折叠 ──
|
|
17
|
+
|
|
18
|
+
/** 所需通道非 complete 时返回 unavailable(带机器可读 reason),complete 返回 undefined。 */
|
|
19
|
+
function coverageGap(ctx: ScoringContext, channel: CoverageChannel): EvalUnavailable | undefined {
|
|
20
|
+
const c = ctx.coverage[channel];
|
|
21
|
+
if (c.status === "complete") return undefined;
|
|
22
|
+
return unavailable(`coverage:${channel}=${c.status}${c.reason ? ` (${c.reason})` : ""}`);
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
// ── 工具匹配小语言 ──
|
|
8
26
|
|
|
@@ -49,7 +67,7 @@ function toolMatches(tc: ToolCall, name: string, match?: ToolMatch): boolean {
|
|
|
49
67
|
return true;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
// ──
|
|
70
|
+
// ── received:把调用的出入参带回断言结果,view 展开可见,不用翻原始事件流 ──
|
|
53
71
|
|
|
54
72
|
function briefJson(value: unknown, max = 800): string {
|
|
55
73
|
let s: string;
|
|
@@ -95,18 +113,50 @@ function subagentMatches(call: SubagentCall, name: string, match?: SubagentMatch
|
|
|
95
113
|
return true;
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
/** ToolMatch 的期望描述(`≥1 call matching input.city = "Brooklyn"` 之类)。 */
|
|
117
|
+
function describeToolExpectation(name: string, match?: ToolMatch): string {
|
|
118
|
+
const conditions: string[] = [];
|
|
119
|
+
if (match?.input) {
|
|
120
|
+
for (const [k, v] of Object.entries(match.input)) conditions.push(`input.${k} = ${briefJson(v, 120)}`);
|
|
121
|
+
}
|
|
122
|
+
if (match?.status) conditions.push(`status = ${match.status}`);
|
|
123
|
+
const cond = conditions.length ? ` matching ${conditions.join(", ")}` : "";
|
|
124
|
+
const count = match?.count !== undefined ? `exactly ${match.count} calls of ${name}` : `≥1 call of ${name}`;
|
|
125
|
+
return `${count}${cond}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// ── builders ──
|
|
99
129
|
|
|
100
130
|
export function succeeded(): Spec {
|
|
101
131
|
return {
|
|
102
132
|
name: "succeeded",
|
|
103
133
|
severity: "gate",
|
|
104
|
-
evaluate: (ctx) =>
|
|
134
|
+
evaluate: (ctx) => {
|
|
135
|
+
// status 通道非 complete(恒 completed 的映射)时,末态不可信,通过与失败都评不了。
|
|
136
|
+
const gap = coverageGap(ctx, "status");
|
|
137
|
+
if (gap) return gap;
|
|
138
|
+
const ok = ctx.status !== "failed" && !ctx.facts.parked;
|
|
139
|
+
if (ok) return 1;
|
|
140
|
+
return {
|
|
141
|
+
score: 0,
|
|
142
|
+
received: ctx.facts.parked
|
|
143
|
+
? `${ctx.facts.inputRequests.length || 1} unanswered input request`
|
|
144
|
+
: `status: ${ctx.status}`,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
105
147
|
};
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
export function parked(): Spec {
|
|
109
|
-
return {
|
|
151
|
+
return {
|
|
152
|
+
name: "parked",
|
|
153
|
+
severity: "gate",
|
|
154
|
+
evaluate: (ctx) => {
|
|
155
|
+
const gap = coverageGap(ctx, "status");
|
|
156
|
+
if (gap) return gap;
|
|
157
|
+
return ctx.facts.parked ? 1 : { score: 0, received: `status: ${ctx.status} (no pending input request)` };
|
|
158
|
+
},
|
|
159
|
+
};
|
|
110
160
|
}
|
|
111
161
|
|
|
112
162
|
export function messageIncludes(token: string | RegExp): Spec {
|
|
@@ -120,8 +170,15 @@ export function messageIncludes(token: string | RegExp): Spec {
|
|
|
120
170
|
.map((e) => e.text)
|
|
121
171
|
.join("\n");
|
|
122
172
|
const ok = token instanceof RegExp ? token.test(text) : text.includes(token);
|
|
123
|
-
|
|
124
|
-
|
|
173
|
+
if (ok) return 1;
|
|
174
|
+
// 正断言:非 complete 通道上没找到记 unavailable,不判失败。
|
|
175
|
+
const gap = coverageGap(ctx, "messages");
|
|
176
|
+
if (gap) return gap;
|
|
177
|
+
return {
|
|
178
|
+
score: 0,
|
|
179
|
+
expected: token instanceof RegExp ? `matches ${token}` : `contains ${JSON.stringify(token)}`,
|
|
180
|
+
received: text ? (text.length > 4000 ? text.slice(0, 4000) + "…" : text) : "(no assistant messages)",
|
|
181
|
+
};
|
|
125
182
|
},
|
|
126
183
|
};
|
|
127
184
|
}
|
|
@@ -133,11 +190,24 @@ export function calledTool(name: string, match?: ToolMatch): Spec {
|
|
|
133
190
|
evaluate: (ctx) => {
|
|
134
191
|
const matched = ctx.facts.toolCalls.filter((tc) => toolMatches(tc, name, match));
|
|
135
192
|
const n = matched.length;
|
|
136
|
-
const
|
|
193
|
+
const ok = match?.count !== undefined ? n === match.count : n >= 1;
|
|
137
194
|
// 命中给命中调用的出入参;没命中给同名调用(条件不满足的近失);再没有就列出实际调过的工具。
|
|
138
195
|
const sameName = ctx.facts.toolCalls.filter((tc) => tc.name === name || tc.originalName === name);
|
|
139
196
|
const shown = matched.length ? matched : sameName.length ? sameName : ctx.facts.toolCalls;
|
|
140
|
-
return { score,
|
|
197
|
+
if (ok) return { score: 1, received: describeCalls(shown) };
|
|
198
|
+
// 精确 count 且实测已超出:partial 只会少采,超出是确凿失败;其余未命中按覆盖折叠。
|
|
199
|
+
const definitiveOvershoot = match?.count !== undefined && n > match.count;
|
|
200
|
+
if (!definitiveOvershoot) {
|
|
201
|
+
const gap = coverageGap(ctx, "actions");
|
|
202
|
+
if (gap) return gap;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
score: 0,
|
|
206
|
+
expected: describeToolExpectation(name, match),
|
|
207
|
+
received:
|
|
208
|
+
describeCalls(shown) ??
|
|
209
|
+
`${ctx.facts.toolCalls.length} tool calls, none matching`,
|
|
210
|
+
};
|
|
141
211
|
},
|
|
142
212
|
};
|
|
143
213
|
}
|
|
@@ -148,7 +218,11 @@ export function notCalledTool(name: string, match?: ToolMatch): Spec {
|
|
|
148
218
|
severity: "gate",
|
|
149
219
|
evaluate: (ctx) => {
|
|
150
220
|
const matched = ctx.facts.toolCalls.filter((tc) => toolMatches(tc, name, match));
|
|
151
|
-
|
|
221
|
+
// 负断言:找到反例即 failed(证据确凿),没找到时空流证明不了「没发生」。
|
|
222
|
+
if (matched.length > 0) return { score: 0, received: describeCalls(matched) };
|
|
223
|
+
const gap = coverageGap(ctx, "actions");
|
|
224
|
+
if (gap) return gap;
|
|
225
|
+
return 1;
|
|
152
226
|
},
|
|
153
227
|
};
|
|
154
228
|
}
|
|
@@ -162,8 +236,15 @@ export function toolOrder(names: string[]): Spec {
|
|
|
162
236
|
for (const tc of ctx.facts.toolCalls) {
|
|
163
237
|
if (i < names.length && (tc.name === names[i] || tc.originalName === names[i])) i++;
|
|
164
238
|
}
|
|
239
|
+
if (i === names.length) return 1;
|
|
240
|
+
const gap = coverageGap(ctx, "actions");
|
|
241
|
+
if (gap) return gap;
|
|
165
242
|
const actual = ctx.facts.toolCalls.map((tc) => tc.originalName ?? tc.name).join(" → ");
|
|
166
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
score: 0,
|
|
245
|
+
expected: names.join(" → "),
|
|
246
|
+
received: actual ? `${actual} (missing ${names[i]})` : "(no tool calls)",
|
|
247
|
+
};
|
|
167
248
|
},
|
|
168
249
|
};
|
|
169
250
|
}
|
|
@@ -172,10 +253,12 @@ export function usedNoTools(): Spec {
|
|
|
172
253
|
return {
|
|
173
254
|
name: "usedNoTools",
|
|
174
255
|
severity: "gate",
|
|
175
|
-
evaluate: (ctx) =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
256
|
+
evaluate: (ctx) => {
|
|
257
|
+
if (ctx.facts.toolCalls.length > 0) return { score: 0, received: describeCalls(ctx.facts.toolCalls) };
|
|
258
|
+
const gap = coverageGap(ctx, "actions");
|
|
259
|
+
if (gap) return gap;
|
|
260
|
+
return 1;
|
|
261
|
+
},
|
|
179
262
|
};
|
|
180
263
|
}
|
|
181
264
|
|
|
@@ -183,10 +266,14 @@ export function maxToolCalls(max: number): Spec {
|
|
|
183
266
|
return {
|
|
184
267
|
name: `maxToolCalls(${max})`,
|
|
185
268
|
severity: "gate",
|
|
186
|
-
evaluate: (ctx) =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
269
|
+
evaluate: (ctx) => {
|
|
270
|
+
const n = ctx.facts.toolCalls.length;
|
|
271
|
+
// 上限断言:实测已超限是确凿失败;未超限时,partial 通道可能漏采,不能按不完整计数放行。
|
|
272
|
+
if (n > max) return { score: 0, expected: `≤ ${max} tool calls`, received: describeCalls(ctx.facts.toolCalls) };
|
|
273
|
+
const gap = coverageGap(ctx, "actions");
|
|
274
|
+
if (gap) return gap;
|
|
275
|
+
return 1;
|
|
276
|
+
},
|
|
190
277
|
};
|
|
191
278
|
}
|
|
192
279
|
|
|
@@ -199,9 +286,15 @@ export function loadedSkill(skill: string): Spec {
|
|
|
199
286
|
evaluate: (ctx) => {
|
|
200
287
|
const loaded = ctx.events.filter((e): e is Extract<StreamEvent, { type: "skill.loaded" }> => e.type === "skill.loaded");
|
|
201
288
|
const matched = loaded.filter((e) => e.skill === skill);
|
|
289
|
+
if (matched.length) return { score: 1, received: matched.map((e) => e.skill).join(", ") };
|
|
290
|
+
const gap = coverageGap(ctx, "events");
|
|
291
|
+
if (gap) return gap;
|
|
202
292
|
// 没命中时把实际加载过的 skill 列出来(常见失败是名字对不上,而不是一个都没加载)。
|
|
203
|
-
|
|
204
|
-
|
|
293
|
+
return {
|
|
294
|
+
score: 0,
|
|
295
|
+
expected: `skill ${JSON.stringify(skill)} loaded`,
|
|
296
|
+
received: loaded.length ? loaded.map((e) => e.skill).join(", ") : "(no skills loaded)",
|
|
297
|
+
};
|
|
205
298
|
},
|
|
206
299
|
};
|
|
207
300
|
}
|
|
@@ -213,8 +306,13 @@ export function noFailedActions(): Spec {
|
|
|
213
306
|
evaluate: (ctx) => {
|
|
214
307
|
const failedTools = ctx.facts.toolCalls.filter((tc) => tc.status === "failed");
|
|
215
308
|
const failedSubs = ctx.facts.subagentCalls.filter((s) => s.status === "failed");
|
|
216
|
-
|
|
217
|
-
|
|
309
|
+
if (failedTools.length || failedSubs.length) {
|
|
310
|
+
const received = [describeCalls(failedTools), describeSubagents(failedSubs)].filter(Boolean).join("\n") || undefined;
|
|
311
|
+
return { score: 0, received };
|
|
312
|
+
}
|
|
313
|
+
const gap = coverageGap(ctx, "actions");
|
|
314
|
+
if (gap) return gap;
|
|
315
|
+
return 1;
|
|
218
316
|
},
|
|
219
317
|
};
|
|
220
318
|
}
|
|
@@ -226,8 +324,14 @@ export function calledSubagent(name: string, match?: SubagentMatch): Spec {
|
|
|
226
324
|
evaluate: (ctx) => {
|
|
227
325
|
const matched = ctx.facts.subagentCalls.filter((call) => subagentMatches(call, name, match));
|
|
228
326
|
const n = matched.length;
|
|
229
|
-
const
|
|
230
|
-
return { score,
|
|
327
|
+
const ok = match?.count !== undefined ? n === match.count : n >= 1;
|
|
328
|
+
if (ok) return { score: 1, received: describeSubagents(matched) };
|
|
329
|
+
const definitiveOvershoot = match?.count !== undefined && n > match.count;
|
|
330
|
+
if (!definitiveOvershoot) {
|
|
331
|
+
const gap = coverageGap(ctx, "actions");
|
|
332
|
+
if (gap) return gap;
|
|
333
|
+
}
|
|
334
|
+
return { score: 0, received: describeSubagents(matched.length ? matched : ctx.facts.subagentCalls) };
|
|
231
335
|
},
|
|
232
336
|
};
|
|
233
337
|
}
|
|
@@ -238,8 +342,18 @@ export function eventOfType(type: string, opts?: { count?: number }): Spec {
|
|
|
238
342
|
severity: "gate",
|
|
239
343
|
evaluate: (ctx) => {
|
|
240
344
|
const n = ctx.events.filter((e) => e.type === type).length;
|
|
241
|
-
|
|
242
|
-
|
|
345
|
+
const ok = opts?.count !== undefined ? n === opts.count : n >= 1;
|
|
346
|
+
if (ok) return 1;
|
|
347
|
+
const definitiveOvershoot = opts?.count !== undefined && n > opts.count;
|
|
348
|
+
if (!definitiveOvershoot) {
|
|
349
|
+
const gap = coverageGap(ctx, "events");
|
|
350
|
+
if (gap) return gap;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
score: 0,
|
|
354
|
+
expected: opts?.count !== undefined ? `exactly ${opts.count} × ${type}` : `≥1 × ${type}`,
|
|
355
|
+
received: `${n} × ${type}`,
|
|
356
|
+
};
|
|
243
357
|
},
|
|
244
358
|
};
|
|
245
359
|
}
|
|
@@ -248,7 +362,13 @@ export function notEventOfType(type: string): Spec {
|
|
|
248
362
|
return {
|
|
249
363
|
name: `notEvent(${type})`,
|
|
250
364
|
severity: "gate",
|
|
251
|
-
evaluate: (ctx) =>
|
|
365
|
+
evaluate: (ctx) => {
|
|
366
|
+
const hits = ctx.events.filter((e) => e.type === type);
|
|
367
|
+
if (hits.length > 0) return { score: 0, received: `${hits.length} × ${type}` };
|
|
368
|
+
const gap = coverageGap(ctx, "events");
|
|
369
|
+
if (gap) return gap;
|
|
370
|
+
return 1;
|
|
371
|
+
},
|
|
252
372
|
};
|
|
253
373
|
}
|
|
254
374
|
|
|
@@ -261,39 +381,61 @@ export function eventOrder(types: StreamEvent["type"][]): Spec {
|
|
|
261
381
|
for (const ev of ctx.events) {
|
|
262
382
|
if (i < types.length && ev.type === types[i]) i++;
|
|
263
383
|
}
|
|
264
|
-
|
|
384
|
+
if (i === types.length) return 1;
|
|
385
|
+
const gap = coverageGap(ctx, "events");
|
|
386
|
+
if (gap) return gap;
|
|
387
|
+
return {
|
|
388
|
+
score: 0,
|
|
389
|
+
expected: types.join(" → "),
|
|
390
|
+
received: `missing ${types[i]} (matched ${i}/${types.length})`,
|
|
391
|
+
};
|
|
265
392
|
},
|
|
266
393
|
};
|
|
267
394
|
}
|
|
268
395
|
|
|
396
|
+
/** label 是失败时的全部解释(谓词不透明),必填、进断言标题。 */
|
|
269
397
|
export function eventsSatisfy(
|
|
398
|
+
label: string,
|
|
270
399
|
predicate: (events: readonly StreamEvent[]) => boolean,
|
|
271
|
-
label = "predicate",
|
|
272
400
|
): Spec {
|
|
401
|
+
if (typeof label !== "string" || label.trim().length === 0 || typeof predicate !== "function") {
|
|
402
|
+
throw new TypeError(
|
|
403
|
+
"eventsSatisfy(label, predicate) requires a non-empty string label followed by a predicate function; " +
|
|
404
|
+
`received (${typeof label}, ${typeof predicate}). The former (predicate, label) order is not supported.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
273
407
|
return {
|
|
274
|
-
name:
|
|
408
|
+
name: label,
|
|
275
409
|
severity: "gate",
|
|
276
|
-
evaluate: (ctx) =>
|
|
410
|
+
evaluate: (ctx) => {
|
|
411
|
+
if (predicate(ctx.events)) return 1;
|
|
412
|
+
const gap = coverageGap(ctx, "events");
|
|
413
|
+
if (gap) return gap;
|
|
414
|
+
return { score: 0, received: `${ctx.events.length} events in scope` };
|
|
415
|
+
},
|
|
277
416
|
};
|
|
278
417
|
}
|
|
279
418
|
|
|
280
|
-
// ── 工作区 / 沙箱 ──
|
|
281
|
-
|
|
282
|
-
function diffMatchesRe(diff: DiffData, re: RegExp): boolean {
|
|
283
|
-
for (const [path, content] of Object.entries(diff.generatedFiles)) {
|
|
284
|
-
if (re.test(path) || re.test(content)) return true;
|
|
285
|
-
}
|
|
286
|
-
for (const path of diff.deletedFiles) {
|
|
287
|
-
if (re.test(path)) return true;
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
419
|
+
// ── 工作区 / 沙箱(断的是 agent 归因增量,见 docs/feature/sandbox/architecture.md)──
|
|
291
420
|
|
|
292
421
|
export function fileChanged(path: string): Spec {
|
|
293
422
|
return {
|
|
294
423
|
name: `fileChanged(${path})`,
|
|
295
424
|
severity: "gate",
|
|
296
|
-
|
|
425
|
+
// 断「任一 send 窗口触及」(行为证据):净效果为 none(改完又改回)也算发生过。
|
|
426
|
+
evaluate: (ctx) => {
|
|
427
|
+
const summary = ctx.diff.files[path];
|
|
428
|
+
if (summary !== undefined && summary.net !== "deleted") return 1;
|
|
429
|
+
const windows = ctx.diff.windows.length;
|
|
430
|
+
return {
|
|
431
|
+
score: 0,
|
|
432
|
+
expected: "changed by agent in some send window",
|
|
433
|
+
received:
|
|
434
|
+
summary !== undefined
|
|
435
|
+
? `net effect: ${summary.net} (touched in ${summary.windows.join(", ")})`
|
|
436
|
+
: `not changed in any of ${windows} send window${windows === 1 ? "" : "s"}`,
|
|
437
|
+
};
|
|
438
|
+
},
|
|
297
439
|
};
|
|
298
440
|
}
|
|
299
441
|
|
|
@@ -301,7 +443,10 @@ export function fileDeleted(path: string): Spec {
|
|
|
301
443
|
return {
|
|
302
444
|
name: `fileDeleted(${path})`,
|
|
303
445
|
severity: "gate",
|
|
304
|
-
evaluate: (ctx) =>
|
|
446
|
+
evaluate: (ctx) =>
|
|
447
|
+
ctx.diff.files[path]?.net === "deleted"
|
|
448
|
+
? 1
|
|
449
|
+
: { score: 0, expected: "deleted by agent", received: ctx.diff.files[path] ? `net effect: ${ctx.diff.files[path]!.net}` : "not touched by agent" },
|
|
305
450
|
};
|
|
306
451
|
}
|
|
307
452
|
|
|
@@ -309,7 +454,19 @@ export function notInDiff(re: RegExp): Spec {
|
|
|
309
454
|
return {
|
|
310
455
|
name: `notInDiff(${re})`,
|
|
311
456
|
severity: "gate",
|
|
312
|
-
evaluate: (ctx) =>
|
|
457
|
+
evaluate: (ctx) => {
|
|
458
|
+
for (const path of Object.keys(ctx.diff.files)) {
|
|
459
|
+
if (re.test(path)) return { score: 0, received: `matched path ${path}` };
|
|
460
|
+
}
|
|
461
|
+
for (const window of ctx.diff.windows) {
|
|
462
|
+
for (const [path, change] of Object.entries(window.changes)) {
|
|
463
|
+
if (change.after !== undefined && re.test(change.after)) {
|
|
464
|
+
return { score: 0, received: `matched in ${path} (window ${window.window})` };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return 1;
|
|
469
|
+
},
|
|
313
470
|
};
|
|
314
471
|
}
|
|
315
472
|
|
|
@@ -319,7 +476,10 @@ export function noFailedShellCommands(): Spec {
|
|
|
319
476
|
severity: "gate",
|
|
320
477
|
evaluate: (ctx) => {
|
|
321
478
|
const failed = ctx.facts.toolCalls.filter((tc) => tc.name === "shell" && tc.status === "failed");
|
|
322
|
-
|
|
479
|
+
if (failed.length) return { score: 0, received: describeCalls(failed) };
|
|
480
|
+
const gap = coverageGap(ctx, "actions");
|
|
481
|
+
if (gap) return gap;
|
|
482
|
+
return 1;
|
|
323
483
|
},
|
|
324
484
|
};
|
|
325
485
|
}
|
|
@@ -332,7 +492,11 @@ export function maxTokens(max: number): Spec {
|
|
|
332
492
|
severity: "gate",
|
|
333
493
|
evaluate: (ctx) => {
|
|
334
494
|
const total = ctx.usage.inputTokens + ctx.usage.outputTokens;
|
|
335
|
-
|
|
495
|
+
// 上限断言:实测已超限是确凿失败;未超限时缺 usage 不能按零聚合。
|
|
496
|
+
if (total > max) return { score: 0, expected: `≤ ${max} tokens`, received: `${total} tokens` };
|
|
497
|
+
const gap = coverageGap(ctx, "usage");
|
|
498
|
+
if (gap) return gap;
|
|
499
|
+
return 1;
|
|
336
500
|
},
|
|
337
501
|
};
|
|
338
502
|
}
|
|
@@ -343,7 +507,10 @@ export function maxCost(usd: number): Spec {
|
|
|
343
507
|
severity: "gate",
|
|
344
508
|
evaluate: (ctx) => {
|
|
345
509
|
const cost = ctx.usage.costUSD ?? 0;
|
|
346
|
-
|
|
510
|
+
if (cost > usd) return { score: 0, expected: `≤ $${usd}`, received: `$${cost.toFixed(4)}` };
|
|
511
|
+
const gap = coverageGap(ctx, "usage");
|
|
512
|
+
if (gap) return gap;
|
|
513
|
+
return 1;
|
|
347
514
|
},
|
|
348
515
|
};
|
|
349
516
|
}
|