eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
521 lines (520 loc) • 23.5 kB
TypeScript
import type { LanguageModel } from "ai";
import type { StandardSchemaV1 } from "#compiled/@standard-schema/spec/index.js";
import type { HandleMessageStreamEvent, RuntimeIdentity } from "#protocol/message.js";
import type { CancelSessionResult, SendTurnInput, SessionState } from "#client/types.js";
import type { InputRequest, InputResponse } from "#runtime/input/types.js";
import type { JsonObject, JsonValue } from "#shared/json.js";
import type { AgentModelOptionsDefinition } from "#shared/agent-definition.js";
import type { EvalReporter } from "#evals/runner/reporters/types.js";
import type { EveEvalEventMatch, EveEvalInputRequestMatchOptions, EveEvalSkillLoadMatchOptions, EveEvalSubagentCallMatchOptions, EveEvalToolCallMatchOptions } from "#evals/match.js";
/** Lifecycle outcome of an eval-observed tool or subagent action. */
export type EveEvalActionStatus = "pending" | "completed" | "failed" | "rejected";
/**
* One tool call extracted from the captured stream, pairing the
* `actions.requested` request with its matching `action.result`.
*/
export interface EveEvalToolCall {
/** Authored tool name (e.g. `"get_weather"`). */
readonly name: string;
/** Tool input as requested by the model. */
readonly input: JsonObject;
/** Tool output from the matching `action.result`; `undefined` when the call never resolved. */
readonly output: JsonValue | undefined;
/** Whether the request is unresolved, completed, failed, or user-rejected. */
readonly status: EveEvalActionStatus;
/** Zero-based index of the turn the call happened in. */
readonly turnIndex: number;
/** Owning session id, when the runner knows it. */
readonly sessionId?: string;
}
/**
* One subagent delegation extracted from the captured stream
* (`subagent.called` / `subagent.started`, joined with `subagent.completed`).
*/
export interface EveEvalSubagentCall {
/** Runtime-action call id joining this delegation's lifecycle events, when observed. */
readonly callId?: string;
/** Durable child session id for local and remote workflow delegations. */
readonly childSessionId?: string;
/** Subagent name. */
readonly name: string;
/** Remote agent URL for remote delegations (`subagent.called` remote metadata). */
readonly remoteUrl?: string;
/** Output from the matching `subagent.completed` event; `undefined` when the call never completed. */
readonly output?: JsonValue;
/** Whether the delegation is unresolved, completed, failed, or rejected. */
readonly status: EveEvalActionStatus;
/** Zero-based index of the turn the delegation happened in. */
readonly turnIndex: number;
/** Owning session id, when the runner knows it. */
readonly sessionId?: string;
}
/**
* Execution facts the runner extracts from a completed session's stream events.
*/
export interface EveEvalDerivedFacts {
readonly toolCalls: readonly EveEvalToolCall[];
readonly toolCallCount: number;
readonly subagentCalls: readonly EveEvalSubagentCall[];
readonly subagentCallCount: number;
/** Every HITL input request raised during the run (`input.requested`). */
readonly inputRequests: readonly InputRequest[];
/** True when the run ended parked on unanswered HITL input requests. */
readonly parked: boolean;
readonly messageCount: number;
readonly reasoningBlockCount: number;
readonly failureCode?: string;
}
/**
* Captured event stream and facts for one session involved in an eval.
*/
export interface EveEvalSessionResult {
readonly derived: EveEvalDerivedFacts;
readonly events: readonly HandleMessageStreamEvent[];
readonly primary: boolean;
readonly sessionId?: string;
readonly state: SessionState;
}
/**
* Full result of executing one eval against an eve agent.
*/
export interface EveEvalTaskResult {
/**
* The final turn's structured data when present, otherwise its last assistant
* message. Retained for reporters and artifacts that log one output value.
*/
output: unknown;
/** The agent's last assistant message, or null when none was produced. */
readonly finalMessage: string | null;
readonly sessionId?: string;
/**
* How the run's final turn ended: `"completed"` (session finished),
* `"failed"` (terminal failure), or `"waiting"` (parked for the next
* user message).
*/
readonly status: "completed" | "failed" | "waiting";
/** The captured stream events from the run. */
readonly events: readonly HandleMessageStreamEvent[];
/** Lines written through `t.log` while the eval ran. */
readonly logs?: readonly string[];
/** Facts extracted from the stream (tool calls, message counts, etc.). */
readonly derived: EveEvalDerivedFacts;
/** Per-session event streams captured while executing this eval. */
readonly sessions?: readonly EveEvalSessionResult[];
/**
* Runtime identity metadata captured from the `session.started` stream event.
* Present when the eve server populates the event with its runtime metadata.
*/
readonly runtimeIdentity?: RuntimeIdentity;
}
/**
* How a failing assertion affects the verdict. A `"gate"` is a hard
* assertion: missing it fails the eval. A `"soft"` assertion is tracked
* data that only fails the eval under `eve eval --strict` (and only when it
* carries a threshold).
*/
export type AssertionSeverity = "gate" | "soft";
/**
* A value-level assertion produced by the builders in `eve/evals/expect`
* (e.g. `includes`, `equals`, `similarity`) and applied to an explicit value
* via `t.check(value, assertion)`. Boolean assertions score exactly 0 or 1.
*
* The chainable `gate`/`soft`/`atLeast` return a new assertion with the
* severity or threshold overridden, so the threshold rides on the assertion
* itself rather than a detached map.
*/
export interface Assertion {
readonly name: string;
readonly severity: AssertionSeverity;
/** Minimum passing score. `undefined` on a soft assertion = tracked only. */
readonly threshold?: number;
score(value: unknown): number | Promise<number>;
gate(threshold?: number): Assertion;
soft(threshold?: number): Assertion;
atLeast(threshold: number): Assertion;
}
/**
* Handle to a recorded assertion, returned by every `t` assertion method.
* Chain `gate`/`soft`/`atLeast` to override the recorded severity or
* threshold. Recorded assertions are finalized by the runner; use `t.require`
* or a `require*` lookup when later control flow depends on a passing result.
*/
export interface AssertionHandle {
gate(threshold?: number): this;
soft(threshold?: number): this;
atLeast(threshold: number): this;
}
/**
* The recorded outcome of one assertion, consumed by the verdict, reporters,
* and artifacts. A boolean assertion has `score` 0 or 1.
*/
export interface AssertionResult {
readonly name: string;
readonly score: number;
readonly severity: AssertionSeverity;
readonly threshold?: number;
readonly passed: boolean;
/** Human-readable failure detail, shown in console output and artifacts. */
readonly message?: string;
readonly metadata?: Readonly<Record<string, unknown>>;
}
/** Recorded assertions shared by aggregate, session, and turn scopes. */
export interface EveEvalAssertions {
succeeded(): AssertionHandle;
parked(): AssertionHandle;
messageIncludes(token: string | RegExp): AssertionHandle;
calledTool(name: string, options?: EveEvalToolCallMatchOptions): AssertionHandle;
/** Sugar for `calledTool("load_skill", { input: { skill }, ... })`. */
loadedSkill(skill: string, options?: EveEvalSkillLoadMatchOptions): AssertionHandle;
notCalledTool(name: string): AssertionHandle;
/** Asserts that tool requests appeared in order, allowing unrelated requests between them. */
toolOrder(names: readonly string[]): AssertionHandle;
usedNoTools(): AssertionHandle;
maxToolCalls(max: number): AssertionHandle;
calledSubagent(name: string, options?: EveEvalSubagentCallMatchOptions): AssertionHandle;
noFailedActions(): AssertionHandle;
event<TType extends HandleMessageStreamEvent["type"]>(type: TType, options?: Omit<Extract<EveEvalEventMatch, {
type: TType;
}>, "type">): AssertionHandle;
notEvent<TType extends HandleMessageStreamEvent["type"]>(type: TType, options?: Omit<Extract<EveEvalEventMatch, {
type: TType;
}>, "type" | "count">): AssertionHandle;
eventOrder(matchers: readonly EveEvalEventMatch[]): AssertionHandle;
eventsSatisfy(label: string, predicate: (events: readonly HandleMessageStreamEvent[]) => boolean): AssertionHandle;
}
/** Assertions over the final output captured by one session or immutable turn. */
export interface EveEvalOutputAssertions {
outputEquals(value: unknown): AssertionHandle;
outputMatches(schema: StandardSchemaV1): AssertionHandle;
}
/** Typed stream event returned by {@link EveEvalLiveTurn.waitForEvent}. */
export type EveEvalStreamEvent<TType extends HandleMessageStreamEvent["type"] = HandleMessageStreamEvent["type"]> = Extract<HandleMessageStreamEvent, {
type: TType;
}>;
/** Matcher options for waiting until one live turn emits a specific event. */
export type EveEvalWaitForEventOptions<TType extends HandleMessageStreamEvent["type"]> = Omit<Extract<EveEvalEventMatch, {
type: TType;
}>, "count" | "type">;
/**
* One accepted turn whose event stream is still in progress.
*
* The handle owns the stream consumer: event waiters observe its buffer and
* {@link result} settles and records that same stream exactly once.
*/
export interface EveEvalLiveTurn {
/** Events observed on this turn so far. */
readonly events: readonly HandleMessageStreamEvent[];
/** Session driver that started or owns this turn. */
readonly session: EveEvalSession;
/** Durable session id available as soon as the turn is accepted or attached. */
readonly sessionId: string;
/** Request cooperative cancellation of this turn's session. */
cancel(): Promise<CancelSessionResult>;
/** Wait for the turn boundary and return the recorded immutable result. */
result(): Promise<EveEvalTurn>;
/** Wait until the live stream emits one typed event matching `options`. */
waitForEvent<TType extends HandleMessageStreamEvent["type"]>(type: TType, options?: EveEvalWaitForEventOptions<TType>): Promise<EveEvalStreamEvent<TType>>;
}
/** Operations and state shared by the primary eval context and independent sessions. */
export interface EveEvalSessionDriver {
/** All events observed on this session so far. */
readonly events: readonly HandleMessageStreamEvent[];
/** Input requests left pending by the last parked turn. */
readonly pendingInputRequests: readonly InputRequest[];
/** Serializable cursor for resuming this session. */
readonly state: SessionState;
/** eve session id after the first successful send. */
readonly sessionId: string | undefined;
/** Request cooperative cancellation of this session's active turn. */
cancel(): Promise<CancelSessionResult>;
/** Require exactly one pending input request matching `filter`, or abort dependent control flow. */
requireInputRequest(filter?: EveEvalInputRequestMatchOptions): InputRequest;
/** Resolve specific pending requests and run the resumed turn. */
respond(...responses: InputResponse[]): Promise<EveEvalTurn>;
/** Resolve every pending request with the same option id. */
respondAll(optionId: string): Promise<EveEvalTurn>;
/** Send one turn through this session. */
send(input: SendTurnInput): Promise<EveEvalTurn>;
/** Start one turn and return as soon as its session is accepted. */
start(input: SendTurnInput): Promise<EveEvalLiveTurn>;
/** Send one text turn with a local file attached as a data URL. */
sendFile(text: string, filePath: string, mediaType?: string): Promise<EveEvalTurn>;
}
/** Driver for one independent session, exposed by `t.newSession()` and target attachment helpers. */
export interface EveEvalSession extends EveEvalSessionDriver, EveEvalAssertions, EveEvalOutputAssertions {
}
/**
* One completed eval-driver turn.
*/
export interface EveEvalTurn extends EveEvalAssertions, EveEvalOutputAssertions {
readonly data: unknown;
readonly events: readonly HandleMessageStreamEvent[];
readonly inputRequests: readonly InputRequest[];
readonly message: string | undefined;
readonly sessionId: string;
readonly status: "completed" | "failed" | "waiting";
readonly toolCalls: readonly EveEvalToolCall[];
/** Require exactly one matching tool call, record a gate, and return it for dependent checks. */
requireToolCall(name: string, options?: Omit<EveEvalToolCallMatchOptions, "count">): EveEvalToolCall;
expectOk(): this;
}
/**
* The judge model used by `t.judge.*` assertions, configured per-eval or as
* the run-wide default in `evals.config.ts`. Only ever used for scoring; it
* never changes the agent under test. String model ids route through the
* Vercel AI Gateway; provider model instances run directly.
*/
export interface EveEvalJudgeConfig {
readonly model: LanguageModel;
readonly modelOptions?: AgentModelOptionsDefinition;
}
/**
* Per-call options for `t.judge.autoevals.*` assertions.
*/
export interface JudgeOpts {
/** Value to grade. Defaults to the final assistant message (`t.reply`). */
readonly on?: unknown;
/** Judge model for this call only; overrides the eval/config judge model. */
readonly model?: LanguageModel;
readonly modelOptions?: AgentModelOptionsDefinition;
}
/**
* Braintrust autoevals graders, bound to the resolved judge model. The grader
* family is named so its semantics are explicit: `factuality`'s consistency
* buckets and `closedQA`'s yes/no grading are autoevals' behavior, not eve's.
* These are eve-owned wrappers, not the raw library.
*/
export interface AutoevalsJudges {
factuality(expected: string, opts?: JudgeOpts): AssertionHandle;
summarizes(expected: string, opts?: JudgeOpts): AssertionHandle;
closedQA(criteria: string, opts?: JudgeOpts): AssertionHandle;
sql(expected: string, opts?: JudgeOpts): AssertionHandle;
}
/**
* Model-backed assertion namespaces on `t.judge`. A future non-autoevals
* engine would slot in as a sibling of `autoevals`.
*/
export interface JudgeContext {
readonly autoevals: AutoevalsJudges;
}
/**
* The single context passed to an eval's `test(t)` function. It drives the
* primary session, carries the run-level
* and value-level assertion vocabulary, and exposes `judge` for LLM-as-judge.
*
* Scoped assertions (`succeeded`, `calledTool`, …) record an entry evaluated
* after the test body; `check`, `require`, and `judge` evaluate explicit values.
*/
export interface EveEvalContext extends EveEvalSessionDriver, EveEvalAssertions {
/** Eval timeout signal. */
readonly signal: AbortSignal;
/** Current target under test. */
readonly target: EveEvalTargetHandle;
/** The primary session's last assistant message, or null. */
readonly reply: string | null;
/** Structured eval log hook. */
log(message: string): void;
/** Pause the eval task, defaulting to 1 second, while respecting the eval timeout signal. */
sleep(ms?: number): Promise<void>;
/** Create an additional independent session against the same target. */
newSession(): EveEvalSession;
/** Apply a value-level assertion (from `eve/evals/expect`) to a value. */
check(value: unknown, assertion: Assertion): AssertionHandle;
/** Record an immediate gate and abort dependent control flow when it fails. */
require<T>(value: T, assertion: Assertion): Promise<T>;
/** Mark this eval as intentionally skipped and stop executing its test body. */
skip(reason: string): never;
/** LLM-as-judge assertions, bound to the resolved judge model. */
readonly judge: JudgeContext;
}
/**
* Describes the eve server an eval runs against.
*/
export interface EveEvalTarget {
/**
* `"local"` for a dev server the runner starts in-process, `"remote"` for
* a deployed instance addressed by `--url`.
*/
readonly kind: "local" | "remote";
/** Base HTTP URL the eval client connects to and sends message requests. */
readonly url: string;
/** Capabilities discovered from the live target's info route. */
readonly capabilities: EveEvalTargetCapabilities;
}
export interface EveEvalTargetCapabilities {
readonly devRoutes: boolean;
}
export interface EveEvalScheduleDispatchResult {
readonly scheduleId: string;
readonly sessionIds: readonly string[];
}
/**
* Live target handle exposed to eval runs.
*/
export interface EveEvalTargetHandle extends EveEvalTarget {
/** Dispatch a dev-only authored schedule. Requires a target with dev routes enabled. */
dispatchSchedule(scheduleId: string): Promise<EveEvalScheduleDispatchResult>;
/** Authenticated fetch against the target base URL. */
fetch(path: string, init?: RequestInit): Promise<Response>;
/**
* Attach to a pre-existing session and consume one turn boundary.
*
* When that boundary is `session.waiting`, the attached session recovers
* the current continuation token from the stream, so `session.send(...)`
* and `session.respond(...)` continue the same durable session.
*/
attachSession(sessionId: string, opts?: {
readonly startIndex?: number;
}): Promise<EveEvalSession>;
/**
* Observe one in-progress turn from a session created outside the eval.
* The returned live-turn handle starts consuming immediately and owns the
* stream through its next turn boundary.
*/
watchTurn(sessionId: string, opts?: {
readonly startIndex?: number;
}): EveEvalLiveTurn;
}
/**
* Shared fields between the user-facing input and the validated eval.
*
* Eval identity (`id`) is derived from the `evals/<path>.eval.ts` file
* path by the discovery layer; it is not authored on the input.
*/
interface EveEvalBase {
readonly description?: string;
/**
* Judge model for this eval's `t.judge.*` assertions. Optional: when
* omitted, judge assertions fall back to the `judge` declared in
* `evals.config.ts`. Only used for scoring; never changes the agent
* under test.
*/
readonly judge?: EveEvalJudgeConfig;
readonly timeoutMs?: number;
/** Used by `--tag` filtering. */
readonly tags?: readonly string[];
readonly metadata?: Readonly<Record<string, unknown>>;
readonly reporters?: readonly EvalReporter[];
}
/**
* Complete top-level key set accepted by {@link defineEval}, used to reject
* unknown authored keys.
*/
export interface EveEvalInputFields extends EveEvalBase {
readonly test?: (t: EveEvalContext) => void | Promise<void>;
}
/**
* Full eval input passed to `defineEval()`.
*
* Each eval file is exactly one case: an imperative `test(t)` function that
* drives the agent and asserts on what it produced. Eval identity is derived
* from the file path, so authors do not specify an `id` or `name`.
*/
export interface EveEvalInput extends EveEvalBase {
/** Imperative interaction-and-assertion script. */
test(t: EveEvalContext): void | Promise<void>;
}
/**
* Eval returned by `defineEval()`. Carries no `id` yet: discovery stamps
* the path-derived id at import time to produce a full {@link EveEval}. The
* `_tag` literal (`"EveEval"`) brands the value so discovery and the runner
* can recognize a defined eval.
*/
export type EveEvalDefinition = EveEvalInput & {
readonly _tag: "EveEval";
};
/**
* Validated eval consumed by the runner and reporters. The `id` is the
* path-derived slug attached by discovery (e.g. `evals/weather.eval.ts` →
* `"weather"`, `evals/runtime/multi-turn.eval.ts` → `"runtime/multi-turn"`).
* Files that default-export an array of evals derive
* `<file-id>/<zero-padded index>` ids (e.g. `"weather/0000"`).
*/
export type EveEval = EveEvalDefinition & {
readonly id: string;
};
/**
* Per-eval outcome computed by the runner:
*
* - `"passed"` — no execution error, every gate held, every soft threshold met
* - `"failed"` — a gate assertion failed or execution errored (timeout, transport, thrown task)
* - `"scored"` — every gate held but a soft assertion fell below its threshold
* - `"skipped"` — the test body intentionally called `t.skip(reason)`
*/
export type EveEvalVerdict = "passed" | "failed" | "scored" | "skipped";
/**
* Result of executing and asserting one eval.
*
* `id` is the path-derived eval id
* (e.g. `evals/weather.eval.ts` → `"weather"`).
*/
export interface EveEvalResult {
readonly id: string;
readonly result: EveEvalTaskResult;
/** Every assertion recorded by the eval's `test(t)`, in record order. */
readonly assertions: readonly AssertionResult[];
/** Per-eval verdict; see {@link EveEvalVerdict}. */
readonly verdict: EveEvalVerdict;
readonly error?: string;
/** Why the eval intentionally skipped, present only for a `"skipped"` verdict. */
readonly skipReason?: string;
readonly startedAt: string;
readonly completedAt: string;
}
/**
* Aggregated outcome of one `eve eval` run across every executed eval.
*/
export interface EveEvalRunSummary {
readonly target: EveEvalTarget;
readonly results: readonly EveEvalResult[];
readonly startedAt: string;
readonly completedAt: string;
/** Evals with verdict `"passed"`. */
readonly passed: number;
/** Evals with verdict `"failed"` (gate failures and execution errors). */
readonly failed: number;
/** Evals with verdict `"scored"` (below-threshold soft assertions only). */
readonly scored: number;
/** Evals intentionally skipped by their test body. */
readonly skipped: number;
/** The execution-error subset of `failed` (timeouts, connection failures, exceptions). */
readonly errored: number;
}
/**
* Run-wide eval configuration authored in `evals.config.ts`.
*
* Exactly one `evals.config.ts` is required at the root of the `evals/`
* directory; it supplies the defaults every eval in the run shares.
*/
export interface EveEvalConfigInput {
/**
* Default judge model for `t.judge.*` assertions across every eval.
* Optional: evals that use no judge need not set it, and individual evals
* may override it with their own `judge`. Only ever used for scoring.
*/
readonly judge?: EveEvalJudgeConfig;
/**
* Reporters that observe every eval in the run (e.g. a shared
* `Braintrust()` experiment). Suppressed by `eve eval --skip-report`.
*/
readonly reporters?: readonly EvalReporter[];
/**
* Default maximum number of evals executing at once. Must be a positive
* integer. `eve eval --max-concurrency` overrides it; defaults to 8 when
* neither is set.
*/
readonly maxConcurrency?: number;
/**
* Default per-eval timeout in milliseconds. An eval's own `timeoutMs`
* overrides it, and `eve eval --timeout` overrides both.
*/
readonly timeoutMs?: number;
}
/**
* Validated eval run configuration returned by `defineEvalConfig()`. The
* `_tag` literal brands the value so discovery can recognize it.
*/
export type EveEvalConfig = EveEvalConfigInput & {
readonly _tag: "EveEvalConfig";
};
export {};