eve 0.14.0 → 0.15.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/CHANGELOG.md +14 -0
- package/dist/src/channel/compiled-channel.js +2 -1
- package/dist/src/cli/dev/tui/runner.js +1 -1
- package/dist/src/cli/dev/tui/setup-panel.d.ts +3 -4
- package/dist/src/cli/dev/tui/setup-panel.js +2 -2
- package/dist/src/cli/dev/tui/terminal-renderer.js +1 -1
- package/dist/src/evals/assertions/collector.d.ts +19 -6
- package/dist/src/evals/assertions/collector.js +1 -1
- package/dist/src/evals/assertions/run.d.ts +24 -17
- package/dist/src/evals/assertions/run.js +2 -2
- package/dist/src/evals/assertions/scoped.d.ts +17 -0
- package/dist/src/evals/assertions/scoped.js +1 -0
- package/dist/src/evals/context.d.ts +1 -0
- package/dist/src/evals/context.js +1 -1
- package/dist/src/evals/control-flow.d.ts +9 -0
- package/dist/src/evals/control-flow.js +1 -0
- package/dist/src/evals/define-eval.d.ts +1 -1
- package/dist/src/evals/define-eval.js +1 -1
- package/dist/src/evals/expect/index.d.ts +5 -3
- package/dist/src/evals/expect/index.js +1 -1
- package/dist/src/evals/index.d.ts +2 -2
- package/dist/src/evals/match.d.ts +50 -13
- package/dist/src/evals/match.js +1 -1
- package/dist/src/evals/runner/artifacts.js +1 -1
- package/dist/src/evals/runner/derive-run-facts.js +1 -1
- package/dist/src/evals/runner/execute-eval.js +1 -1
- package/dist/src/evals/runner/execute-task.d.ts +1 -0
- package/dist/src/evals/runner/execute-task.js +1 -1
- package/dist/src/evals/runner/reporters/braintrust.js +1 -1
- package/dist/src/evals/runner/reporters/console.js +1 -1
- package/dist/src/evals/runner/reporters/junit.js +3 -2
- package/dist/src/evals/runner/run-evals.js +1 -1
- package/dist/src/evals/runner/verdict.d.ts +1 -0
- package/dist/src/evals/runner/verdict.js +1 -1
- package/dist/src/evals/session.d.ts +9 -5
- package/dist/src/evals/session.js +1 -1
- package/dist/src/evals/types.d.ts +69 -47
- package/dist/src/harness/emission.d.ts +13 -27
- package/dist/src/harness/emission.js +1 -1
- package/dist/src/harness/step-hooks.d.ts +3 -3
- package/dist/src/harness/step-hooks.js +1 -1
- package/dist/src/harness/tool-loop.js +1 -1
- package/dist/src/internal/application/compiled-artifacts.js +1 -1
- package/dist/src/internal/application/package.d.ts +12 -0
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/application/paths.d.ts +6 -0
- package/dist/src/internal/application/paths.js +1 -1
- package/dist/src/internal/nitro/host/configure-nitro-routes.js +2 -2
- package/dist/src/internal/workflow/configure-world.d.ts +6 -0
- package/dist/src/internal/workflow/configure-world.js +1 -1
- package/dist/src/internal/workflow/world-compatibility.d.ts +32 -0
- package/dist/src/internal/workflow/world-compatibility.js +1 -0
- package/dist/src/protocol/message.d.ts +7 -5
- package/dist/src/runtime/actions/types.d.ts +10 -11
- package/dist/src/setup/boxes/resolve-provisioning.js +1 -1
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/docs/agent-config.md +13 -3
- package/docs/concepts/execution-model-and-durability.md +1 -1
- package/docs/concepts/sessions-runs-and-streaming.md +1 -1
- package/docs/connections/overview.mdx +40 -2
- package/docs/evals/assertions.mdx +97 -39
- package/docs/evals/cases.mdx +24 -15
- package/docs/evals/judge.mdx +2 -2
- package/docs/evals/overview.mdx +7 -5
- package/docs/evals/reporters.mdx +2 -2
- package/docs/evals/running.mdx +3 -1
- package/docs/evals/targets.mdx +2 -2
- package/docs/guides/auth-and-route-protection.md +48 -1
- package/docs/guides/client/streaming.mdx +1 -1
- package/docs/guides/deployment.md +3 -2
- package/docs/tutorial/connect-a-warehouse.mdx +2 -0
- package/package.json +1 -1
package/docs/agent-config.md
CHANGED
|
@@ -96,11 +96,21 @@ export default defineAgent({
|
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
Install that package in your app. It should export a default factory or
|
|
99
|
-
`createWorld()` function.
|
|
99
|
+
`createWorld()` function. Pin a version built against the same `@workflow/*`
|
|
100
|
+
line as your eve release (currently the `5.0.0-beta` line):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm add @workflow/world-postgres@5.0.0-beta.x
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The npm `latest` tag can lag behind that line, so an unpinned install may pull
|
|
107
|
+
an incompatible major that fails with `ZodError: invalid_union` at run replay.
|
|
100
108
|
|
|
101
109
|
Put credentials and host-specific options in runtime environment variables read
|
|
102
|
-
by the world package, not in `agent.ts`.
|
|
103
|
-
|
|
110
|
+
by the world package, not in `agent.ts`. For the Postgres world, that means
|
|
111
|
+
putting the connection string or credentials in the env vars it reads. If the
|
|
112
|
+
installed package must stay external in hosted output, list it in
|
|
113
|
+
`build.externalDependencies`.
|
|
104
114
|
|
|
105
115
|
## Other defineAgent fields
|
|
106
116
|
|
|
@@ -36,7 +36,7 @@ export default defineAgent({
|
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The world package backs workflow state, queues, hooks, and streams. Keep secrets and deployment-specific options in runtime environment variables read by that package, not in `agent.ts`. See [agent.ts](../agent-config#workflow-world) and [Workflow Worlds](https://workflow-sdk.dev/worlds).
|
|
39
|
+
The world package backs workflow state, queues, hooks, and streams. Keep secrets and deployment-specific options in runtime environment variables read by that package, not in `agent.ts`. The selected world must match eve's bundled `@workflow/*` line (currently the `5.0.0-beta` line); pin it explicitly, since a mismatched world fails with a `ZodError: invalid_union` during run replay. See the [deployment guide](../guides/deployment#8-deploy-without-vercel) for the install command, plus [agent.ts](../agent-config#workflow-world) and [Workflow Worlds](https://workflow-sdk.dev/worlds).
|
|
40
40
|
|
|
41
41
|
## Resuming after a crash
|
|
42
42
|
|
|
@@ -40,7 +40,7 @@ The stream is newline-delimited JSON (NDJSON), one event per line:
|
|
|
40
40
|
| `turn.started` | A new turn began. |
|
|
41
41
|
| `message.received` | An inbound user message was accepted. |
|
|
42
42
|
| `step.started` | A model step began. |
|
|
43
|
-
| `actions.requested` | The model requested tool calls.
|
|
43
|
+
| `actions.requested` | The model requested one or more actions, including tool calls; calls stream before execution. |
|
|
44
44
|
| `action.result` | A tool call returned. |
|
|
45
45
|
| `input.requested` | The run paused for human input ([HITL](/docs/human-in-the-loop) approval or `ask_question`); carries `requests`. |
|
|
46
46
|
| `subagent.called` | A subagent was delegated; carries `childSessionId` to attach to. |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "
|
|
2
|
+
title: "Overview"
|
|
3
3
|
description: "Expose external MCP and OpenAPI servers to the model, with connection tokens the model never sees."
|
|
4
4
|
url: /connections
|
|
5
5
|
---
|
|
@@ -28,6 +28,18 @@ When `getToken` is the only auth, `principalType` defaults to `"app"`: one share
|
|
|
28
28
|
|
|
29
29
|
eve resolves and caches connection tokens per step; they never land in conversation history or reach the model.
|
|
30
30
|
|
|
31
|
+
## Choose app vs. user auth
|
|
32
|
+
|
|
33
|
+
A connection credential can belong to the agent or to the person using it. This choice is separate from route auth, but user-scoped connection auth depends on route auth: eve can only resolve a user token when the active session has `ctx.session.auth.current?.principalType === "user"`.
|
|
34
|
+
|
|
35
|
+
| Credential owner | Use when | Auth shape |
|
|
36
|
+
| ---------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| App | The agent should use one shared service, bot, installation, or app credential. | `auth: { getToken }` defaults to `principalType: "app"`, or use `connect({ connector: "linear/myagent", principalType: "app" })` with Vercel Connect. |
|
|
38
|
+
| User | Each end-user should authorize and use their own third-party account. | `connect("linear/myagent")`, `connect({ connector: "linear/myagent", principalType: "user" })`, or `auth: { principalType: "user", getToken }`. |
|
|
39
|
+
| User from a job | Background work should use the same user's OAuth grant that started the work. | Start or resume the session through a channel whose route auth resolved that user, or pass an explicit user auth context when dispatching through a channel. |
|
|
40
|
+
|
|
41
|
+
`principalType: "user"` does not mean "ask any human later." It means "key this credential to the authenticated user already attached to the eve session." If the run was started by a schedule, a same-project runtime token, `localDev()`, or another internal runtime path without an end-user principal, a user-scoped connection fails with `reason: "principal_required"` instead of starting OAuth. In that case, either authenticate the inbound channel as a user or configure the connection as app-scoped.
|
|
42
|
+
|
|
31
43
|
## No auth
|
|
32
44
|
|
|
33
45
|
Drop `auth` entirely for servers that need no token, such as a localhost server during development or a public one:
|
|
@@ -124,7 +136,33 @@ export default defineMcpClientConnection({
|
|
|
124
136
|
});
|
|
125
137
|
```
|
|
126
138
|
|
|
127
|
-
`"linear/myagent"` is the UID you chose when registering the Connect client.
|
|
139
|
+
`"linear/myagent"` is the UID you chose when registering the Connect client. `connect("linear/myagent")` is shorthand for a user-scoped interactive OAuth connection: eve resolves a token for the active user before each tool call, emits `authorization.required` when that user has not authorized yet, and resumes the parked turn after the callback completes.
|
|
140
|
+
|
|
141
|
+
That means the channel that creates or continues the session must authenticate a real user. For a web app, configure `agent/channels/eve.ts` so your app session maps to `principalType: "user"`; for platform channels, use the built-in channel auth that maps the sender to a user principal. If no authenticated user is attached to the session, the first user-scoped connection call fails with `reason: "principal_required"`.
|
|
142
|
+
|
|
143
|
+
If the remote service should act as the agent itself instead of the end-user, make the Connect connection app-scoped:
|
|
144
|
+
|
|
145
|
+
```ts title="agent/connections/linear.ts"
|
|
146
|
+
import { connect } from "@vercel/connect/eve";
|
|
147
|
+
import { defineMcpClientConnection } from "eve/connections";
|
|
148
|
+
|
|
149
|
+
export default defineMcpClientConnection({
|
|
150
|
+
url: "https://mcp.linear.app/sse",
|
|
151
|
+
description: "Linear workspace: issues, projects, cycles, and comments.",
|
|
152
|
+
auth: connect({ connector: "linear/myagent", principalType: "app" }),
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
App-scoped Connect auth is non-interactive. eve asks Vercel Connect for an app token and does not emit a browser consent challenge; if the connector is not installed or cannot issue an app token, the tool call fails terminally so an operator can fix the connector setup. The full setup (Connect client provisioning, project linking, the runtime consent flow) lives in [Auth & route protection](/docs/guides/auth-and-route-protection).
|
|
157
|
+
|
|
158
|
+
### Troubleshooting Vercel Connect auth
|
|
159
|
+
|
|
160
|
+
| Symptom | What it means | Fix |
|
|
161
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `reason: "principal_required"` | A user-scoped connection ran without an authenticated user on the active session. | Return `principalType: "user"` from the channel's route auth, or change the connection to `principalType: "app"` if it should be shared. |
|
|
163
|
+
| `authorization.required` appears but no UI | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge. | Render the challenge from the stream event and continue the same session after the callback. |
|
|
164
|
+
| OAuth works locally but fails after deploy | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`. |
|
|
165
|
+
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal. | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work. |
|
|
128
166
|
|
|
129
167
|
## Self-hosted interactive OAuth
|
|
130
168
|
|
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Assertions"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Scoped methods, value assertions, the matcher mini-language, and gate vs soft severity."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Assertions are how an eval grades what its `test(t)` function produced. Each one
|
|
6
|
+
Assertions are how an eval grades what its `test(t)` function produced. Each one records a result and returns a chainable handle. The runner reads every recorded result to compute the verdict, so a single run reports every failing assertion rather than dying on the first. There are two deterministic surfaces: scoped methods and `t.check` for grading a specific value. For model-graded assertions, see [Judge](./judge).
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Scoped assertions
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Scoped assertions take no explicit value and gate by default. Assertions on `t` inspect the whole run after `test` finishes. Session assertions snapshot that session when called, and turn assertions inspect one immutable response. A scope is **parked** when it paused on unanswered human-in-the-loop (HITL) input.
|
|
11
11
|
|
|
12
|
-
| Assertion
|
|
13
|
-
|
|
|
14
|
-
| `t.
|
|
15
|
-
| `t.
|
|
16
|
-
| `t.
|
|
17
|
-
| `
|
|
18
|
-
| `t.
|
|
19
|
-
| `t.
|
|
20
|
-
| `t.
|
|
21
|
-
| `t.
|
|
22
|
-
| `t.
|
|
23
|
-
| `t.
|
|
24
|
-
| `t.
|
|
25
|
-
| `t.
|
|
26
|
-
| `t.
|
|
27
|
-
| `t.
|
|
12
|
+
| Assertion | Asserts |
|
|
13
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
14
|
+
| `t.succeeded()` | The run did not fail and did not park on unanswered HITL input |
|
|
15
|
+
| `t.parked()` | The run cleanly parked on HITL input |
|
|
16
|
+
| `t.messageIncludes(token)` | Joined assistant text contains `token` (string or RegExp) |
|
|
17
|
+
| `turn.outputEquals(value)` / `.outputMatches(schema)` | Deep equality or Standard Schema validation of turn/session structured output |
|
|
18
|
+
| `t.calledTool(name, opts?)` | A matching tool call completed (`input`, `output`, `status`, `count`) |
|
|
19
|
+
| `t.loadedSkill(skill, opts?)` | Sugar for `t.calledTool("load_skill", { input: { skill }, ...opts })` |
|
|
20
|
+
| `t.notCalledTool(name)` | No request for `name` in any lifecycle state |
|
|
21
|
+
| `t.toolOrder([...names])` | Tool requests appear in order |
|
|
22
|
+
| `t.usedNoTools()` | No tool calls at all |
|
|
23
|
+
| `t.maxToolCalls(n)` | At most `n` tool calls |
|
|
24
|
+
| `t.noFailedActions()` | No tool, subagent, or skill action reported a failure |
|
|
25
|
+
| `t.calledSubagent(name, opts?)` | A subagent delegation happened (`remoteUrl`, `output` constraints) |
|
|
26
|
+
| `t.event(type, opts?)` / `t.notEvent(type, opts?)` | Typed event presence, data, and count matching |
|
|
27
|
+
| `t.eventOrder([...matchers])` | Matching event groups occur in order |
|
|
28
|
+
| `t.eventsSatisfy(label, predicate)` | Escape hatch: any predicate over the typed event stream |
|
|
28
29
|
|
|
29
|
-
`
|
|
30
|
+
`succeeded()` accepts both a closed session and a healthy session left open for the next user message; it rejects protocol failures and unanswered HITL. `parked()` requires a clean HITL park. Structured output assertions live on turns and independent sessions, where the output is unambiguous (see the [output schema guide](../guides/client/output-schema)).
|
|
30
31
|
|
|
31
32
|
```ts
|
|
32
33
|
await t.send("What is the weather in Brooklyn?");
|
|
33
|
-
t.
|
|
34
|
+
t.succeeded();
|
|
34
35
|
t.calledTool("get_weather");
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
The same vocabulary narrows naturally in multi-turn and externally-created session evals:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const first = await t.send("Call get_weather for Brooklyn");
|
|
42
|
+
first.calledTool("get_weather", { count: 1 });
|
|
43
|
+
|
|
44
|
+
const attached = await t.target.attachSession(sessionId);
|
|
45
|
+
attached.succeeded();
|
|
46
|
+
attached.messageIncludes("Sunny");
|
|
47
|
+
```
|
|
48
|
+
|
|
37
49
|
`t.calledTool` and `t.usedNoTools` are mutually exclusive; assert one or the other, never both in the same run.
|
|
38
50
|
|
|
39
51
|
## Value assertions with `t.check`
|
|
@@ -41,52 +53,98 @@ t.calledTool("get_weather");
|
|
|
41
53
|
`t.check(value, assertion)` grades an explicit value against a builder from `eve/evals/expect`. The value can be `t.reply`, a turn's `.message`, parsed JSON, or any local you computed:
|
|
42
54
|
|
|
43
55
|
```ts
|
|
44
|
-
import { includes, equals, matches, similarity } from "eve/evals/expect";
|
|
56
|
+
import { includes, equals, matches, satisfies, similarity } from "eve/evals/expect";
|
|
45
57
|
|
|
46
|
-
t.check(t.reply, includes(
|
|
58
|
+
t.check(t.reply, includes(/sunny/i)); // substring or RegExp (gate)
|
|
47
59
|
t.check(parsed, equals({ city: "Brooklyn" })); // deep structural equality (gate)
|
|
48
60
|
t.check(parsed, matches(WeatherSchema)); // Standard Schema, e.g. Zod (gate)
|
|
49
61
|
t.check(t.reply, similarity("Sunny, 72F")); // fuzzy 0–1 Levenshtein (soft)
|
|
62
|
+
t.check(
|
|
63
|
+
latencyMs,
|
|
64
|
+
satisfies((value) => value < 1_000, "latency under one second"),
|
|
65
|
+
);
|
|
50
66
|
```
|
|
51
67
|
|
|
52
|
-
| Builder | Scores
|
|
53
|
-
| ---------------------- |
|
|
54
|
-
| `includes(
|
|
55
|
-
| `equals(value)` | deep structural equality
|
|
56
|
-
| `matches(schema)` | validates against a Standard Schema
|
|
57
|
-
| `similarity(expected)` | normalized Levenshtein similarity, 1 = identical
|
|
68
|
+
| Builder | Scores | Default |
|
|
69
|
+
| ---------------------- | ------------------------------------------------------- | ------- |
|
|
70
|
+
| `includes(value)` | coerced string contains a substring or matches a RegExp | gate |
|
|
71
|
+
| `equals(value)` | deep structural equality | gate |
|
|
72
|
+
| `matches(schema)` | validates against a Standard Schema | gate |
|
|
73
|
+
| `similarity(expected)` | normalized Levenshtein similarity, 1 = identical | soft |
|
|
74
|
+
| `satisfies(fn, label)` | custom boolean predicate | gate |
|
|
58
75
|
|
|
59
76
|
Pick the cheapest builder that captures what "correct" means. When exact match is too strict but a judge model is overkill, `similarity` is the middle ground. For nuanced grading, reach for the [judge](./judge).
|
|
60
77
|
|
|
61
78
|
## The matcher mini-language
|
|
62
79
|
|
|
63
|
-
`t.calledTool` and `t.calledSubagent` take
|
|
80
|
+
`t.calledTool` and `t.calledSubagent` take matcher objects. Tools accept `{ input, output, status, count }`; subagents accept `{ remoteUrl, output, status, count }`. Calls match `status: "completed"` by default; use `"pending"`, `"failed"`, or `"rejected"` explicitly for lifecycle checks. `count` is the exact number of calls matching every supplied constraint.
|
|
81
|
+
|
|
82
|
+
Matcher values accept a literal (objects partial-deep-match), a RegExp, or a predicate function that returns a boolean:
|
|
64
83
|
|
|
65
84
|
```ts
|
|
66
|
-
t.calledTool("bash", { input: { command: /^pwd/ },
|
|
85
|
+
t.calledTool("bash", { input: { command: /^pwd/ }, count: 1 });
|
|
67
86
|
|
|
68
87
|
t.calledTool("echo", { output: (value) => String(value).includes(marker) });
|
|
69
88
|
|
|
89
|
+
parked.calledTool("guarded", { status: "pending", count: 1 });
|
|
90
|
+
t.calledTool("guarded", { output: /approved/, count: 1 });
|
|
91
|
+
|
|
70
92
|
t.calledSubagent("weather", {
|
|
71
|
-
remoteUrl: () => process.env.WEATHER_AGENT_URL
|
|
93
|
+
remoteUrl: (value) => value === process.env.WEATHER_AGENT_URL,
|
|
72
94
|
output: /72F/,
|
|
73
95
|
});
|
|
74
96
|
```
|
|
75
97
|
|
|
98
|
+
`requireInputRequest` uses the same matcher language for `input`, `prompt`, and `display`. Its `optionIds` matcher receives option ids in request order; a literal array must match that complete ordered list exactly:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const request = session.requireInputRequest({
|
|
102
|
+
toolName: "ask_question",
|
|
103
|
+
optionIds: ["red", "blue"],
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
76
107
|
## Run state and derived facts
|
|
77
108
|
|
|
78
|
-
Beyond the raw `t.events` stream, the runner derives typed facts the assertions read: tool calls (name, input, output,
|
|
109
|
+
Beyond the raw `t.events` stream, the runner derives typed facts the assertions read: tool calls (name, input, output, lifecycle status), subagent calls, and HITL input requests. A turn that leaves the session open for a next message is the normal end state of a successful turn; parking on unanswered HITL input is tracked separately.
|
|
79
110
|
|
|
80
|
-
|
|
111
|
+
Typed event matching covers presence, absence, exact counts, partial event data, and ordering:
|
|
81
112
|
|
|
82
113
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
114
|
+
turn.notEvent("result.completed");
|
|
115
|
+
turn.eventOrder([
|
|
116
|
+
{ type: "subagent.called", data: { name: "researcher" }, count: 2 },
|
|
117
|
+
{ type: "subagent.completed", data: { subagentName: "researcher" }, count: 2 },
|
|
118
|
+
]);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
When a protocol invariant needs cross-event correlation, `eventsSatisfy` remains the escape hatch:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
t.eventsSatisfy("assistant reply includes the marker", (events) =>
|
|
125
|
+
events.some((e) => e.type === "message.completed" && e.data.message?.includes(marker)),
|
|
87
126
|
);
|
|
88
127
|
```
|
|
89
128
|
|
|
129
|
+
## Required preconditions
|
|
130
|
+
|
|
131
|
+
Recorded assertions never throw and are not awaitable. When later control flow depends on a value, use `await t.require(value, assertion)`. It records a gate, returns the original value when it passes, and stops the test body without adding a duplicate execution error when it fails:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
await t.require(
|
|
135
|
+
sessionIds,
|
|
136
|
+
satisfies((ids) => ids.length > 0, "dispatch started a session"),
|
|
137
|
+
);
|
|
138
|
+
await t.target.attachSession(sessionIds[0]!);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use the matching `require*` lookups when dependent code needs protocol data:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const call = turn.requireToolCall("search");
|
|
145
|
+
const request = session.requireInputRequest({ toolName: "guarded" });
|
|
146
|
+
```
|
|
147
|
+
|
|
90
148
|
## Severity
|
|
91
149
|
|
|
92
150
|
Every assertion returns a chainable handle. Severity rides on the assertion, so there is no separate thresholds map to keep in sync.
|
package/docs/evals/cases.mdx
CHANGED
|
@@ -5,6 +5,14 @@ description: "Author single-turn and multi-turn evals with test(t), and fan one
|
|
|
5
5
|
|
|
6
6
|
Each eval file is one graded case by default, and a single file can fan out over a dataset by default-exporting an array (covered below). The runner executes each `test(t)` function against the target, captures every event, and computes a verdict from the [assertions](./assertions) you recorded. Every eval shares one shape, whether single-turn, multi-turn, human-in-the-loop (HITL), or dataset-driven: one `async test(t)` function that drives the agent and asserts inline.
|
|
7
7
|
|
|
8
|
+
Before adding a case, create the required config at the root of `evals/`. An empty config is enough when you do not need shared judge, reporter, concurrency, or timeout settings:
|
|
9
|
+
|
|
10
|
+
```ts title="evals/evals.config.ts"
|
|
11
|
+
import { defineEvalConfig } from "eve/evals";
|
|
12
|
+
|
|
13
|
+
export default defineEvalConfig({});
|
|
14
|
+
```
|
|
15
|
+
|
|
8
16
|
## Single-turn evals
|
|
9
17
|
|
|
10
18
|
The common case sends one turn and asserts on the reply. `t.send(input)` resolves once the turn settles, and `t.reply` is the last assistant message:
|
|
@@ -16,7 +24,7 @@ import { includes } from "eve/evals/expect";
|
|
|
16
24
|
export default defineEval({
|
|
17
25
|
async test(t) {
|
|
18
26
|
await t.send("What is the weather in Brooklyn?");
|
|
19
|
-
t.
|
|
27
|
+
t.succeeded();
|
|
20
28
|
t.check(t.reply, includes("Sunny"));
|
|
21
29
|
},
|
|
22
30
|
});
|
|
@@ -30,7 +38,7 @@ import { defineEval } from "eve/evals";
|
|
|
30
38
|
export default defineEval({
|
|
31
39
|
async test(t) {
|
|
32
40
|
await t.send("Hello!");
|
|
33
|
-
t.
|
|
41
|
+
t.succeeded();
|
|
34
42
|
t.notCalledTool("get_weather");
|
|
35
43
|
},
|
|
36
44
|
});
|
|
@@ -42,6 +50,7 @@ Identity is the file path, so directories are the grouping mechanism. `evals/wea
|
|
|
42
50
|
|
|
43
51
|
```text
|
|
44
52
|
evals/
|
|
53
|
+
├── evals.config.ts
|
|
45
54
|
├── weather/
|
|
46
55
|
│ ├── shared.ts # helpers, not an eval
|
|
47
56
|
│ ├── brooklyn-forecast.eval.ts
|
|
@@ -69,25 +78,21 @@ export default defineEval({
|
|
|
69
78
|
});
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
|
|
81
|
+
Use scoped assertions for intermediate turns. When later control flow depends on a value-level check, `t.require` records a gate and stops the script if it fails:
|
|
73
82
|
|
|
74
83
|
```ts title="evals/session-continuity.eval.ts"
|
|
75
84
|
import { defineEval } from "eve/evals";
|
|
76
|
-
import {
|
|
85
|
+
import { equals } from "eve/evals/expect";
|
|
77
86
|
|
|
78
87
|
export default defineEval({
|
|
79
88
|
async test(t) {
|
|
80
|
-
await t.send("My favorite word is marigold.");
|
|
81
|
-
const firstSessionId = t.sessionId;
|
|
89
|
+
const first = await t.send("My favorite word is marigold.");
|
|
82
90
|
|
|
83
91
|
const second = await t.send("Thanks for remembering.");
|
|
84
|
-
second.
|
|
85
|
-
if (t.sessionId !== firstSessionId) {
|
|
86
|
-
throw new Error(`Expected one session; got ${firstSessionId} then ${t.sessionId}.`);
|
|
87
|
-
}
|
|
92
|
+
await t.require(second.sessionId, equals(first.sessionId));
|
|
88
93
|
|
|
89
|
-
t.
|
|
90
|
-
|
|
94
|
+
t.succeeded();
|
|
95
|
+
second.messageIncludes("Thanks for remembering.");
|
|
91
96
|
},
|
|
92
97
|
});
|
|
93
98
|
```
|
|
@@ -98,12 +103,16 @@ export default defineEval({
|
|
|
98
103
|
|
|
99
104
|
- `t.send(input)` sends a turn and waits for it to settle. It accepts the same input as `ClientSession.send()` (a string or a structured message) and resolves to a turn carrying `.message` and `.expectOk()`.
|
|
100
105
|
- `t.sendFile(text, path, mediaType?)` attaches a local file as a data URL.
|
|
101
|
-
- `t.
|
|
106
|
+
- `t.requireInputRequest(filter?)` records a gate, requires exactly one pending request, and returns it. Filters match tool name, action input, prompt, display, and option ids.
|
|
102
107
|
- `t.respond(...responses)` answers specific pending input requests and sends them as the next turn.
|
|
103
108
|
- `t.respondAll(optionId)` answers every pending input request with the same option and sends the responses as the next turn.
|
|
104
109
|
- `t.reply` is the last assistant message (or `null`); `t.sessionId` is the current session id; `t.events` is the full typed event stream captured so far.
|
|
105
110
|
|
|
106
|
-
Each `send` (and `respond`/`respondAll`) resolves to
|
|
111
|
+
Each `send` (and `respond`/`respondAll`) resolves to an immutable turn with `.message`, `.data`, `.events`, `.inputRequests`, `.toolCalls`, `.sessionId`, `.status`, and `.expectOk()`. Use `.sessionId` to relate turns or attach follow-up work to the session that produced a specific turn. `expectOk()` throws only when the turn ended failed; a session left open for a next message is the normal end state of a successful turn.
|
|
112
|
+
|
|
113
|
+
Use `expectOk()` only when the next operation depends on that intermediate turn succeeding. A final `t.succeeded()` already records a complete-run gate.
|
|
114
|
+
|
|
115
|
+
To intentionally omit an eval for the current target, call `t.skip(reason)` before sending messages or recording assertions. Skipped evals are reported separately and do not affect the exit code.
|
|
107
116
|
|
|
108
117
|
Events from every session are captured in the result and artifacts. `t.log(message)` records debug lines into the eval artifact; `--verbose` also streams them to stdout as evals run. `t.signal` is an `AbortSignal` that fires on timeout.
|
|
109
118
|
|
|
@@ -126,7 +135,7 @@ export default rows.map((row) =>
|
|
|
126
135
|
description: row.task,
|
|
127
136
|
async test(t) {
|
|
128
137
|
await t.send(row.prompt);
|
|
129
|
-
t.
|
|
138
|
+
t.succeeded();
|
|
130
139
|
t.check(t.reply, equals(row.sql));
|
|
131
140
|
},
|
|
132
141
|
}),
|
package/docs/evals/judge.mdx
CHANGED
|
@@ -11,7 +11,7 @@ import { defineEval } from "eve/evals";
|
|
|
11
11
|
export default defineEval({
|
|
12
12
|
async test(t) {
|
|
13
13
|
await t.send("Explain quantum tunneling to a 10-year-old.");
|
|
14
|
-
t.
|
|
14
|
+
t.succeeded();
|
|
15
15
|
t.judge.autoevals.closedQA("uses no math beyond arithmetic").atLeast(0.8);
|
|
16
16
|
},
|
|
17
17
|
});
|
|
@@ -52,7 +52,7 @@ t.judge.autoevals.closedQA("cites a source").atLeast(0.6); // soft, fails under
|
|
|
52
52
|
t.judge.autoevals.factuality(reference).gate(0.8); // hard gate at 0.8
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
A judge runs once per assertion and burns tokens, so reach for one only when nothing deterministic will do.
|
|
55
|
+
A judge runs once per assertion and burns tokens, so reach for one only when nothing deterministic will do. Judge calls start when recorded, and the runner waits for all of them during finalization; assertion handles themselves are intentionally not awaitable.
|
|
56
56
|
|
|
57
57
|
## Configuring the judge model
|
|
58
58
|
|
package/docs/evals/overview.mdx
CHANGED
|
@@ -33,7 +33,7 @@ export default defineEval({
|
|
|
33
33
|
description: "Basic message and tool-usage coverage for the weather agent.",
|
|
34
34
|
async test(t) {
|
|
35
35
|
await t.send("What is the weather in Brooklyn?");
|
|
36
|
-
t.
|
|
36
|
+
t.succeeded();
|
|
37
37
|
t.calledTool("get_weather");
|
|
38
38
|
t.check(t.reply, includes("Sunny"));
|
|
39
39
|
},
|
|
@@ -62,14 +62,14 @@ Everything is optional. `judge` sets the default model for [LLM-as-judge](./judg
|
|
|
62
62
|
|
|
63
63
|
`t` is both the driver and the assertion surface. There are no separate `input`, `run`, `checks`, or `scores` fields. You write ordinary control flow, sending turns and asserting inline.
|
|
64
64
|
|
|
65
|
-
- **Drive** the agent: `t.send(...)`, `t.respond(...)`, `t.respondAll(...)`, `t.sendFile(...)`, `t.
|
|
65
|
+
- **Drive** the agent: `t.send(...)`, `t.respond(...)`, `t.respondAll(...)`, `t.sendFile(...)`, `t.requireInputRequest(...)`, `t.newSession()`. Read what came back with `t.reply` (the last assistant message), `t.sessionId`, and `t.events`. See [Cases](./cases).
|
|
66
66
|
- **Assert** with three surfaces, covered next.
|
|
67
67
|
|
|
68
68
|
## Three assertion surfaces
|
|
69
69
|
|
|
70
70
|
Each surface matches a genuinely different kind of judgment:
|
|
71
71
|
|
|
72
|
-
- **
|
|
72
|
+
- **Scoped methods** read the final whole run on `t`, snapshot one independent session when invoked there, or inspect one immutable `EveEvalTurn`. See [Assertions](./assertions).
|
|
73
73
|
- **`t.check(value, assertion)`** grades an explicit value with a deterministic builder from `eve/evals/expect`, such as `t.check(t.reply, includes("sunny"))`. Grade `t.reply`, an intermediate draft, parsed JSON, or anything else. See [Assertions](./assertions).
|
|
74
74
|
- **`t.judge.autoevals.*`** is the LLM-as-judge surface, like `t.judge.autoevals.closedQA("cites a source")`. It grades `t.reply` by default and uses the configured judge model, never the agent under test. See [Judge](./judge).
|
|
75
75
|
|
|
@@ -83,12 +83,14 @@ Every assertion returns a chainable handle, so severity rides on the assertion i
|
|
|
83
83
|
Override per assertion: `.gate(threshold?)` promotes to a hard gate, `.soft(threshold?)` demotes to tracked, and `.atLeast(threshold)` is a soft assertion with a bar.
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
t.
|
|
86
|
+
t.succeeded(); // gate
|
|
87
87
|
t.calledTool("get_weather").soft(); // record as a metric, don't gate
|
|
88
88
|
t.judge.autoevals.closedQA("cites a source"); // soft, tracked (no threshold)
|
|
89
89
|
t.judge.autoevals.factuality(reference).atLeast(0.7); // soft, gated under --strict at 0.7
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
Use `await t.require(value, assertion)` for a gate that must pass before the script can safely continue. Use `t.skip(reason)` as the first operation for an intentionally unsupported target capability.
|
|
93
|
+
|
|
92
94
|
## Run evals with eve eval
|
|
93
95
|
|
|
94
96
|
```bash
|
|
@@ -101,7 +103,7 @@ Exit code `0` means every eval passed its gates. See [Running evals](./running)
|
|
|
101
103
|
|
|
102
104
|
## A good baseline
|
|
103
105
|
|
|
104
|
-
Most apps do fine with a few small smoke evals. Assert behavior with `t.
|
|
106
|
+
Most apps do fine with a few small smoke evals. Assert behavior with `t.succeeded()` plus one or two content checks, keep dataset fixtures in `evals/data/`, and reach for a judge or Braintrust only when you need fuzzy grading or shared result review. In CI, run `eve eval --strict` so soft threshold misses fail the build too.
|
|
105
107
|
|
|
106
108
|
## What to read next
|
|
107
109
|
|
package/docs/evals/reporters.mdx
CHANGED
|
@@ -33,7 +33,7 @@ export default defineEval({
|
|
|
33
33
|
reporters: [Braintrust({ projectName: "weather-agent" })],
|
|
34
34
|
async test(t) {
|
|
35
35
|
await t.send("What is the weather in Brooklyn?");
|
|
36
|
-
t.
|
|
36
|
+
t.succeeded();
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
```
|
|
@@ -52,7 +52,7 @@ Braintrust needs its SDK installed in the app and credentials in the environment
|
|
|
52
52
|
eve eval --strict --junit .eve/junit.xml
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Each eval becomes one `<testcase>` named by its path-derived id; failed gates and execution errors
|
|
55
|
+
Each eval becomes one `<testcase>` named by its path-derived id; failed gates and execution errors become failures, while `t.skip(reason)` produces a JUnit `<skipped>` result.
|
|
56
56
|
|
|
57
57
|
## Custom reporters
|
|
58
58
|
|
package/docs/evals/running.mdx
CHANGED
|
@@ -26,10 +26,12 @@ Positional ids match exactly or by directory prefix: `eve eval weather` runs `ev
|
|
|
26
26
|
|
|
27
27
|
| Code | Means |
|
|
28
28
|
| ---- | ------------------------------------------------------------------------------- |
|
|
29
|
-
| `0` | Every eval passed its gates (and soft thresholds, under `--strict`)
|
|
29
|
+
| `0` | Every non-skipped eval passed its gates (and soft thresholds, under `--strict`) |
|
|
30
30
|
| `1` | Any eval failed (a failed gate, an execution error, or a strict threshold miss) |
|
|
31
31
|
| `2` | Configuration error |
|
|
32
32
|
|
|
33
|
+
An eval that calls `t.skip(reason)` is reported as skipped, does not count as passed or failed, and never changes the exit code.
|
|
34
|
+
|
|
33
35
|
## Artifacts
|
|
34
36
|
|
|
35
37
|
Each run drops artifacts under `.eve/evals/<timestamp>/`: a run `summary.json`, a `results.jsonl` index, and per-eval assertion results, verdicts, captured event streams, and `t.log` lines under `evals/`. The console output stays tight on purpose; when an eval fails, the artifact has the full story.
|
package/docs/evals/targets.mdx
CHANGED
|
@@ -16,7 +16,7 @@ export default defineEval({
|
|
|
16
16
|
async test(t) {
|
|
17
17
|
const { sessionIds } = await t.target.dispatchSchedule("heartbeat");
|
|
18
18
|
await t.target.attachSession(sessionIds[0]!);
|
|
19
|
-
t.
|
|
19
|
+
t.succeeded();
|
|
20
20
|
t.calledTool("send_report");
|
|
21
21
|
},
|
|
22
22
|
});
|
|
@@ -26,7 +26,7 @@ export default defineEval({
|
|
|
26
26
|
- `t.target.dispatchSchedule(id)` triggers a [schedule](../schedules) through the dev-only schedule route and returns the session ids it created. It works only against a target with dev routes enabled (the local `eve eval` dev server, or a deployment running in development mode), and throws otherwise.
|
|
27
27
|
- `t.target.attachSession(sessionId, { startIndex? })` consumes one turn from a session created outside the eval, by a channel or a schedule, so its events feed the run-level assertions. `startIndex` skips events before that position, so a session already partway through its stream resumes from where you left off rather than replaying from the start.
|
|
28
28
|
|
|
29
|
-
Sessions attached this way are full `EveEvalSession`s: you can keep driving them
|
|
29
|
+
Sessions attached this way are full `EveEvalSession`s: you can keep driving them and assert directly on that session (`session.succeeded()`, `session.calledTool(...)`). Aggregate assertions on `t` continue to read the whole run, including every attached session.
|
|
30
30
|
|
|
31
31
|
## Authentication
|
|
32
32
|
|
|
@@ -224,6 +224,53 @@ Route auth does not enforce session ownership. If multiple users or tenants can
|
|
|
224
224
|
|
|
225
225
|
Tool and connection auth is how your agent reaches an external service that wants an interactive sign-in, like an OAuth MCP server. Connections declare `auth` on the connection definition. Tools should resolve providers inline with `ctx.getToken(provider)` and call `ctx.requireAuth(provider)` only when a downstream service rejects a token; eve drives the sign-in, caches the token per step, and re-runs the call once the caller authorizes.
|
|
226
226
|
|
|
227
|
+
The principal for user-scoped tool and connection auth comes from route auth. `connect("...")` from `@vercel/connect/eve` defaults to `principalType: "user"`, so the active session must have `ctx.session.auth.current.principalType === "user"` before the first token lookup can start OAuth. If the session is anonymous, local-dev-only, runtime-scoped, or service-scoped, eve fails fast with `reason: "principal_required"` because there is no end-user identity to bind the OAuth grant to.
|
|
228
|
+
|
|
229
|
+
Use app-scoped auth when the external service should act as the agent itself:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
auth: connect({ connector: "linear/myagent", principalType: "app" });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use user-scoped auth when the external service should act as the signed-in person:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
auth: connect("linear/myagent");
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
For user-scoped auth in a browser app, the route-auth entry for the eve channel should verify your app session and return a user principal:
|
|
242
|
+
|
|
243
|
+
```ts title="agent/channels/eve.ts"
|
|
244
|
+
import { eveChannel } from "eve/channels/eve";
|
|
245
|
+
import { localDev, type AuthFn } from "eve/channels/auth";
|
|
246
|
+
import { getSession } from "@/lib/auth";
|
|
247
|
+
|
|
248
|
+
function appSession(): AuthFn<Request> {
|
|
249
|
+
return async (request) => {
|
|
250
|
+
const session = await getSession(request);
|
|
251
|
+
if (!session) return null;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
authenticator: "app",
|
|
255
|
+
principalId: session.userId,
|
|
256
|
+
principalType: "user",
|
|
257
|
+
attributes: {
|
|
258
|
+
email: session.email,
|
|
259
|
+
teamId: session.teamId,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default eveChannel({
|
|
266
|
+
auth: [appSession(), localDev()],
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Keep `principalId` stable for the same person, and include an `issuer` when the same app may accept users from multiple identity providers. The connection token cache keys user credentials by issuer and principal id so two providers cannot accidentally share a grant.
|
|
271
|
+
|
|
272
|
+
Built-in platform channels that identify a human sender, such as Slack, Discord, Teams, Telegram, Twilio, Linear, and GitHub, attach a user principal for that sender by default. A Slack mention, DM, or button click can therefore authorize a user-scoped connection for the Slack user who sent it without adding a separate browser-session auth function.
|
|
273
|
+
|
|
227
274
|
### On a connection
|
|
228
275
|
|
|
229
276
|
Attach `connect()` from `@vercel/connect/eve` to the connection:
|
|
@@ -241,7 +288,7 @@ export default defineMcpClientConnection({
|
|
|
241
288
|
});
|
|
242
289
|
```
|
|
243
290
|
|
|
244
|
-
The first call that needs
|
|
291
|
+
The first call that needs a user-scoped connection kicks off an OAuth sign-in, surfaced as an authorization challenge (a URL the caller visits). [Vercel Connect](https://vercel.com/docs/connect) brokers the flow and holds the credentials, which are resolved and cached per workflow step, never serialized into history, and never shown to the model. For non-interactive connections, pass a static token or `connect({ connector, principalType: "app" })` in place of user-scoped `connect()`. [Connections](../connections) covers both shapes.
|
|
245
292
|
|
|
246
293
|
### On a single tool
|
|
247
294
|
|
|
@@ -66,7 +66,7 @@ The most common UI events are:
|
|
|
66
66
|
| `message.received` | Confirm the user message landed. |
|
|
67
67
|
| `reasoning.appended` | Render reasoning deltas when the model provides them. |
|
|
68
68
|
| `message.appended` | Render assistant text deltas. |
|
|
69
|
-
| `actions.requested` | Show tool calls
|
|
69
|
+
| `actions.requested` | Show tool calls as the model requests them, before execution. |
|
|
70
70
|
| `action.result` | Show tool call results. |
|
|
71
71
|
| `input.requested` | Pause the UI for approval or a question answer. |
|
|
72
72
|
| `result.completed` | Read structured output from an [output schema](./output-schema). |
|