UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

183 lines (135 loc) 8.62 kB
--- title: "Cases" description: "Author single-turn and multi-turn evals with test(t), and fan one file out over a dataset." --- 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. 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: ```ts title="evals/evals.config.ts" import { defineEvalConfig } from "eve/evals"; export default defineEvalConfig({}); ``` ## Single-turn evals 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: ```ts title="evals/weather/brooklyn-forecast.eval.ts" import { defineEval } from "eve/evals"; import { includes } from "eve/evals/expect"; export default defineEval({ async test(t) { await t.send("What is the weather in Brooklyn?"); t.succeeded(); t.check(t.reply, includes("Sunny")); }, }); ``` Some evals only care about behavior, not text. Assert on the run and skip the content check entirely: ```ts title="evals/weather/no-tools-for-greetings.eval.ts" import { defineEval } from "eve/evals"; export default defineEval({ async test(t) { await t.send("Hello!"); t.succeeded(); t.notCalledTool("get_weather"); }, }); ``` ## Organizing with directories Identity is the file path, so directories are the grouping mechanism. `evals/weather/brooklyn-forecast.eval.ts` gets the id `weather/brooklyn-forecast`, and `eve eval weather` runs everything under `evals/weather/`. Shared constants and helpers live in sibling non-eval files (any name that doesn't end in `.eval.ts`): ```text evals/ ├── evals.config.ts ├── weather/ │ ├── shared.ts # helpers, not an eval │ ├── brooklyn-forecast.eval.ts │ └── no-tools-for-greetings.eval.ts └── smoke.eval.ts ``` ## Multi-turn evals Drive several turns in sequence for branching, HITL approvals, structured output, attachments, or multiple sessions. Because assertions live in the function, an intermediate value is a local variable. Judge a draft before the next turn overwrites it, then keep going. ```ts title="evals/draft-then-send.eval.ts" import { defineEval } from "eve/evals"; import { includes } from "eve/evals/expect"; export default defineEval({ async test(t) { const draft = await t.send("Draft the follow-up email."); t.check(draft.message, includes("Best regards")); t.judge.autoevals.closedQA("professional tone", { on: draft.message }).atLeast(0.6); await t.send("Now send it."); t.calledTool("send_email"); }, }); ``` 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: ```ts title="evals/session-continuity.eval.ts" import { defineEval } from "eve/evals"; import { equals } from "eve/evals/expect"; export default defineEval({ async test(t) { const first = await t.send("My favorite word is marigold."); const second = await t.send("Thanks for remembering."); await t.require(second.sessionId, equals(first.sessionId)); t.succeeded(); second.messageIncludes("Thanks for remembering."); }, }); ``` ## The drive API `t` drives the primary session; `t.newSession()` returns an independent `EveEvalSession` against the same target, whose events feed the same run-level assertions. - `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()`. - `t.start(input)` starts a turn but returns as soon as the server accepts it. The returned live turn exposes `.sessionId`, `.waitForEvent(...)`, `.cancel()`, and `.result()` for coordinating with work that is still running. - `t.cancel()` requests cooperative cancellation of the primary session's active turn. Both `accepted` and `no_active_turn` are successful outcomes. - `t.sendFile(text, path, mediaType?)` attaches a local file as a data URL. - `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. - `t.respond(...responses)` answers specific pending input requests and sends them as the next turn. - `t.respondAll(optionId)` answers every pending input request with the same option and sends the responses as the next turn. - `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. 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. ## In-flight turns Use `start()` when the eval must observe or affect a turn before it settles. A live turn owns one stream consumer: `waitForEvent()` reads typed events from its buffer, and `result()` waits for the boundary and records the same buffered stream as an immutable turn. Event data matchers use the same partial-deep matcher language as `t.event(...)`. ```ts title="evals/cancel-running-tool.eval.ts" import { defineEval } from "eve/evals"; export default defineEval({ async test(t) { const live = await t.start("Run the long operation."); await live.waitForEvent("actions.requested", { data: { actions: (actions) => actions.some( (action) => action.kind === "tool-call" && action.toolName === "long_operation", ), }, }); await live.cancel(); const turn = await live.result(); turn.eventOrder([{ type: "turn.cancelled" }, { type: "session.waiting" }]); }, }); ``` `waitForEvent()` rejects if the stream fails or reaches its turn boundary before the requested event. Call `result()` after any coordination to settle the stream and make its events available to run-level assertions. Use `expectOk()` only when the next operation depends on that intermediate turn succeeding. A final `t.succeeded()` already records a complete-run gate. 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. 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. For driving sessions created outside the eval, by a channel webhook or a schedule, see [Targets](./targets). ## Datasets: exporting an array To fan one file out over a dataset, default-export an array of `defineEval(...)` values. Eval modules are ESM, so top-level `await` can load anything. Ids derive from the file name plus a zero-padded index in array order (`sql/0000`, `sql/0001`, and so on). The loaders (`loadJson`, `loadYaml` from `eve/evals/loaders`) parse fixture files relative to the app root: ```ts title="evals/sql.eval.ts" import { defineEval } from "eve/evals"; import { loadYaml } from "eve/evals/loaders"; import { equals } from "eve/evals/expect"; const doc = await loadYaml("evals/data/cases.yaml"); const rows = doc.evals as readonly { task: string; prompt: string; sql: string }[]; export default rows.map((row) => defineEval({ description: row.task, async test(t) { await t.send(row.prompt); t.succeeded(); t.check(t.reply, equals(row.sql)); }, }), ); ``` The loaders are meant for fixtures, not runtime agent code. ## What to read next - [Assertions](./assertions): assert on what the eval did - [Judge](./judge): grade quality with an LLM judge - [TypeScript client](../guides/client/messages): the send/turn protocol eval sessions build on