@cursor/july 0.1.16 → 0.1.18
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 +9 -4
- package/dist/bin/agent-serve.js +60 -2
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +13 -3
- package/dist/channels/slack/auth.d.ts +7 -1
- package/dist/channels/slack/auth.d.ts.map +1 -1
- package/dist/channels/slack/auth.js +14 -0
- package/dist/channels/slack/blocks.d.ts +61 -0
- package/dist/channels/slack/blocks.d.ts.map +1 -0
- package/dist/channels/slack/blocks.js +90 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +1 -1
- package/dist/channels/slack/defaults.d.ts +2 -0
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +29 -3
- package/dist/channels/slack/dispatch.d.ts +3 -17
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +4 -1
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +6 -1
- package/dist/channels/slack/index.d.ts +2 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +2 -0
- package/dist/channels/slack/init.d.ts.map +1 -1
- package/dist/channels/slack/init.js +4 -1
- package/dist/channels/slack/interactive.d.ts +39 -3
- package/dist/channels/slack/interactive.d.ts.map +1 -1
- package/dist/channels/slack/interactive.js +153 -7
- package/dist/channels/slack/manifest.d.ts +13 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +36 -14
- package/dist/channels/slack/message-body.d.ts +13 -0
- package/dist/channels/slack/message-body.d.ts.map +1 -0
- package/dist/channels/slack/message-body.js +82 -0
- package/dist/channels/slack/setup.d.ts +4 -1
- package/dist/channels/slack/setup.d.ts.map +1 -1
- package/dist/channels/slack/setup.js +16 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +24 -3
- package/dist/channels/slack/thread-context.d.ts +0 -2
- package/dist/channels/slack/thread-context.d.ts.map +1 -1
- package/dist/channels/slack/thread-context.js +1 -65
- package/dist/channels/slack/types.d.ts +93 -1
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +4 -1
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +4 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.DjHC-Ggf.js → app.BR5EdVSq.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DwviwmIq.js → VPLocalSearchBox.CNCzfUKC.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BIpeYSon.js → theme.CwKvMSWv.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.js → example-agents_security-reviewer.md.Dhj_m7_B.js} +1 -1
- package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.js → guides_mcp-oauth.md.Dd8EgSem.js} +3 -2
- package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.lean.js → guides_mcp-oauth.md.Dd8EgSem.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.FWale6Ip.js → guides_slack.md.D_xFKlnF.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.CsSr3rDz.js → reference_connections.md.B9Q3TOve.js} +12 -5
- package/dist/docs/assets/{reference_connections.md.CsSr3rDz.lean.js → reference_connections.md.B9Q3TOve.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +5 -5
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +6 -5
- package/dist/docs/guides/slack.html +5 -5
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +14 -7
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals/assertions.d.ts +190 -0
- package/dist/evals/assertions.d.ts.map +1 -0
- package/dist/evals/assertions.js +461 -0
- package/dist/evals/expect.d.ts +95 -0
- package/dist/evals/expect.d.ts.map +1 -0
- package/dist/evals/expect.js +194 -0
- package/dist/evals/judge.d.ts +133 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +228 -0
- package/dist/evals/loaders.d.ts +51 -0
- package/dist/evals/loaders.d.ts.map +1 -0
- package/dist/evals/loaders.js +124 -0
- package/dist/evals/matchers.d.ts +95 -0
- package/dist/evals/matchers.d.ts.map +1 -0
- package/dist/evals/matchers.js +145 -0
- package/dist/evals/reporters.d.ts +70 -0
- package/dist/evals/reporters.d.ts.map +1 -0
- package/dist/evals/reporters.js +221 -0
- package/dist/evals/results.d.ts +103 -0
- package/dist/evals/results.d.ts.map +1 -0
- package/dist/evals/results.js +28 -0
- package/dist/evals/run-facts.d.ts +77 -0
- package/dist/evals/run-facts.d.ts.map +1 -0
- package/dist/evals/run-facts.js +187 -0
- package/dist/evals.d.ts +118 -31
- package/dist/evals.d.ts.map +1 -1
- package/dist/evals.js +46 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtin-tools/index.d.ts +10 -0
- package/dist/internal/builtin-tools/index.d.ts.map +1 -0
- package/dist/internal/builtin-tools/index.js +27 -0
- package/dist/internal/builtin-tools/reminders.d.ts +9 -0
- package/dist/internal/builtin-tools/reminders.d.ts.map +1 -0
- package/dist/internal/builtin-tools/reminders.js +138 -0
- package/dist/internal/cli-ax.d.ts +27 -0
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +90 -12
- package/dist/internal/cli-mcp.d.ts +8 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -1
- package/dist/internal/cli-mcp.js +98 -0
- package/dist/internal/cli-slack.d.ts +29 -1
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +799 -13
- package/dist/internal/cursor-event-relay.d.ts +30 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +37 -0
- package/dist/internal/deploy-client.d.ts +7 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +9 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +70 -9
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +2 -0
- package/dist/internal/eval-judge-model.d.ts +59 -0
- package/dist/internal/eval-judge-model.d.ts.map +1 -0
- package/dist/internal/eval-judge-model.js +131 -0
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +12 -20
- package/dist/internal/eval-runner.d.ts +31 -25
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +329 -126
- package/dist/internal/evals-client.d.ts.map +1 -1
- package/dist/internal/evals-client.js +4 -1
- package/dist/internal/local-env.d.ts +8 -0
- package/dist/internal/local-env.d.ts.map +1 -0
- package/dist/internal/local-env.js +47 -0
- package/dist/internal/playground/static.d.ts +3 -0
- package/dist/internal/playground/static.d.ts.map +1 -1
- package/dist/internal/playground/static.js +3 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +21 -0
- package/dist/internal/run-client.d.ts +15 -0
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +3 -2
- package/dist/internal/sdk-runner.d.ts +51 -1
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +176 -36
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +11 -1
- package/dist/internal/session-engine.d.ts +18 -5
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +81 -19
- package/dist/internal/slack-provision-client.d.ts +133 -0
- package/dist/internal/slack-provision-client.d.ts.map +1 -0
- package/dist/internal/slack-provision-client.js +286 -0
- package/dist/playground/assets/cursor-icons-16-f_W_ogc-.woff2 +0 -0
- package/dist/playground/assets/index-DRjS5klQ.js +85 -0
- package/dist/playground/assets/index-TCDs-UhZ.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +98 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -0
- package/docs/example-agents/security-reviewer.md +1 -1
- package/docs/guides/mcp-oauth.md +10 -5
- package/docs/guides/slack.md +3 -2
- package/docs/reference/connections.md +28 -18
- package/package.json +20 -2
- package/skills/create-agent/SKILL.md +15 -7
- package/skills/debug/SKILL.md +3 -1
- package/skills/mcp-auth/SKILL.md +1 -1
- package/skills/setup-slack/SKILL.md +177 -11
- package/src/bin/agent-serve.ts +63 -2
- package/src/channels/slack/api.ts +16 -3
- package/src/channels/slack/auth.ts +16 -1
- package/src/channels/slack/blocks.ts +139 -0
- package/src/channels/slack/cursor-account.ts +1 -0
- package/src/channels/slack/defaults.ts +32 -4
- package/src/channels/slack/dispatch.ts +6 -19
- package/src/channels/slack/inbound.ts +6 -1
- package/src/channels/slack/index.ts +2 -0
- package/src/channels/slack/init.ts +3 -0
- package/src/channels/slack/interactive.ts +184 -19
- package/src/channels/slack/manifest.ts +37 -6
- package/src/channels/slack/message-body.ts +90 -0
- package/src/channels/slack/setup.ts +16 -2
- package/src/channels/slack/slack-channel.ts +37 -6
- package/src/channels/slack/thread-context.ts +1 -69
- package/src/channels/slack/types.ts +105 -1
- package/src/connections.ts +4 -1
- package/src/evals/assertions.ts +704 -0
- package/src/evals/expect.ts +272 -0
- package/src/evals/judge.ts +346 -0
- package/src/evals/loaders.ts +113 -0
- package/src/evals/matchers.ts +246 -0
- package/src/evals/reporters.ts +274 -0
- package/src/evals/results.ts +132 -0
- package/src/evals/run-facts.ts +274 -0
- package/src/evals.ts +205 -42
- package/src/index.ts +2 -0
- package/src/internal/builtin-tools/index.ts +40 -0
- package/src/internal/builtin-tools/reminders.ts +158 -0
- package/src/internal/cli-ax.ts +134 -15
- package/src/internal/cli-mcp.ts +118 -5
- package/src/internal/cli-slack.ts +1017 -15
- package/src/internal/cursor-event-relay.ts +67 -0
- package/src/internal/deploy-client.ts +10 -0
- package/src/internal/discovery.ts +83 -5
- package/src/internal/distribution.ts +2 -0
- package/src/internal/eval-judge-model.ts +148 -0
- package/src/internal/eval-run-store.ts +10 -14
- package/src/internal/eval-runner.ts +460 -214
- package/src/internal/evals-client.ts +5 -0
- package/src/internal/local-env.ts +53 -0
- package/src/internal/playground/static.ts +3 -0
- package/src/internal/reminder-runner.ts +30 -0
- package/src/internal/run-client.ts +17 -1
- package/src/internal/sdk-runner.ts +234 -35
- package/src/internal/server.ts +12 -0
- package/src/internal/session-engine.ts +103 -21
- package/src/internal/slack-provision-client.ts +442 -0
- package/src/types.ts +125 -4
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DZtu0ZIc.js +0 -1
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
- package/dist/playground/assets/index-Bwl4Onx4.js +0 -85
- package/dist/playground/assets/index-ByV4nfch.css +0 -1
- /package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.lean.js → example-agents_security-reviewer.md.Dhj_m7_B.lean.js} +0 -0
- /package/dist/docs/assets/{guides_slack.md.FWale6Ip.lean.js → guides_slack.md.D_xFKlnF.lean.js} +0 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assertion recording and the scoped assertion vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Recorded assertions never throw: the recorder collects every result so one
|
|
5
|
+
* run reports all failures instead of dying on the first. Severity rides on the
|
|
6
|
+
* returned handle, so there is no separate thresholds map:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* t.succeeded(); // gate (hard)
|
|
10
|
+
* t.calledTool("get_weather").soft(); // tracked, does not gate
|
|
11
|
+
* t.check(t.reply, similarity("Sunny")).atLeast(0.8);
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The same vocabulary is available on the run (`t`) and on each turn returned
|
|
15
|
+
* by `t.send(...)`, which narrows the events under inspection to that turn.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { SessionEvent } from "../types.js";
|
|
19
|
+
import type { Expectation } from "./expect.js";
|
|
20
|
+
import { evaluateExpectation, expectationSeverity } from "./expect.js";
|
|
21
|
+
import type {
|
|
22
|
+
EvalCountMatcher,
|
|
23
|
+
EvalEventMatcher,
|
|
24
|
+
EvalEventOrderMatcher,
|
|
25
|
+
EvalMatcher,
|
|
26
|
+
EvalSubagentMatcher,
|
|
27
|
+
EvalToolCallMatcher,
|
|
28
|
+
} from "./matchers.js";
|
|
29
|
+
import { describeMatcherObject, matchCount, matchValue } from "./matchers.js";
|
|
30
|
+
import type {
|
|
31
|
+
EvalInputRequestFact,
|
|
32
|
+
EvalRunFacts,
|
|
33
|
+
EvalToolCallFact,
|
|
34
|
+
} from "./run-facts.js";
|
|
35
|
+
import { deriveRunFacts } from "./run-facts.js";
|
|
36
|
+
|
|
37
|
+
/** Hard gates fail the eval; soft assertions are tracked scores. */
|
|
38
|
+
export type EvalAssertionSeverity = "gate" | "soft";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Overall grade for one eval case.
|
|
42
|
+
*
|
|
43
|
+
* - `passed` — every gate passed and no soft threshold was missed.
|
|
44
|
+
* - `failed` — a gate failed, or the test body threw.
|
|
45
|
+
* - `scored` — only soft thresholds were missed: visible, and fatal only under
|
|
46
|
+
* `--strict`.
|
|
47
|
+
* - `skipped` — `t.skip(reason)`, or a judge with no credentials available.
|
|
48
|
+
*/
|
|
49
|
+
export type EvalVerdict = "passed" | "failed" | "scored" | "skipped";
|
|
50
|
+
|
|
51
|
+
export interface EvalAssertionResult {
|
|
52
|
+
name: string;
|
|
53
|
+
passed: boolean;
|
|
54
|
+
detail?: string;
|
|
55
|
+
/** Defaults to `gate`; omitted on the wire for gates to stay compact. */
|
|
56
|
+
severity?: EvalAssertionSeverity;
|
|
57
|
+
/** 0–1 score for scored assertions (`similarity`, judges). */
|
|
58
|
+
score?: number;
|
|
59
|
+
/** Bar a scored assertion must clear. */
|
|
60
|
+
threshold?: number;
|
|
61
|
+
/** Set when the assertion inspects one turn rather than the whole run. */
|
|
62
|
+
scope?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Chainable severity control returned by every assertion.
|
|
67
|
+
*
|
|
68
|
+
* - `.gate(threshold?)` is hard: a miss fails the eval and `eval` exits 1.
|
|
69
|
+
* - `.soft(threshold?)` is tracked: a below-threshold miss marks the eval
|
|
70
|
+
* `scored`, fatal only under `--strict`. With no threshold it never fails.
|
|
71
|
+
* - `.atLeast(threshold)` is soft with a bar.
|
|
72
|
+
*/
|
|
73
|
+
export interface EvalAssertionHandle {
|
|
74
|
+
gate(threshold?: number): EvalAssertionHandle;
|
|
75
|
+
soft(threshold?: number): EvalAssertionHandle;
|
|
76
|
+
atLeast(threshold: number): EvalAssertionHandle;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AssertionSlot extends EvalAssertionResult {
|
|
80
|
+
severity: EvalAssertionSeverity;
|
|
81
|
+
/** Recorded boolean outcome, before any threshold is applied. */
|
|
82
|
+
rawPassed: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Thrown by `t.require(...)` to stop the test body without an error result. */
|
|
86
|
+
export class EvalRequireFailure extends Error {
|
|
87
|
+
constructor(readonly assertionName: string) {
|
|
88
|
+
super(`required assertion failed: ${assertionName}`);
|
|
89
|
+
this.name = "EvalRequireFailure";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Thrown by `t.skip(reason)` to end the case as skipped. */
|
|
94
|
+
export class EvalSkipSignal extends Error {
|
|
95
|
+
constructor(readonly reason: string) {
|
|
96
|
+
super(reason);
|
|
97
|
+
this.name = "EvalSkipSignal";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface RecordAssertionInput {
|
|
102
|
+
name: string;
|
|
103
|
+
passed: boolean;
|
|
104
|
+
detail?: string;
|
|
105
|
+
severity?: EvalAssertionSeverity;
|
|
106
|
+
score?: number;
|
|
107
|
+
threshold?: number;
|
|
108
|
+
scope?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Collects assertion results and any in-flight judge scoring for one case.
|
|
113
|
+
*/
|
|
114
|
+
export class EvalAssertionRecorder {
|
|
115
|
+
private readonly slots: AssertionSlot[] = [];
|
|
116
|
+
private readonly pending: Array<Promise<void>> = [];
|
|
117
|
+
|
|
118
|
+
record(input: RecordAssertionInput): EvalAssertionHandle {
|
|
119
|
+
const slot: AssertionSlot = {
|
|
120
|
+
name: input.name,
|
|
121
|
+
passed: input.passed,
|
|
122
|
+
rawPassed: input.passed,
|
|
123
|
+
detail: input.detail,
|
|
124
|
+
severity: input.severity ?? "gate",
|
|
125
|
+
score: input.score,
|
|
126
|
+
threshold: input.threshold,
|
|
127
|
+
scope: input.scope,
|
|
128
|
+
};
|
|
129
|
+
applyThreshold(slot);
|
|
130
|
+
this.slots.push(slot);
|
|
131
|
+
return handleFor(slot);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reserve a slot now and fill it in when `settle` resolves. Judges start when
|
|
136
|
+
* recorded so several graders overlap; {@link waitForPending} joins them.
|
|
137
|
+
*/
|
|
138
|
+
recordAsync(
|
|
139
|
+
input: RecordAssertionInput,
|
|
140
|
+
settle: Promise<Partial<RecordAssertionInput>>
|
|
141
|
+
): EvalAssertionHandle {
|
|
142
|
+
const handle = this.record(input);
|
|
143
|
+
const slot = this.slots[this.slots.length - 1]!;
|
|
144
|
+
this.pending.push(
|
|
145
|
+
settle.then(
|
|
146
|
+
(patch) => {
|
|
147
|
+
Object.assign(slot, patch);
|
|
148
|
+
if (patch.passed !== undefined) {
|
|
149
|
+
slot.rawPassed = patch.passed;
|
|
150
|
+
}
|
|
151
|
+
applyThreshold(slot);
|
|
152
|
+
},
|
|
153
|
+
(error: unknown) => {
|
|
154
|
+
// Grading blew up, which is a broken assertion rather than a low
|
|
155
|
+
// score. Soft severity covers "the model scored badly", so promote to
|
|
156
|
+
// a gate here or the failure would be invisible in the verdict.
|
|
157
|
+
slot.rawPassed = false;
|
|
158
|
+
slot.passed = false;
|
|
159
|
+
slot.score = undefined;
|
|
160
|
+
slot.threshold = undefined;
|
|
161
|
+
slot.severity = "gate";
|
|
162
|
+
slot.detail = error instanceof Error ? error.message : String(error);
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
return handle;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Await every async assertion started during the test body. */
|
|
170
|
+
async waitForPending(): Promise<void> {
|
|
171
|
+
while (this.pending.length > 0) {
|
|
172
|
+
const inFlight = this.pending.splice(0, this.pending.length);
|
|
173
|
+
await Promise.all(inFlight);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
results(): EvalAssertionResult[] {
|
|
178
|
+
return this.slots.map((slot) => ({
|
|
179
|
+
name: slot.name,
|
|
180
|
+
passed: slot.passed,
|
|
181
|
+
detail: slot.detail,
|
|
182
|
+
// Keep the wire shape compact: `gate` is the default everywhere.
|
|
183
|
+
severity: slot.severity === "gate" ? undefined : slot.severity,
|
|
184
|
+
score: slot.score,
|
|
185
|
+
threshold: slot.threshold,
|
|
186
|
+
scope: slot.scope,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
get count(): number {
|
|
191
|
+
return this.slots.length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Re-derive `passed` from the score whenever a threshold is in play.
|
|
197
|
+
*
|
|
198
|
+
* A tracked score with no bar always passes — there is nothing to miss. A
|
|
199
|
+
* boolean assertion keeps its recorded outcome even when demoted to soft, so
|
|
200
|
+
* the report stays honest; severity decides the verdict, not the `passed` flag.
|
|
201
|
+
*/
|
|
202
|
+
function applyThreshold(slot: AssertionSlot): void {
|
|
203
|
+
if (slot.threshold === undefined) {
|
|
204
|
+
slot.passed = slot.score !== undefined ? true : slot.rawPassed;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const score = slot.score ?? (slot.rawPassed ? 1 : 0);
|
|
208
|
+
slot.passed = score >= slot.threshold;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleFor(slot: AssertionSlot): EvalAssertionHandle {
|
|
212
|
+
const handle: EvalAssertionHandle = {
|
|
213
|
+
gate(threshold?: number) {
|
|
214
|
+
slot.severity = "gate";
|
|
215
|
+
if (threshold !== undefined) {
|
|
216
|
+
slot.threshold = threshold;
|
|
217
|
+
}
|
|
218
|
+
applyThreshold(slot);
|
|
219
|
+
return handle;
|
|
220
|
+
},
|
|
221
|
+
soft(threshold?: number) {
|
|
222
|
+
slot.severity = "soft";
|
|
223
|
+
if (threshold !== undefined) {
|
|
224
|
+
slot.threshold = threshold;
|
|
225
|
+
}
|
|
226
|
+
applyThreshold(slot);
|
|
227
|
+
return handle;
|
|
228
|
+
},
|
|
229
|
+
atLeast(threshold: number) {
|
|
230
|
+
return handle.soft(threshold);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
return handle;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Grade a case from its recorded assertions.
|
|
238
|
+
*
|
|
239
|
+
* With no assertions at all, the caller's fallback applies (at least one
|
|
240
|
+
* successful turn), which keeps pre-severity eval files working unchanged.
|
|
241
|
+
*/
|
|
242
|
+
export function computeVerdict(
|
|
243
|
+
results: readonly EvalAssertionResult[]
|
|
244
|
+
): Exclude<EvalVerdict, "skipped"> {
|
|
245
|
+
const failedGate = results.some(
|
|
246
|
+
(r) => (r.severity ?? "gate") === "gate" && !r.passed
|
|
247
|
+
);
|
|
248
|
+
if (failedGate) {
|
|
249
|
+
return "failed";
|
|
250
|
+
}
|
|
251
|
+
const missedSoftBar = results.some(
|
|
252
|
+
(r) =>
|
|
253
|
+
(r.severity ?? "gate") === "soft" &&
|
|
254
|
+
r.threshold !== undefined &&
|
|
255
|
+
!r.passed
|
|
256
|
+
);
|
|
257
|
+
return missedSoftBar ? "scored" : "passed";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Whether a verdict should fail the process exit code. */
|
|
261
|
+
export function verdictFailsRun(
|
|
262
|
+
verdict: EvalVerdict,
|
|
263
|
+
strict: boolean
|
|
264
|
+
): boolean {
|
|
265
|
+
if (verdict === "failed") {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
return strict && verdict === "scored";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The deterministic assertion vocabulary, available on the run (`t`) and on
|
|
273
|
+
* each turn returned by `t.send(...)`.
|
|
274
|
+
*/
|
|
275
|
+
export interface EvalAssertions {
|
|
276
|
+
/**
|
|
277
|
+
* Gate: the run did not fail and is not parked on an unanswered approval.
|
|
278
|
+
* A session left open for the next message is a healthy end state.
|
|
279
|
+
*/
|
|
280
|
+
succeeded(): EvalAssertionHandle;
|
|
281
|
+
/** Gate: the run cleanly parked on an unanswered human approval request. */
|
|
282
|
+
parked(): EvalAssertionHandle;
|
|
283
|
+
/** Gate: joined assistant text contains `token`. */
|
|
284
|
+
messageIncludes(token: string | RegExp): EvalAssertionHandle;
|
|
285
|
+
/**
|
|
286
|
+
* Gate: a matching call to `name` happened.
|
|
287
|
+
*
|
|
288
|
+
* With no matcher this is request-based (any lifecycle state), so it keeps
|
|
289
|
+
* meaning "the agent used this tool". Pass `{ status: "completed" }` to
|
|
290
|
+
* require the call actually returned, or `{ input, output, count }` to
|
|
291
|
+
* constrain it further.
|
|
292
|
+
*/
|
|
293
|
+
calledTool(name: string, matcher?: EvalToolCallMatcher): EvalAssertionHandle;
|
|
294
|
+
/** Gate: no request for `name` in any lifecycle state. */
|
|
295
|
+
notCalledTool(name: string): EvalAssertionHandle;
|
|
296
|
+
/**
|
|
297
|
+
* Gate: the agent opened the skill's `SKILL.md`.
|
|
298
|
+
*
|
|
299
|
+
* Skills are materialized as files rather than exposed as a tool, so this
|
|
300
|
+
* matches any tool call whose input references `skills/<name>/SKILL.md`
|
|
301
|
+
* (a file read, a grep, or a shell `cat` all count).
|
|
302
|
+
*/
|
|
303
|
+
loadedSkill(name: string): EvalAssertionHandle;
|
|
304
|
+
/** Gate: tool requests appear in this relative order (extra calls allowed). */
|
|
305
|
+
toolOrder(names: readonly string[]): EvalAssertionHandle;
|
|
306
|
+
/** Gate: no tool calls at all. */
|
|
307
|
+
usedNoTools(): EvalAssertionHandle;
|
|
308
|
+
/** Gate: at most `max` tool calls. */
|
|
309
|
+
maxToolCalls(max: number): EvalAssertionHandle;
|
|
310
|
+
/** Gate: no tool call reported an error. */
|
|
311
|
+
noFailedActions(): EvalAssertionHandle;
|
|
312
|
+
/** Gate: a matching subagent delegation happened. */
|
|
313
|
+
calledSubagent(
|
|
314
|
+
name: string,
|
|
315
|
+
matcher?: EvalSubagentMatcher
|
|
316
|
+
): EvalAssertionHandle;
|
|
317
|
+
/** Gate: at least one matching event of `type` occurred. */
|
|
318
|
+
event(type: string, matcher?: EvalEventMatcher): EvalAssertionHandle;
|
|
319
|
+
/** Gate: no matching event of `type` occurred. */
|
|
320
|
+
notEvent(type: string, matcher?: EvalEventMatcher): EvalAssertionHandle;
|
|
321
|
+
/** Gate: matching event groups occur in this relative order. */
|
|
322
|
+
eventOrder(matchers: readonly EvalEventOrderMatcher[]): EvalAssertionHandle;
|
|
323
|
+
/** Gate: escape hatch for any predicate over the typed event stream. */
|
|
324
|
+
eventsSatisfy(
|
|
325
|
+
label: string,
|
|
326
|
+
predicate: (events: readonly SessionEvent[]) => boolean
|
|
327
|
+
): EvalAssertionHandle;
|
|
328
|
+
/** Grade an explicit value with an `Expectation` builder. */
|
|
329
|
+
check(value: unknown, assertion: Expectation): EvalAssertionHandle;
|
|
330
|
+
/**
|
|
331
|
+
* Record a 0–1 score the eval computed itself, for a benchmark the built-in
|
|
332
|
+
* builders cannot express (recall against a gold set, a custom judge).
|
|
333
|
+
*
|
|
334
|
+
* Soft and tracked-only by default, so a new benchmark can report its number
|
|
335
|
+
* for a while before anyone gates on it. Add the bar when the number is
|
|
336
|
+
* trustworthy: `t.score("recall", recall).atLeast(0.6)`.
|
|
337
|
+
*/
|
|
338
|
+
score(name: string, value: number): EvalAssertionHandle;
|
|
339
|
+
/**
|
|
340
|
+
* Record a gate and return the matching tool call so dependent code can read
|
|
341
|
+
* its input/output. Stops the test body when no call matches.
|
|
342
|
+
*/
|
|
343
|
+
requireToolCall(
|
|
344
|
+
name: string,
|
|
345
|
+
matcher?: EvalToolCallMatcher
|
|
346
|
+
): EvalToolCallFact;
|
|
347
|
+
/**
|
|
348
|
+
* Record a gate that exactly one approval request is pending, and return it.
|
|
349
|
+
* Stops the test body otherwise.
|
|
350
|
+
*/
|
|
351
|
+
requireInputRequest(filter?: {
|
|
352
|
+
toolName?: EvalMatcher<string>;
|
|
353
|
+
args?: EvalMatcher;
|
|
354
|
+
}): EvalInputRequestFact;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface CreateAssertionsOptions {
|
|
358
|
+
recorder: EvalAssertionRecorder;
|
|
359
|
+
/** Events under inspection. Read lazily so run scope sees later turns. */
|
|
360
|
+
events: () => readonly SessionEvent[];
|
|
361
|
+
/** Label prefix for turn-scoped assertions, e.g. `turn[2]`. */
|
|
362
|
+
scope?: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Build the assertion vocabulary bound to one scope. */
|
|
366
|
+
export function createAssertions(
|
|
367
|
+
options: CreateAssertionsOptions
|
|
368
|
+
): EvalAssertions {
|
|
369
|
+
const { recorder, events, scope } = options;
|
|
370
|
+
const facts = (): EvalRunFacts => deriveRunFacts(events());
|
|
371
|
+
const name = (text: string): string =>
|
|
372
|
+
scope === undefined ? text : `${scope}.${text}`;
|
|
373
|
+
|
|
374
|
+
const record = (input: RecordAssertionInput): EvalAssertionHandle =>
|
|
375
|
+
recorder.record({ ...input, name: name(input.name), scope });
|
|
376
|
+
|
|
377
|
+
const matchingToolCalls = (
|
|
378
|
+
toolName: string,
|
|
379
|
+
matcher: EvalToolCallMatcher | undefined
|
|
380
|
+
): EvalToolCallFact[] =>
|
|
381
|
+
facts().toolCalls.filter((call) => {
|
|
382
|
+
if (call.toolName !== toolName) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
if (matcher === undefined) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
if (matcher.status !== undefined && call.status !== matcher.status) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
if (!matchValue(call.input, matcher.input)) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
if (!matchValue(call.output, matcher.output)) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const matchingEvents = (
|
|
401
|
+
type: string,
|
|
402
|
+
matcher: EvalEventMatcher | undefined
|
|
403
|
+
): SessionEvent[] =>
|
|
404
|
+
events().filter(
|
|
405
|
+
(event) => event.type === type && matchValue(event.data, matcher?.data)
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
succeeded() {
|
|
410
|
+
const run = facts();
|
|
411
|
+
const detail = !run.ok
|
|
412
|
+
? (run.failureMessage ?? "turn failed")
|
|
413
|
+
: run.parked
|
|
414
|
+
? `parked on ${run.pendingInputRequests.length} unanswered approval request(s)`
|
|
415
|
+
: undefined;
|
|
416
|
+
return record({
|
|
417
|
+
name: "succeeded",
|
|
418
|
+
passed: run.ok && !run.parked,
|
|
419
|
+
detail,
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
parked() {
|
|
424
|
+
const run = facts();
|
|
425
|
+
const pending = run.pendingInputRequests;
|
|
426
|
+
return record({
|
|
427
|
+
name: "parked",
|
|
428
|
+
passed: run.parked && run.turnsFailed === 0,
|
|
429
|
+
detail: run.parked
|
|
430
|
+
? pending.map((r) => r.toolName).join(", ")
|
|
431
|
+
: "no unanswered approval request",
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
messageIncludes(token: string | RegExp) {
|
|
436
|
+
const text = facts().assistantText;
|
|
437
|
+
const passed =
|
|
438
|
+
typeof token === "string" ? text.includes(token) : token.test(text);
|
|
439
|
+
return record({
|
|
440
|
+
name: `messageIncludes(${String(token)})`,
|
|
441
|
+
passed,
|
|
442
|
+
detail: passed
|
|
443
|
+
? undefined
|
|
444
|
+
: `text=${JSON.stringify(text.slice(0, 120))}`,
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
calledTool(toolName: string, matcher?: EvalToolCallMatcher) {
|
|
449
|
+
const matched = matchingToolCalls(toolName, matcher);
|
|
450
|
+
const passed = matchCount(matched.length, matcher?.count);
|
|
451
|
+
const observed = facts()
|
|
452
|
+
.toolCalls.map((c) => c.toolName)
|
|
453
|
+
.join(", ");
|
|
454
|
+
return record({
|
|
455
|
+
name: `calledTool(${toolName}${describeMatcherObject(
|
|
456
|
+
matcher as Record<string, unknown> | undefined
|
|
457
|
+
)})`,
|
|
458
|
+
passed,
|
|
459
|
+
detail: passed
|
|
460
|
+
? undefined
|
|
461
|
+
: `matched ${matched.length}; called [${observed}]`,
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
notCalledTool(toolName: string) {
|
|
466
|
+
const matched = matchingToolCalls(toolName, undefined);
|
|
467
|
+
return record({
|
|
468
|
+
name: `notCalledTool(${toolName})`,
|
|
469
|
+
passed: matched.length === 0,
|
|
470
|
+
detail: matched.length === 0 ? undefined : "tool was called",
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
loadedSkill(skill: string) {
|
|
475
|
+
const needle = `skills/${skill}/SKILL.md`;
|
|
476
|
+
const matched = facts().toolCalls.filter((call) =>
|
|
477
|
+
JSON.stringify(call.input ?? "").includes(needle)
|
|
478
|
+
);
|
|
479
|
+
return record({
|
|
480
|
+
name: `loadedSkill(${skill})`,
|
|
481
|
+
passed: matched.length > 0,
|
|
482
|
+
detail:
|
|
483
|
+
matched.length > 0 ? undefined : `no tool call referenced ${needle}`,
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
toolOrder(names: readonly string[]) {
|
|
488
|
+
const called = facts().toolCalls.map((c) => c.toolName);
|
|
489
|
+
let cursor = 0;
|
|
490
|
+
for (const expected of names) {
|
|
491
|
+
const found = called.indexOf(expected, cursor);
|
|
492
|
+
if (found === -1) {
|
|
493
|
+
return record({
|
|
494
|
+
name: `toolOrder([${names.join(", ")}])`,
|
|
495
|
+
passed: false,
|
|
496
|
+
detail: `missing ${expected} after position ${cursor}; called [${called.join(", ")}]`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
cursor = found + 1;
|
|
500
|
+
}
|
|
501
|
+
return record({
|
|
502
|
+
name: `toolOrder([${names.join(", ")}])`,
|
|
503
|
+
passed: true,
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
usedNoTools() {
|
|
508
|
+
const called = facts().toolCalls;
|
|
509
|
+
return record({
|
|
510
|
+
name: "usedNoTools",
|
|
511
|
+
passed: called.length === 0,
|
|
512
|
+
detail:
|
|
513
|
+
called.length === 0
|
|
514
|
+
? undefined
|
|
515
|
+
: `called [${called.map((c) => c.toolName).join(", ")}]`,
|
|
516
|
+
});
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
maxToolCalls(max: number) {
|
|
520
|
+
const total = facts().toolCalls.length;
|
|
521
|
+
return record({
|
|
522
|
+
name: `maxToolCalls(${max})`,
|
|
523
|
+
passed: total <= max,
|
|
524
|
+
detail: total <= max ? undefined : `made ${total} tool calls`,
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
noFailedActions() {
|
|
529
|
+
const failed = facts().failedToolCalls;
|
|
530
|
+
return record({
|
|
531
|
+
name: "noFailedActions",
|
|
532
|
+
passed: failed.length === 0,
|
|
533
|
+
detail:
|
|
534
|
+
failed.length === 0
|
|
535
|
+
? undefined
|
|
536
|
+
: failed.map((c) => c.toolName).join(", "),
|
|
537
|
+
});
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
calledSubagent(subagentName: string, matcher?: EvalSubagentMatcher) {
|
|
541
|
+
const matched = facts().subagents.filter((sub) => {
|
|
542
|
+
if (sub.name !== subagentName) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
if (matcher === undefined) {
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
if (matcher.status !== undefined && sub.status !== matcher.status) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (!matchValue(sub.callId, matcher.callId)) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
return matchValue(sub.output, matcher.output);
|
|
555
|
+
});
|
|
556
|
+
const passed = matchCount(matched.length, matcher?.count);
|
|
557
|
+
return record({
|
|
558
|
+
name: `calledSubagent(${subagentName}${describeMatcherObject(
|
|
559
|
+
matcher as Record<string, unknown> | undefined
|
|
560
|
+
)})`,
|
|
561
|
+
passed,
|
|
562
|
+
detail: passed
|
|
563
|
+
? undefined
|
|
564
|
+
: `matched ${matched.length}; delegated [${facts()
|
|
565
|
+
.subagents.map((s) => s.name ?? s.callId)
|
|
566
|
+
.join(", ")}]`,
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
event(type: string, matcher?: EvalEventMatcher) {
|
|
571
|
+
const matched = matchingEvents(type, matcher);
|
|
572
|
+
const passed = matchCount(matched.length, matcher?.count);
|
|
573
|
+
return record({
|
|
574
|
+
name: `event(${type}${describeMatcherObject(
|
|
575
|
+
matcher as Record<string, unknown> | undefined
|
|
576
|
+
)})`,
|
|
577
|
+
passed,
|
|
578
|
+
detail: passed ? undefined : `matched ${matched.length}`,
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
notEvent(type: string, matcher?: EvalEventMatcher) {
|
|
583
|
+
const matched = matchingEvents(type, matcher);
|
|
584
|
+
return record({
|
|
585
|
+
name: `notEvent(${type}${describeMatcherObject(
|
|
586
|
+
matcher as Record<string, unknown> | undefined
|
|
587
|
+
)})`,
|
|
588
|
+
passed: matched.length === 0,
|
|
589
|
+
detail: matched.length === 0 ? undefined : `matched ${matched.length}`,
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
eventOrder(matchers: readonly EvalEventOrderMatcher[]) {
|
|
594
|
+
const stream = events();
|
|
595
|
+
const label = `eventOrder([${matchers.map((m) => m.type).join(", ")}])`;
|
|
596
|
+
let cursor = 0;
|
|
597
|
+
for (const matcher of matchers) {
|
|
598
|
+
const required = typeof matcher.count === "number" ? matcher.count : 1;
|
|
599
|
+
let seen = 0;
|
|
600
|
+
while (seen < required && cursor < stream.length) {
|
|
601
|
+
const event = stream[cursor]!;
|
|
602
|
+
cursor++;
|
|
603
|
+
if (
|
|
604
|
+
event.type === matcher.type &&
|
|
605
|
+
matchValue(event.data, matcher.data)
|
|
606
|
+
) {
|
|
607
|
+
seen++;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (seen < required) {
|
|
611
|
+
return record({
|
|
612
|
+
name: label,
|
|
613
|
+
passed: false,
|
|
614
|
+
detail: `only ${seen}/${required} ${matcher.type} event(s) in order`,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return record({ name: label, passed: true });
|
|
619
|
+
},
|
|
620
|
+
|
|
621
|
+
eventsSatisfy(
|
|
622
|
+
label: string,
|
|
623
|
+
predicate: (events: readonly SessionEvent[]) => boolean
|
|
624
|
+
) {
|
|
625
|
+
let passed = false;
|
|
626
|
+
let detail: string | undefined;
|
|
627
|
+
try {
|
|
628
|
+
passed = predicate(events()) === true;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
631
|
+
}
|
|
632
|
+
return record({
|
|
633
|
+
name: `eventsSatisfy(${label})`,
|
|
634
|
+
passed,
|
|
635
|
+
detail: passed ? undefined : (detail ?? label),
|
|
636
|
+
});
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
check(value: unknown, assertion: Expectation) {
|
|
640
|
+
const outcome = evaluateExpectation(value, assertion);
|
|
641
|
+
return record({
|
|
642
|
+
name: `check(${assertion.kind})`,
|
|
643
|
+
passed: outcome.passed,
|
|
644
|
+
detail: outcome.detail,
|
|
645
|
+
score: outcome.score,
|
|
646
|
+
severity: expectationSeverity(assertion),
|
|
647
|
+
});
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
score(scoreName: string, value: number) {
|
|
651
|
+
const finite = Number.isFinite(value);
|
|
652
|
+
return record({
|
|
653
|
+
name: `score(${scoreName})`,
|
|
654
|
+
// A NaN score is a bug in the eval's own math, so fail loudly rather
|
|
655
|
+
// than reporting a number nobody can act on.
|
|
656
|
+
passed: finite,
|
|
657
|
+
severity: "soft",
|
|
658
|
+
score: finite ? value : undefined,
|
|
659
|
+
detail: finite ? undefined : `score is not a finite number: ${value}`,
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
|
|
663
|
+
requireToolCall(toolName: string, matcher?: EvalToolCallMatcher) {
|
|
664
|
+
const matched = matchingToolCalls(toolName, matcher);
|
|
665
|
+
const assertionName = `requireToolCall(${toolName})`;
|
|
666
|
+
const first = matched[0];
|
|
667
|
+
record({
|
|
668
|
+
name: assertionName,
|
|
669
|
+
passed: first !== undefined,
|
|
670
|
+
detail: first === undefined ? "no matching tool call" : undefined,
|
|
671
|
+
});
|
|
672
|
+
if (first === undefined) {
|
|
673
|
+
throw new EvalRequireFailure(name(assertionName));
|
|
674
|
+
}
|
|
675
|
+
return first;
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
requireInputRequest(filter?: {
|
|
679
|
+
toolName?: EvalMatcher<string>;
|
|
680
|
+
args?: EvalMatcher;
|
|
681
|
+
}) {
|
|
682
|
+
const pending = facts().pendingInputRequests.filter(
|
|
683
|
+
(request) =>
|
|
684
|
+
matchValue(request.toolName, filter?.toolName) &&
|
|
685
|
+
matchValue(request.args, filter?.args)
|
|
686
|
+
);
|
|
687
|
+
const assertionName = "requireInputRequest";
|
|
688
|
+
const passed = pending.length === 1;
|
|
689
|
+
record({
|
|
690
|
+
name: assertionName,
|
|
691
|
+
passed,
|
|
692
|
+
detail: passed
|
|
693
|
+
? undefined
|
|
694
|
+
: `expected exactly 1 pending approval request, found ${pending.length}`,
|
|
695
|
+
});
|
|
696
|
+
if (!passed) {
|
|
697
|
+
throw new EvalRequireFailure(name(assertionName));
|
|
698
|
+
}
|
|
699
|
+
return pending[0]!;
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export type { EvalCountMatcher, EvalInputRequestFact, EvalToolCallFact };
|