niceeval 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +69 -7
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +57 -3
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +32 -24
- package/dist/report/aggregate.js +158 -50
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +93 -160
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +87 -81
- package/dist/report/compute.js +597 -417
- package/dist/report/flag.d.ts +32 -6
- package/dist/report/flag.js +92 -4
- package/dist/report/format.d.ts +19 -11
- package/dist/report/format.js +30 -13
- package/dist/report/index.d.ts +16 -16
- package/dist/report/index.js +20 -21
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +57 -33
- package/dist/report/locale.js +122 -56
- package/dist/report/metrics.d.ts +23 -4
- package/dist/report/metrics.js +110 -25
- package/dist/report/primitives.d.ts +48 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +9 -7
- package/dist/report/react/AttemptList.js +17 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +4 -4
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +10 -0
- package/dist/report/react/ExperimentComparison.js +12 -0
- package/dist/report/react/ExperimentList.d.ts +4 -3
- package/dist/report/react/ExperimentList.js +17 -18
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +59 -28
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +110 -147
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -16
- package/dist/report/report.js +259 -28
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +253 -184
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +38 -7
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +247 -284
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +49 -14
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +4 -2
- package/dist/shared/aggregate.js +8 -7
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
- package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
- package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
- package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
- package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
- package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
- package/docs-site/zh/how-to/publish-report.mdx +105 -0
- package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
- package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
- package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
- package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
- package/docs-site/zh/index.mdx +24 -26
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +32 -5
- package/docs-site/zh/reference/capabilities.mdx +8 -8
- package/docs-site/zh/reference/cli.mdx +40 -12
- package/docs-site/zh/reference/define-agent.mdx +58 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
- package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
- package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
- package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
- package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +10 -2
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +13 -2
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +71 -8
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +446 -124
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +162 -30
- package/src/context/session.test.ts +2 -1
- package/src/context/session.ts +115 -7
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +81 -17
- package/src/i18n/zh-CN.ts +80 -17
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +178 -61
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -279
- package/src/report/compute.ts +723 -491
- package/src/report/dual-render.test.tsx +741 -1024
- package/src/report/flag.ts +104 -12
- package/src/report/format.ts +32 -12
- package/src/report/index.ts +119 -46
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +136 -65
- package/src/report/metrics.ts +108 -25
- package/src/report/primitives.tsx +196 -45
- package/src/report/react/AttemptList.tsx +30 -43
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +73 -0
- package/src/report/react/ExperimentList.tsx +50 -32
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +86 -34
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +89 -5
- package/src/report/react/fixtures.ts +114 -154
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +138 -158
- package/src/report/react/styles.css +243 -82
- package/src/report/report.test.ts +779 -841
- package/src/report/report.ts +423 -41
- package/src/report/text/faces.ts +290 -193
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +44 -7
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +261 -271
- package/src/report/web.ts +63 -20
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +13 -11
- package/src/results/attempt-evidence.ts +20 -13
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +150 -60
- package/src/results/host-equivalence.test.ts +34 -20
- package/src/results/index.ts +12 -4
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +15 -5
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +89 -54
- package/src/results/select.ts +104 -34
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +43 -14
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +243 -37
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +8 -6
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +127 -56
- package/src/show/render.ts +662 -131
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +320 -54
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +423 -11
- package/src/view/app/components/CodeView.tsx +41 -14
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +37 -17
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +10 -3
- package/src/view/data.ts +155 -49
- package/src/view/index.ts +56 -41
- package/src/view/server.ts +37 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +227 -0
- package/src/view/view-report.test.ts +167 -62
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
- package/dist/report/built-ins/experiment-comparison.js +0 -13
- package/dist/report/built-ins/index.d.ts +0 -1
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/docs-site/zh/guides/publish-report.mdx +0 -91
- package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
- package/src/report/built-in-user-parity.test.tsx +0 -640
- package/src/report/built-ins/experiment-comparison.tsx +0 -19
- package/src/report/built-ins/index.ts +0 -2
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/eval/cases.md
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import { createEvalContext, type ContextState } from "./context.ts";
|
|
3
|
-
import { includes } from "../expect/index.ts";
|
|
4
|
+
import { commandSucceeded, includes } from "../expect/index.ts";
|
|
5
|
+
import { completeCoverage, resolveAgentCoverage } from "../scoring/coverage.ts";
|
|
4
6
|
import type { Agent, AgentContext, InputRequest, Sandbox, StreamEvent, Turn, TurnInput } from "../types.ts";
|
|
5
7
|
|
|
6
8
|
// 计算工具 + 最终回复"1 + 1 = **2** 哦!😊"——复现截图里的场景:助手回复明明包含 "2",
|
|
@@ -10,6 +12,7 @@ function calculatorAgent(): Agent {
|
|
|
10
12
|
name: "calculator",
|
|
11
13
|
// 测试注入了真实的 fake sandbox,kind: "sandbox" 让 t.sandbox 过沙箱能力守卫。
|
|
12
14
|
kind: "sandbox",
|
|
15
|
+
coverage: completeCoverage,
|
|
13
16
|
async send(_input: TurnInput, ctx: AgentContext): Promise<Turn> {
|
|
14
17
|
ctx.session.capture("sess-1");
|
|
15
18
|
const events: StreamEvent[] = [
|
|
@@ -84,11 +87,12 @@ describe("createEvalContext / TestContext live state", () => {
|
|
|
84
87
|
scripts: state.late.scripts,
|
|
85
88
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
86
89
|
status: "completed",
|
|
90
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
87
91
|
readFile: async () => undefined,
|
|
88
92
|
});
|
|
89
|
-
expect(result.
|
|
90
|
-
expect(result.score).toBe(1);
|
|
91
|
-
expect(result.evidence).toBeUndefined();
|
|
93
|
+
expect(result.outcome).toBe("passed");
|
|
94
|
+
expect(result.outcome === "passed" ? result.score : -1).toBe(1);
|
|
95
|
+
expect(result.outcome === "passed" ? result.evidence : "?").toBeUndefined();
|
|
92
96
|
});
|
|
93
97
|
|
|
94
98
|
it("t.check(...) attaches the actually-checked value as evidence when it fails", async () => {
|
|
@@ -103,10 +107,41 @@ describe("createEvalContext / TestContext live state", () => {
|
|
|
103
107
|
scripts: state.late.scripts,
|
|
104
108
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
105
109
|
status: "completed",
|
|
110
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
106
111
|
readFile: async () => undefined,
|
|
107
112
|
});
|
|
108
|
-
expect(result.
|
|
109
|
-
expect(result.
|
|
113
|
+
expect(result.outcome).toBe("failed");
|
|
114
|
+
expect(result.outcome === "failed" ? result.received : undefined).toBe("1 + 1 = **2** 哦!😊");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("t.check(CommandResult, …) 失败时 received 塌成退出码+输出尾部,evidence 是命令行", async () => {
|
|
118
|
+
const { context, state } = makeContext(calculatorAgent());
|
|
119
|
+
const commandResult = {
|
|
120
|
+
stdout: `\n> test\n> pytest tests/\n${"collecting …\n".repeat(200)}1 failed, 5 passed in 12.72s\n`,
|
|
121
|
+
stderr: "",
|
|
122
|
+
exitCode: 1,
|
|
123
|
+
command: "npm run test",
|
|
124
|
+
};
|
|
125
|
+
context.check(commandResult, commandSucceeded());
|
|
126
|
+
|
|
127
|
+
const [result] = await state.collector.finalize({
|
|
128
|
+
events: [],
|
|
129
|
+
facts: { toolCalls: [], subagentCalls: [], inputRequests: [], parked: false, messageCount: 0, compactions: 0 },
|
|
130
|
+
diff: state.late.diff,
|
|
131
|
+
scripts: state.late.scripts,
|
|
132
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
133
|
+
status: "completed",
|
|
134
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
135
|
+
readFile: async () => undefined,
|
|
136
|
+
});
|
|
137
|
+
expect(result.outcome).toBe("failed");
|
|
138
|
+
if (result.outcome !== "failed") return;
|
|
139
|
+
const [firstLine] = result.received!.split("\n");
|
|
140
|
+
expect(firstLine).toMatch(/^exit 1 · "…/);
|
|
141
|
+
expect(firstLine).toContain("1 failed, 5 passed in 12.72s"); // 摘要面只保留的这一行自含失败计数
|
|
142
|
+
expect(firstLine!.length).toBeLessThan(200);
|
|
143
|
+
expect(result.received).toContain("output tail:"); // 更长尾部保留换行,attempt 首页展开
|
|
144
|
+
expect(result.evidence).toBe("npm run test");
|
|
110
145
|
});
|
|
111
146
|
|
|
112
147
|
it("t.events reflects the turn's events after send(), not an empty snapshot", async () => {
|
|
@@ -147,6 +182,7 @@ function scriptedAgent(turns: Turn[]): Agent & { received: TurnInput[] } {
|
|
|
147
182
|
const agent: Agent = {
|
|
148
183
|
name: "scripted",
|
|
149
184
|
kind: "remote",
|
|
185
|
+
coverage: completeCoverage,
|
|
150
186
|
async send(input: TurnInput) {
|
|
151
187
|
received.push(input);
|
|
152
188
|
const turn = turns[Math.min(i, turns.length - 1)] as Turn;
|
|
@@ -208,6 +244,8 @@ describe("t.respond() / t.respondAll(): structured InputResponse", () => {
|
|
|
208
244
|
await context.send("apply two edits");
|
|
209
245
|
|
|
210
246
|
await expect(context.respond("approve")).rejects.toThrow(/字符串回答无法对位|cannot be matched/);
|
|
247
|
+
// 报错但响应已发出同样违约:agent 只收到最初那一次 send,没有第二次输入。
|
|
248
|
+
expect(agent.received).toHaveLength(1);
|
|
211
249
|
});
|
|
212
250
|
|
|
213
251
|
it("object form { request, optionId } disambiguates when multiple requests are pending", async () => {
|
|
@@ -299,6 +337,7 @@ function baseScoringContext(state: ContextState) {
|
|
|
299
337
|
scripts: state.late.scripts,
|
|
300
338
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
301
339
|
status: "completed" as const,
|
|
340
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
302
341
|
readFile: async () => undefined,
|
|
303
342
|
};
|
|
304
343
|
}
|
|
@@ -314,7 +353,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
|
|
|
314
353
|
|
|
315
354
|
const [result] = await state.collector.finalize(baseScoringContext(state));
|
|
316
355
|
expect(result.name).toBe("parked");
|
|
317
|
-
expect(result.
|
|
356
|
+
expect(result.outcome).toBe("passed");
|
|
318
357
|
});
|
|
319
358
|
|
|
320
359
|
it("turn.noFailedActions() looks only at this turn's own tool calls", async () => {
|
|
@@ -333,7 +372,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
|
|
|
333
372
|
|
|
334
373
|
const [result] = await state.collector.finalize(baseScoringContext(state));
|
|
335
374
|
expect(result.name).toBe("noFailedActions");
|
|
336
|
-
expect(result.
|
|
375
|
+
expect(result.outcome).toBe("failed");
|
|
337
376
|
});
|
|
338
377
|
|
|
339
378
|
it("turn.maxTokens()/turn.maxCost() read this turn's own Turn.usage, not the session total", async () => {
|
|
@@ -347,9 +386,9 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
|
|
|
347
386
|
|
|
348
387
|
const [tokens, cost] = await state.collector.finalize(baseScoringContext(state));
|
|
349
388
|
expect(tokens.name).toBe("maxTokens(1000)");
|
|
350
|
-
expect(tokens.
|
|
389
|
+
expect(tokens.outcome).toBe("passed");
|
|
351
390
|
expect(cost.name).toBe("maxCost(0.01)");
|
|
352
|
-
expect(cost.
|
|
391
|
+
expect(cost.outcome).toBe("failed");
|
|
353
392
|
});
|
|
354
393
|
|
|
355
394
|
it("turn.loadedSkill() reads the skill.loaded event scoped to this turn", async () => {
|
|
@@ -364,7 +403,7 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
|
|
|
364
403
|
turn.loadedSkill("pdf-export");
|
|
365
404
|
|
|
366
405
|
const [result] = await state.collector.finalize(baseScoringContext(state));
|
|
367
|
-
expect(result.
|
|
406
|
+
expect(result.outcome).toBe("passed");
|
|
368
407
|
});
|
|
369
408
|
|
|
370
409
|
// Skill 加载是一等事件,不是「名字叫 load_skill 的工具调用」——adapter 负责归一
|
|
@@ -382,6 +421,6 @@ describe("TurnHandle scoped assertions (parked/loadedSkill/noFailedActions/maxTo
|
|
|
382
421
|
turn.loadedSkill("pdf-export");
|
|
383
422
|
|
|
384
423
|
const [result] = await state.collector.finalize(baseScoringContext(state));
|
|
385
|
-
expect(result.
|
|
424
|
+
expect(result.outcome).toBe("failed");
|
|
386
425
|
});
|
|
387
426
|
});
|
package/src/context/context.ts
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { basename, extname } from "node:path";
|
|
6
6
|
import { SessionManager, RunSession, lastAssistantText } from "./session.ts";
|
|
7
|
-
import { AssertionCollector, computePassed } from "../scoring/collector.ts";
|
|
7
|
+
import { AssertionCollector, computePassed, unavailable } from "../scoring/collector.ts";
|
|
8
|
+
import type { ResolvedCoverage } from "../scoring/coverage.ts";
|
|
8
9
|
import { deepEqual, validateSchema } from "../scoring/match.ts";
|
|
9
10
|
import type { Spec } from "../scoring/collector.ts";
|
|
10
11
|
import * as Scoped from "../scoring/scoped.ts";
|
|
11
12
|
import { buildJudge } from "../scoring/judge.ts";
|
|
12
13
|
import { EvalSkipped, EvalRequirementFailed, TurnFailed } from "./control-flow.ts";
|
|
13
14
|
import { deriveRunFacts } from "../o11y/derive.ts";
|
|
15
|
+
import { diffIsEmpty, diffMatches, emptyDiffData } from "../scoring/diff.ts";
|
|
14
16
|
import { t } from "../i18n/index.ts";
|
|
15
17
|
import { resolveLocalPath } from "../sandbox/paths.ts";
|
|
16
18
|
import { brief } from "../util.ts";
|
|
@@ -73,6 +75,15 @@ export interface ContextDeps {
|
|
|
73
75
|
otel?: import("../o11y/otlp/turn-otel.ts").AgentOtelChannel;
|
|
74
76
|
/** Eval definition directory; used to resolve host-side relative fixture paths. */
|
|
75
77
|
evalBaseDir?: string;
|
|
78
|
+
/** runner 绑定的作用域反馈(t.progress / t.diagnostic 与 adapter ctx 共用实现);
|
|
79
|
+
* 省略时(测试直调)progress 退回 log、diagnostic 静默丢弃。 */
|
|
80
|
+
feedback?: import("../types.ts").ScopedFeedback;
|
|
81
|
+
/** adapter send 在飞时的通知(errored 归因到嵌套的 `agent.run` 阶段用);透传给 SessionManager。 */
|
|
82
|
+
onSendActive?: (active: boolean) => void;
|
|
83
|
+
/** 变更分类账的 send 窗口钩子(仅沙箱型);透传给 SessionManager(见 SessionDeps.ledgerHooks)。 */
|
|
84
|
+
ledgerHooks?: import("./session.ts").SessionDeps["ledgerHooks"];
|
|
85
|
+
/** 每轮 send 的墙钟包络回报(runner 挂 turn 时间树节点);透传给 SessionManager。 */
|
|
86
|
+
onTurn?: import("./session.ts").SessionDeps["onTurn"];
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
/**
|
|
@@ -80,7 +91,7 @@ export interface ContextDeps {
|
|
|
80
91
|
* 这是唯一仍需要构造证据之外强制检查的能力——`t.sandbox.file`/`t.sandbox.fileChanged()` 等直接读沙箱
|
|
81
92
|
* 文件系统,没有沙箱就没有东西可读,不报错会静默返回空结果。其余能力(多轮对话、
|
|
82
93
|
* 工具断言……)都不再问卷式声明,由「做没做到」的构造证据决定,见
|
|
83
|
-
* docs-site/zh/
|
|
94
|
+
* docs-site/zh/explanation/adapter.mdx「能力从哪来」一节。
|
|
84
95
|
*/
|
|
85
96
|
function capabilityGuard(agentName: string, cap: string, method: string): () => never {
|
|
86
97
|
return () => {
|
|
@@ -100,12 +111,16 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
100
111
|
log: deps.log,
|
|
101
112
|
telemetry: deps.telemetry,
|
|
102
113
|
otel: deps.otel,
|
|
114
|
+
feedback: deps.feedback,
|
|
115
|
+
onSendActive: deps.onSendActive,
|
|
116
|
+
onTurn: deps.onTurn,
|
|
117
|
+
ledgerHooks: deps.ledgerHooks,
|
|
103
118
|
});
|
|
104
119
|
const collector = new AssertionCollector();
|
|
105
120
|
const state: ContextState = {
|
|
106
121
|
collector,
|
|
107
122
|
manager,
|
|
108
|
-
late: { diff:
|
|
123
|
+
late: { diff: emptyDiffData(), scripts: {} },
|
|
109
124
|
};
|
|
110
125
|
|
|
111
126
|
async function resolveValue(value: unknown, sc: ScoringContext): Promise<unknown> {
|
|
@@ -113,8 +128,36 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
113
128
|
return value;
|
|
114
129
|
}
|
|
115
130
|
|
|
116
|
-
/**
|
|
131
|
+
/** CommandResult 形状的值(duck-type,不 import sandbox 域):received 按「退出码 + 输出尾部」投影。 */
|
|
132
|
+
function asCommandResult(value: unknown): { stdout: string; stderr: string; exitCode: number; command?: string } | undefined {
|
|
133
|
+
if (!value || typeof value !== "object") return undefined;
|
|
134
|
+
const v = value as { stdout?: unknown; stderr?: unknown; exitCode?: unknown };
|
|
135
|
+
return typeof v.stdout === "string" && typeof v.stderr === "string" && typeof v.exitCode === "number"
|
|
136
|
+
? (value as { stdout: string; stderr: string; exitCode: number; command?: string })
|
|
137
|
+
: undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 断言失败时给 view 看的「实际被检查了什么」,而不是重复 matcher 自己的名字。
|
|
141
|
+
* 按值的形状落成人可读事实(而不是留一坨 JSON 给渲染层解析):CommandResult 的第一行是
|
|
142
|
+
* `exit N · "…输出尾部摘要"`(stdout+stderr 合并折单行,信号常收在末尾——pytest / vitest
|
|
143
|
+
* 的 failed 计数都在最后几行;榜单与 --eval 标注这类单行面只保留这一行),随后附原样保留
|
|
144
|
+
* 换行的更长尾部——runner 不另存 eval 侧命令的输出,这条记录就是它唯一的家,attempt 首页
|
|
145
|
+
* 与 result.json 靠它给出「更进一步」;文件引用带 `// path` 头;其余走通用 JSON 预览。 */
|
|
117
146
|
function previewCheckedValue(value: unknown): string {
|
|
147
|
+
const cmd = asCommandResult(value);
|
|
148
|
+
if (cmd) {
|
|
149
|
+
const combined = `${cmd.stdout}\n${cmd.stderr}`.trim();
|
|
150
|
+
const folded = combined.replace(/\s+/g, " ").trim();
|
|
151
|
+
const summary = folded.length > 160 ? `…${folded.slice(-159)}` : folded;
|
|
152
|
+
const headline = summary.length > 0 ? `exit ${cmd.exitCode} · "${summary}"` : `exit ${cmd.exitCode}`;
|
|
153
|
+
if (combined.length <= 160) return headline;
|
|
154
|
+
let tail = combined.slice(-3600);
|
|
155
|
+
if (combined.length > 3600) {
|
|
156
|
+
const firstBreak = tail.indexOf("\n");
|
|
157
|
+
if (firstBreak >= 0) tail = tail.slice(firstBreak + 1); // 不从半行开始
|
|
158
|
+
}
|
|
159
|
+
return `${headline}\noutput tail:\n${tail}`;
|
|
160
|
+
}
|
|
118
161
|
if (value && typeof value === "object" && typeof (value as { path?: unknown }).path === "string") {
|
|
119
162
|
const content = (value as { content?: unknown }).content;
|
|
120
163
|
if (typeof content === "string") return brief(`// ${(value as { path: string }).path}\n${content}`, 4000);
|
|
@@ -122,13 +165,17 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
122
165
|
return brief(value, 4000);
|
|
123
166
|
}
|
|
124
167
|
|
|
168
|
+
/** 失败断言的 evidence:被检查值自带命令摘要(CommandResult.command)时就是「命令行本身」。 */
|
|
169
|
+
function checkedValueEvidence(value: unknown): string | undefined {
|
|
170
|
+
const command = asCommandResult(value)?.command;
|
|
171
|
+
return typeof command === "string" && command.length > 0 ? command : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// agent 归因 diff 的只读视图:get = 最后触及窗口的终态;matches 扫触及路径与各窗口内容。
|
|
125
175
|
const diffView: DiffView = {
|
|
126
|
-
get: (path) => state.late.diff.
|
|
127
|
-
isEmpty: () =>
|
|
128
|
-
|
|
129
|
-
state.late.diff.deletedFiles.length === 0,
|
|
130
|
-
matches: (re) =>
|
|
131
|
-
Object.entries(state.late.diff.generatedFiles).some(([p, c]) => re.test(p) || re.test(c)),
|
|
176
|
+
get: (path) => state.late.diff.get(path),
|
|
177
|
+
isEmpty: () => diffIsEmpty(state.late.diff),
|
|
178
|
+
matches: (re) => diffMatches(state.late.diff, re),
|
|
132
179
|
};
|
|
133
180
|
|
|
134
181
|
const sandboxAssertions = {
|
|
@@ -168,6 +215,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
168
215
|
getEvents: () => readonly StreamEvent[],
|
|
169
216
|
getStatus: () => "completed" | "failed" | "waiting",
|
|
170
217
|
getUsage: () => Usage,
|
|
218
|
+
getCoverage: () => ResolvedCoverage,
|
|
171
219
|
) {
|
|
172
220
|
return collector.record({
|
|
173
221
|
...spec,
|
|
@@ -179,6 +227,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
179
227
|
facts: deriveRunFacts(events),
|
|
180
228
|
status: getStatus(),
|
|
181
229
|
usage: getUsage(),
|
|
230
|
+
coverage: getCoverage(),
|
|
182
231
|
});
|
|
183
232
|
},
|
|
184
233
|
});
|
|
@@ -195,24 +244,34 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
195
244
|
}
|
|
196
245
|
|
|
197
246
|
function makeSessionHandle(session: RunSession): SessionHandle {
|
|
198
|
-
|
|
199
|
-
|
|
247
|
+
// session 作用域 = 记录断言时快照(见 docs/feature/scoring/architecture/scopes.md):
|
|
248
|
+
// 之后该 session 再发生的轮次不改变这条断言的评估材料;只看最后一轮用 send() 的 TurnHandle。
|
|
249
|
+
const scoped = (spec: Spec) => {
|
|
250
|
+
const events = session.events.slice();
|
|
251
|
+
const status = session.lastStatus;
|
|
252
|
+
const usage = { ...session.usage };
|
|
253
|
+
const coverage = session.coverage;
|
|
254
|
+
return recordScoped(spec, () => events, () => status, () => usage, () => coverage);
|
|
255
|
+
};
|
|
200
256
|
|
|
201
257
|
const handle: SessionHandle = {
|
|
202
|
-
send: async (
|
|
203
|
-
|
|
204
|
-
|
|
258
|
+
send: async (input) => {
|
|
259
|
+
const text = typeof input === "string" ? input : input.text;
|
|
260
|
+
const files = typeof input === "string" ? undefined : input.files;
|
|
261
|
+
const turn = await manager.send(session, text, files);
|
|
262
|
+
return makeTurnHandle(turn, collector, deps, text, manager.resolveTurnCoverage(turn));
|
|
263
|
+
},
|
|
264
|
+
sendFile: async (path, text) => {
|
|
265
|
+
const turn = await manager.send(session, text ?? "", [await readInputFile(path)]);
|
|
266
|
+
return makeTurnHandle(turn, collector, deps, text ?? "", manager.resolveTurnCoverage(turn));
|
|
267
|
+
},
|
|
205
268
|
requireInputRequest: (filter) => requireInputRequest(session, filter),
|
|
206
269
|
respond: async (...answers) => {
|
|
207
270
|
if (answers.length === 0) throw new Error(t("hitl.respondEmpty"));
|
|
208
271
|
const built = buildRespondInput(session, answers);
|
|
209
272
|
session.pendingInputRequests.length = 0;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
collector,
|
|
213
|
-
deps,
|
|
214
|
-
built.text,
|
|
215
|
-
);
|
|
273
|
+
const turn = await manager.send(session, built.text, undefined, built.responses);
|
|
274
|
+
return makeTurnHandle(turn, collector, deps, built.text, manager.resolveTurnCoverage(turn));
|
|
216
275
|
},
|
|
217
276
|
respondAll: async (optionId) => {
|
|
218
277
|
if (session.pendingInputRequests.length === 0) {
|
|
@@ -226,7 +285,8 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
226
285
|
}));
|
|
227
286
|
session.pendingInputRequests.length = 0;
|
|
228
287
|
const input = requests.map(() => optionId).join("\n");
|
|
229
|
-
|
|
288
|
+
const turn = await manager.send(session, input, undefined, responses);
|
|
289
|
+
return makeTurnHandle(turn, collector, deps, input, manager.resolveTurnCoverage(turn));
|
|
230
290
|
},
|
|
231
291
|
get reply() {
|
|
232
292
|
return session.lastMessage;
|
|
@@ -251,7 +311,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
251
311
|
notEvent: (type) => scoped(Scoped.notEventOfType(type)),
|
|
252
312
|
calledSubagent: (name, match) => scoped(Scoped.calledSubagent(name, match)),
|
|
253
313
|
eventOrder: (types) => scoped(Scoped.eventOrder(types)),
|
|
254
|
-
eventsSatisfy: (
|
|
314
|
+
eventsSatisfy: (label, predicate) => scoped(Scoped.eventsSatisfy(label, predicate)),
|
|
255
315
|
maxTokens: (max) => scoped(Scoped.maxTokens(max)),
|
|
256
316
|
maxCost: (usd) => scoped(Scoped.maxCost(usd)),
|
|
257
317
|
get usage() {
|
|
@@ -266,6 +326,12 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
266
326
|
|
|
267
327
|
const primary = makeSessionHandle(manager.primary);
|
|
268
328
|
|
|
329
|
+
// t 作用域 = 整个 attempt:全部 session(含 t.newSession() 开的)的全部轮次,finalize 时对
|
|
330
|
+
// 聚合结果求值(见 docs/feature/scoring/architecture/scopes.md)。newSession 的事件进入这里,
|
|
331
|
+
// 但不进入主 session 的即时 t.reply / t.events 读取视图;t.judge 默认材料仍是主 session 对话。
|
|
332
|
+
const aggregateScoped = (spec: Spec) =>
|
|
333
|
+
recordScoped(spec, () => manager.allEvents, () => manager.lastStatus, () => manager.usage, () => manager.coverage);
|
|
334
|
+
|
|
269
335
|
// 沙箱能力守卫:非沙箱型 agent(kind !== "sandbox")把文件系统类动作替换成「一调用就报清晰错误」。
|
|
270
336
|
// 其余能力(多轮对话、工具断言……)不再问卷式声明——没接 ctx.session 续接存取器的 agent
|
|
271
337
|
// 每轮各是新对话,没吐 action.* 事件的 agent 上正断言自然不命中,负断言按事件完整性证明
|
|
@@ -288,6 +354,12 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
288
354
|
model: deps.model,
|
|
289
355
|
reasoningEffort: deps.reasoningEffort,
|
|
290
356
|
flags: deps.flags,
|
|
357
|
+
// 作用域反馈:scope 固定为 eval.run(runner 按当前阶段归因,eval 不能冒充其它阶段)。
|
|
358
|
+
progress: (u: import("../types.ts").ProgressUpdate) =>
|
|
359
|
+
deps.feedback
|
|
360
|
+
? deps.feedback.progress(u)
|
|
361
|
+
: deps.log(u.current !== undefined && u.total !== undefined ? `${u.message} (${u.current}/${u.total})` : u.message),
|
|
362
|
+
diagnostic: (d: import("../types.ts").DiagnosticInput) => deps.feedback?.diagnostic(d),
|
|
291
363
|
log: deps.log,
|
|
292
364
|
skip: (reason: string) => {
|
|
293
365
|
if (reason.trim().length === 0) throw new Error(t("context.skipEmpty"));
|
|
@@ -297,16 +369,22 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
297
369
|
|
|
298
370
|
check: (value: unknown, assertion: ValueAssertion) => {
|
|
299
371
|
// evaluate 读 spec 自己的 severity/threshold(而不是捕获记录时的快照):
|
|
300
|
-
// 句柄的 .gate()/.atLeast() 会事后改写 spec,
|
|
372
|
+
// 句柄的 .gate()/.atLeast() 会事后改写 spec,received 判定必须和 finalize 同一口径。
|
|
301
373
|
const spec: Spec = {
|
|
302
374
|
name: assertion.name,
|
|
303
375
|
severity: assertion.severity,
|
|
304
376
|
threshold: assertion.threshold,
|
|
377
|
+
...(assertion.isOptional ? { optional: true as const } : {}),
|
|
305
378
|
evaluate: async (sc) => {
|
|
306
379
|
const resolved = await resolveValue(value, sc);
|
|
307
380
|
const score = await assertion.score(resolved);
|
|
308
381
|
if (computePassed(spec.severity, spec.threshold, score)) return score;
|
|
309
|
-
return {
|
|
382
|
+
return {
|
|
383
|
+
score,
|
|
384
|
+
expected: assertion.expected,
|
|
385
|
+
received: previewCheckedValue(resolved),
|
|
386
|
+
...(checkedValueEvidence(resolved) !== undefined ? { evidence: checkedValueEvidence(resolved) } : {}),
|
|
387
|
+
};
|
|
310
388
|
},
|
|
311
389
|
};
|
|
312
390
|
return collector.record(spec);
|
|
@@ -321,13 +399,43 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
321
399
|
name: assertion.name,
|
|
322
400
|
severity: "gate",
|
|
323
401
|
threshold: assertion.threshold,
|
|
324
|
-
evaluate: () =>
|
|
402
|
+
evaluate: () =>
|
|
403
|
+
passed
|
|
404
|
+
? score
|
|
405
|
+
: {
|
|
406
|
+
score,
|
|
407
|
+
expected: assertion.expected,
|
|
408
|
+
received: previewCheckedValue(v),
|
|
409
|
+
...(checkedValueEvidence(v) !== undefined ? { evidence: checkedValueEvidence(v) } : {}),
|
|
410
|
+
},
|
|
325
411
|
});
|
|
326
412
|
if (!passed) throw new EvalRequirementFailed(assertion.name);
|
|
327
413
|
return value;
|
|
328
414
|
},
|
|
329
415
|
|
|
330
416
|
sandbox: sandboxHandle,
|
|
417
|
+
|
|
418
|
+
// 作用域断言(t 级:聚合全部 session)。这些描述符盖过 primary 的同名方法——
|
|
419
|
+
// t.send/t.reply/t.events 仍是主 session 的即时视图,断言聚合与读取视图是两回事。
|
|
420
|
+
succeeded: () => aggregateScoped(Scoped.succeeded()),
|
|
421
|
+
parked: () => aggregateScoped(Scoped.parked()),
|
|
422
|
+
messageIncludes: (token: string | RegExp) => aggregateScoped(Scoped.messageIncludes(token)),
|
|
423
|
+
calledTool: (name: string, match?: import("../types.ts").ToolMatch) => aggregateScoped(Scoped.calledTool(name, match)),
|
|
424
|
+
notCalledTool: (name: string, match?: import("../types.ts").ToolMatch) => aggregateScoped(Scoped.notCalledTool(name, match)),
|
|
425
|
+
toolOrder: (names: string[]) => aggregateScoped(Scoped.toolOrder(names)),
|
|
426
|
+
usedNoTools: () => aggregateScoped(Scoped.usedNoTools()),
|
|
427
|
+
maxToolCalls: (max: number) => aggregateScoped(Scoped.maxToolCalls(max)),
|
|
428
|
+
loadedSkill: (skill: string) => aggregateScoped(Scoped.loadedSkill(skill)),
|
|
429
|
+
noFailedActions: () => aggregateScoped(Scoped.noFailedActions()),
|
|
430
|
+
event: (type: StreamEvent["type"], opts?: { count?: number }) => aggregateScoped(Scoped.eventOfType(type, opts)),
|
|
431
|
+
notEvent: (type: StreamEvent["type"]) => aggregateScoped(Scoped.notEventOfType(type)),
|
|
432
|
+
calledSubagent: (name: string, match?: import("../types.ts").SubagentMatch) =>
|
|
433
|
+
aggregateScoped(Scoped.calledSubagent(name, match)),
|
|
434
|
+
eventOrder: (types: StreamEvent["type"][]) => aggregateScoped(Scoped.eventOrder(types)),
|
|
435
|
+
eventsSatisfy: (label: string, predicate: (events: readonly StreamEvent[]) => boolean) =>
|
|
436
|
+
aggregateScoped(Scoped.eventsSatisfy(label, predicate)),
|
|
437
|
+
maxTokens: (max: number) => aggregateScoped(Scoped.maxTokens(max)),
|
|
438
|
+
maxCost: (usd: number) => aggregateScoped(Scoped.maxCost(usd)),
|
|
331
439
|
};
|
|
332
440
|
const context = Object.defineProperties(
|
|
333
441
|
{},
|
|
@@ -364,7 +472,13 @@ function mimeTypeFor(path: string): string {
|
|
|
364
472
|
}
|
|
365
473
|
}
|
|
366
474
|
|
|
367
|
-
function makeTurnHandle(
|
|
475
|
+
function makeTurnHandle(
|
|
476
|
+
turn: Turn,
|
|
477
|
+
collector: AssertionCollector,
|
|
478
|
+
deps: ContextDeps,
|
|
479
|
+
input: string,
|
|
480
|
+
coverage: ResolvedCoverage,
|
|
481
|
+
): TurnHandle {
|
|
368
482
|
const message = lastAssistantText(turn.events) ?? "";
|
|
369
483
|
const facts = deriveRunFacts(turn.events);
|
|
370
484
|
const usage = turn.usage ?? { inputTokens: 0, outputTokens: 0 };
|
|
@@ -379,6 +493,7 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
|
|
|
379
493
|
facts,
|
|
380
494
|
status: turn.status,
|
|
381
495
|
usage,
|
|
496
|
+
coverage,
|
|
382
497
|
}),
|
|
383
498
|
});
|
|
384
499
|
|
|
@@ -404,13 +519,28 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
|
|
|
404
519
|
collector.record({
|
|
405
520
|
name: "outputEquals",
|
|
406
521
|
severity: "gate",
|
|
407
|
-
evaluate: () =>
|
|
522
|
+
evaluate: () => {
|
|
523
|
+
if (deepEqual(turn.data, value)) return 1;
|
|
524
|
+
// 正断言:data 通道非 complete 且这一轮根本没给 data,「没采到」不能算成「没输出」。
|
|
525
|
+
if (turn.data === undefined && coverage.data.status !== "complete") {
|
|
526
|
+
const c = coverage.data;
|
|
527
|
+
return unavailable(`coverage:data=${c.status}${c.reason ? ` (${c.reason})` : ""}`);
|
|
528
|
+
}
|
|
529
|
+
return { score: 0, expected: brief(value, 800), received: brief(turn.data, 800) };
|
|
530
|
+
},
|
|
408
531
|
}),
|
|
409
532
|
outputMatches: (schema) =>
|
|
410
533
|
collector.record({
|
|
411
534
|
name: "outputMatches",
|
|
412
535
|
severity: "gate",
|
|
413
|
-
evaluate: async () =>
|
|
536
|
+
evaluate: async () => {
|
|
537
|
+
if (await validateSchema(turn.data, schema)) return 1;
|
|
538
|
+
if (turn.data === undefined && coverage.data.status !== "complete") {
|
|
539
|
+
const c = coverage.data;
|
|
540
|
+
return unavailable(`coverage:data=${c.status}${c.reason ? ` (${c.reason})` : ""}`);
|
|
541
|
+
}
|
|
542
|
+
return { score: 0, received: brief(turn.data, 800) };
|
|
543
|
+
},
|
|
414
544
|
}),
|
|
415
545
|
messageIncludes: (token) => scoped(Scoped.messageIncludes(token)),
|
|
416
546
|
succeeded: () => scoped(Scoped.succeeded()),
|
|
@@ -426,7 +556,7 @@ function makeTurnHandle(turn: Turn, collector: AssertionCollector, deps: Context
|
|
|
426
556
|
notEvent: (type) => scoped(Scoped.notEventOfType(type)),
|
|
427
557
|
calledSubagent: (name, match) => scoped(Scoped.calledSubagent(name, match)),
|
|
428
558
|
eventOrder: (types) => scoped(Scoped.eventOrder(types)),
|
|
429
|
-
eventsSatisfy: (
|
|
559
|
+
eventsSatisfy: (label, predicate) => scoped(Scoped.eventsSatisfy(label, predicate)),
|
|
430
560
|
maxTokens: (max) => scoped(Scoped.maxTokens(max)),
|
|
431
561
|
maxCost: (usd) => scoped(Scoped.maxCost(usd)),
|
|
432
562
|
judge: buildJudge({
|
|
@@ -526,7 +656,9 @@ function inputRequestMatches(request: InputRequest, filter?: InputRequestFilter)
|
|
|
526
656
|
if (filter.display !== undefined && !stringMatches(request.display ?? "", filter.display)) return false;
|
|
527
657
|
if (filter.action !== undefined && !stringMatches(request.action ?? "", filter.action)) return false;
|
|
528
658
|
if (filter.optionIds !== undefined) {
|
|
659
|
+
// 「恰好提供这组选项」:集合完全一致(顺序无关),不是子集包含——写少一个选项不算命中。
|
|
529
660
|
const optionIds = new Set((request.options ?? []).map((o) => o.id));
|
|
661
|
+
if (optionIds.size !== filter.optionIds.length) return false;
|
|
530
662
|
if (!filter.optionIds.every((id) => optionIds.has(id))) return false;
|
|
531
663
|
}
|
|
532
664
|
if (filter.input !== undefined && !partialObjectMatches(request.input, filter.input)) return false;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/eval/cases.md
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
|
|
3
4
|
import { createAgentSession, SessionManager } from "./session.ts";
|
|
4
5
|
import type { Agent, Sandbox, StreamEvent, Turn, TurnInput } from "../types.ts";
|
|
5
6
|
|
|
6
7
|
// createAgentSession() 是 ctx.session 的实现——一条会话线的存取器(见
|
|
7
|
-
// docs-site/zh/
|
|
8
|
+
// docs-site/zh/explanation/adapter.mdx 的 AgentSession 契约)。这里直接测存取器本身;
|
|
8
9
|
// 端到端的「同一条线同一个 ctx.session」由 SessionManager / RunSession 保证。
|
|
9
10
|
|
|
10
11
|
function fakeSandbox(): Sandbox {
|