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.
Files changed (72) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/channel/compiled-channel.js +2 -1
  3. package/dist/src/cli/dev/tui/runner.js +1 -1
  4. package/dist/src/cli/dev/tui/setup-panel.d.ts +3 -4
  5. package/dist/src/cli/dev/tui/setup-panel.js +2 -2
  6. package/dist/src/cli/dev/tui/terminal-renderer.js +1 -1
  7. package/dist/src/evals/assertions/collector.d.ts +19 -6
  8. package/dist/src/evals/assertions/collector.js +1 -1
  9. package/dist/src/evals/assertions/run.d.ts +24 -17
  10. package/dist/src/evals/assertions/run.js +2 -2
  11. package/dist/src/evals/assertions/scoped.d.ts +17 -0
  12. package/dist/src/evals/assertions/scoped.js +1 -0
  13. package/dist/src/evals/context.d.ts +1 -0
  14. package/dist/src/evals/context.js +1 -1
  15. package/dist/src/evals/control-flow.d.ts +9 -0
  16. package/dist/src/evals/control-flow.js +1 -0
  17. package/dist/src/evals/define-eval.d.ts +1 -1
  18. package/dist/src/evals/define-eval.js +1 -1
  19. package/dist/src/evals/expect/index.d.ts +5 -3
  20. package/dist/src/evals/expect/index.js +1 -1
  21. package/dist/src/evals/index.d.ts +2 -2
  22. package/dist/src/evals/match.d.ts +50 -13
  23. package/dist/src/evals/match.js +1 -1
  24. package/dist/src/evals/runner/artifacts.js +1 -1
  25. package/dist/src/evals/runner/derive-run-facts.js +1 -1
  26. package/dist/src/evals/runner/execute-eval.js +1 -1
  27. package/dist/src/evals/runner/execute-task.d.ts +1 -0
  28. package/dist/src/evals/runner/execute-task.js +1 -1
  29. package/dist/src/evals/runner/reporters/braintrust.js +1 -1
  30. package/dist/src/evals/runner/reporters/console.js +1 -1
  31. package/dist/src/evals/runner/reporters/junit.js +3 -2
  32. package/dist/src/evals/runner/run-evals.js +1 -1
  33. package/dist/src/evals/runner/verdict.d.ts +1 -0
  34. package/dist/src/evals/runner/verdict.js +1 -1
  35. package/dist/src/evals/session.d.ts +9 -5
  36. package/dist/src/evals/session.js +1 -1
  37. package/dist/src/evals/types.d.ts +69 -47
  38. package/dist/src/harness/emission.d.ts +13 -27
  39. package/dist/src/harness/emission.js +1 -1
  40. package/dist/src/harness/step-hooks.d.ts +3 -3
  41. package/dist/src/harness/step-hooks.js +1 -1
  42. package/dist/src/harness/tool-loop.js +1 -1
  43. package/dist/src/internal/application/compiled-artifacts.js +1 -1
  44. package/dist/src/internal/application/package.d.ts +12 -0
  45. package/dist/src/internal/application/package.js +1 -1
  46. package/dist/src/internal/application/paths.d.ts +6 -0
  47. package/dist/src/internal/application/paths.js +1 -1
  48. package/dist/src/internal/nitro/host/configure-nitro-routes.js +2 -2
  49. package/dist/src/internal/workflow/configure-world.d.ts +6 -0
  50. package/dist/src/internal/workflow/configure-world.js +1 -1
  51. package/dist/src/internal/workflow/world-compatibility.d.ts +32 -0
  52. package/dist/src/internal/workflow/world-compatibility.js +1 -0
  53. package/dist/src/protocol/message.d.ts +7 -5
  54. package/dist/src/runtime/actions/types.d.ts +10 -11
  55. package/dist/src/setup/boxes/resolve-provisioning.js +1 -1
  56. package/dist/src/setup/scaffold/create/project.js +1 -1
  57. package/docs/agent-config.md +13 -3
  58. package/docs/concepts/execution-model-and-durability.md +1 -1
  59. package/docs/concepts/sessions-runs-and-streaming.md +1 -1
  60. package/docs/connections/overview.mdx +40 -2
  61. package/docs/evals/assertions.mdx +97 -39
  62. package/docs/evals/cases.mdx +24 -15
  63. package/docs/evals/judge.mdx +2 -2
  64. package/docs/evals/overview.mdx +7 -5
  65. package/docs/evals/reporters.mdx +2 -2
  66. package/docs/evals/running.mdx +3 -1
  67. package/docs/evals/targets.mdx +2 -2
  68. package/docs/guides/auth-and-route-protection.md +48 -1
  69. package/docs/guides/client/streaming.mdx +1 -1
  70. package/docs/guides/deployment.md +3 -2
  71. package/docs/tutorial/connect-a-warehouse.mdx +2 -0
  72. package/package.json +1 -1
@@ -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`. If the installed package must stay
103
- external in hosted output, list it in `build.externalDependencies`.
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: "Connections"
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. Connect-managed OAuth is user-scoped by default, so the runtime resolves the per-user token before each tool call. The full setup (Connect client provisioning, project linking, the runtime consent flow) lives in [Auth & route protection](/docs/guides/auth-and-route-protection).
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: "Run-level methods, t.check value assertions, the matcher mini-language, and gate vs soft severity."
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 **records** a result onto `t` and returns a chainable handle. The runner reads the recorded results to compute the verdict, so a single run reports every failing assertion rather than dying on the first. There are two deterministic surfaces: run-level methods on `t`, and `t.check` for grading a specific value. For model-graded assertions, see [Judge](./judge).
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
- ## Run-level assertions
8
+ ## Scoped assertions
9
9
 
10
- Run-level assertions read the whole run, so they take no value. They are methods on `t` and gate by default. Several key off whether a run **parked**: paused on an unanswered human-in-the-loop (HITL) input request, waiting for an approval or answer before it can continue.
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 | Asserts |
13
- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
14
- | `t.completed()` | The run did not fail and did not park on unanswered HITL input |
15
- | `t.didNotFail()` | No terminal failure and no `turn.failed`/`step.failed` events (parked runs pass) |
16
- | `t.waiting()` | The run parked on HITL input (for approval-shaped evals) |
17
- | `t.messageIncludes(token)` | Joined assistant text contains `token` (string or RegExp) |
18
- | `t.outputEquals(value)` / `t.outputMatches(schema)` | Deep equality or Standard Schema (e.g. Zod) validation of the agent's structured output |
19
- | `t.calledTool(name, opts?)` | A matching tool call happened (`input`, `output`, `isError`, `times` constraints) |
20
- | `t.loadedSkill(skill, opts?)` | Sugar for `t.calledTool("load_skill", { input: { skill }, ...opts })` |
21
- | `t.notCalledTool(name)` | No call to `name` |
22
- | `t.toolOrder([...names])` | Tool names appear in order (other calls may interleave) |
23
- | `t.usedNoTools()` | No tool calls at all |
24
- | `t.maxToolCalls(n)` | At most `n` tool calls |
25
- | `t.noFailedActions()` | No tool, subagent, or skill action reported a failure |
26
- | `t.calledSubagent(name, opts?)` | A subagent delegation happened (`remoteUrl`, `output` constraints) |
27
- | `t.event(predicate, label)` | Escape hatch: any predicate over the typed event stream |
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
- `t.completed()` subsumes `t.didNotFail()`, so reach for `completed` unless you specifically want to allow a parked run. The structured output that `t.outputEquals` and `t.outputMatches` read is the agent's structured output (see the [output schema guide](../guides/client/output-schema)).
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.completed();
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("sunny")); // substring (gate)
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 | Default |
53
- | ---------------------- | ------------------------------------------------ | ------- |
54
- | `includes(substring)` | value (coerced to string) contains `substring` | gate |
55
- | `equals(value)` | deep structural equality | gate |
56
- | `matches(schema)` | validates against a Standard Schema | gate |
57
- | `similarity(expected)` | normalized Levenshtein similarity, 1 = identical | soft |
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 a matcher object: `{ input, output, isError, times }` for tools, `{ remoteUrl, output }` for subagents. Each field accepts a literal (objects partial-deep-match), a RegExp, or a function. A matcher function receives the value and returns either a boolean (acts as a predicate) or an expected value to compare against (handy for runner-assigned values like environment-provided URLs):
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/ }, isError: false, times: 1 });
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, error state), 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, and that is what `t.completed()` and `t.waiting()` key off.
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
- The built-in assertions cover almost everything. When you need to read the stream directly, `t.event(predicate, label)` is the escape hatch:
111
+ Typed event matching covers presence, absence, exact counts, partial event data, and ordering:
81
112
 
82
113
  ```ts
83
- t.event(
84
- (events) =>
85
- events.some((e) => e.type === "message.completed" && e.data.message?.includes(marker)),
86
- "assistant reply includes the marker",
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.
@@ -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.completed();
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.completed();
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
- For a precondition no built-in assertion expresses, `throw`. A thrown error marks the eval `failed` with the message in the result:
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 { includes } from "eve/evals/expect";
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.expectOk();
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.completed();
90
- t.check(second.message, includes("Thanks for remembering."));
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.expectInputRequests(filter?)` asserts the previous turn parked on HITL input and returns the pending requests.
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 a turn whose `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.
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.completed();
138
+ t.succeeded();
130
139
  t.check(t.reply, equals(row.sql));
131
140
  },
132
141
  }),
@@ -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.completed();
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. Several slow judge calls in one eval can fan out with `await Promise.all([...])`.
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
 
@@ -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.completed();
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.expectInputRequests(...)`, `t.newSession()`. Read what came back with `t.reply` (the last assistant message), `t.sessionId`, and `t.events`. See [Cases](./cases).
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
- - **Run-level methods** read the whole run, like `t.completed()`, `t.calledTool("get_weather")`, `t.usedNoTools()`, and `t.toolOrder([...])`. They take no value because they observe the run itself. See [Assertions](./assertions).
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.completed(); // gate
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.completed()` 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.
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
 
@@ -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.completed();
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 land as failure messages on the matching test case, so CI surfaces them inline.
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
 
@@ -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.
@@ -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.completed();
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 with `send` and read their event streams. The run-level assertions on `t` (`t.completed()`, `t.calledTool(...)`) read the whole run, including attached sessions.
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 the 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 in place of `connect()`. [Connections](../connections) covers both shapes.
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 requested by the model. |
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). |