Skip to main content
Before writing an assertion or Judge check, make the Agent do something. Drive is the part of test(t) that sends input and receives results: t.send(), t.sendFile(), t.newSession(), and the HITL methods t.respond() / t.respondAll(). Drive produces a Turn. Every assertion and Judge check in NiceEval reads data from a Turn; see Assert and Judge.

t.send() and the Turn it returns

t.send(input) is the Adapter’s basic execution verb. Under the hood, it calls the Adapter’s send(input, ctx) and normalizes the result into a Turn:
t.reply is shorthand for the main session’s last assistant message. It is equivalent to the turn.message from the latest t.send(). t.events is the full accumulated event stream for the main session. When you need structured output, declare output on send using a Standard Schema such as zod. The declaration both asks the application for that output and supplies the type of turn.data:
The Runner validates the value against the declaration immediately after the Adapter returns. If data does not match, the turn is immediately failed and reports the difference. A turn that did not declare output has no turn.data. It is not a pocket for “whatever the Agent happened to return”; the field exists only when you declare it and is strongly typed when it does.
turn.succeeded() looks only at this turn: its status and whether it is waiting for an unanswered HITL request. If later turns depend on an earlier one succeeding, use await turn.succeeded().orStop().

One turn with a file: t.sendFile()

t.sendFile(path, text?) reads a local file relative to the Eval’s directory, infers a MIME type from the extension, and attaches it to this turn’s input as a data URL:

Multi-turn conversations

Each await t.send(...) is a new turn in the same conversation. Assign each response to a local variable so you can assert that individual turn as well as use run-level t assertions:
Whether multi-turn t.send() calls truly continue their context depends on whether the Adapter’s send uses ctx.session session-continuation storage, either a typed slot or id + capture(). Without it, every turn is a new conversation. See Adapter and Write Send.

Independent sessions: t.newSession()

t.newSession() opens a second conversation line parallel to the main session, without interfering with it. Its returned session handle has the same drive API (send / sendFile / respond / respondAll) and scoped-assertion vocabulary, but sees only that session’s events:
Do not assume that t.newSession() inherently guarantees isolation. The Runner only guarantees that the new session line starts with empty state. Opening a fresh conversation with the application is the Adapter’s job. An Adapter that ignores its session state and always resumes the same underlying context makes t.newSession() silently share state without an error. When you write an Adapter, verify isolation with an Eval like the one above before you trust it.

Human-in-the-loop (HITL)

Some Agents stop during a turn to wait for approval or missing information instead of finishing immediately. That turn ends with status: "waiting" and has one or more input.requested events that say what it needs. For the full mental model—handshake sequence, Adapter obligations, and rejected semantics—see HITL. This section explains how to use it from an Eval.
t.requireInputRequest(filter) turns a pending HITL request into a concrete value you can inspect and answer. It throws if zero or more than one pending request matches, so use as many filter fields as you can—id, prompt, display, action, optionIds, or input—to disambiguate. t.respond(...) answers the request and emits the next turn. Each argument is one answer: strings align to pending requests in order, while { request, optionId } explicitly identifies a request, which is useful when several requests pause together. Under the hood it is another ordinary send: answer text goes to input.text, while structured answers go to input.responses one per request. A response that selects a request option carries { requestId, optionId }; a free-text response carries { requestId, text }. The Adapter does not need to parse the text: requestId identifies which answer belongs to which request. See Inputs for the different answers for how each response looks to the Adapter. If the current turn has multiple pending requests of the same kind that should all receive the same answer, such as approving a batch of file changes one by one, use t.respondAll(optionId) to answer them at once. It validates optionId against every pending request first. If the option is absent from a request’s options, it throws instead of silently sending the wrong answer:
  • HITL — The complete concept of paused turns waiting for people: handshake sequence and obligations on both sides.
  • Assert — The assertion vocabulary that reads Turn.events and Turn.data.
  • Judge — Pure Judge Matches registered against explicit input and output through check.
  • Adapter — Where capabilities come from: what unlocks t.newSession(), HITL, and tool-related assertions.