> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# NiceEval 里怎么驱动 agent：send、session 与 HITL

> t.send() 和它返回的 Turn、t.sendFile()、多轮对话、t.newSession()，以及 respond() 处理的人工介入(HITL)。

在断言或 judge 之前，先要让 agent 动起来。**Drive** 是 `test(t)` 里发送输入、拿到结果的那部分——`t.send()`、`t.sendFile()`、`t.newSession()`，以及 HITL 的 `t.respond()` / `t.respondAll()`。驱动产出的都是一个 **Turn**，[NiceEval](https://niceeval.com/) 里的所有断言和 judge 都从 Turn 的数据上读，具体看 [Assert](/docs/zh/explanation/assert) 和 [Judge](/docs/zh/explanation/judge)。

## `t.send()` 和它返回的 `Turn`

`t.send(input)` 是唯一的动词。它底层调用 Adapter 的 `send(input, ctx)`，把返回值归一成一个 `Turn`：

```ts theme={null}
const turn = await t.send("布鲁克林今天天气怎么样?");

turn.message;   // 最后一条 assistant 消息
turn.data;      // 结构化产物:仅当这次 send 声明了 output 时存在,类型随声明(见下)
turn.status;    // "completed" | "failed" | "waiting"
turn.events;    // 这一轮的 StreamEvent[]
turn.usage;     // { inputTokens, outputTokens, ... },adapter 报了才有
turn.succeeded(); // 作用域断言:这一轮没失败、也没卡在未回答的 HITL 上
```

`t.reply` 是主 session 最后一条 assistant 消息的简写，等价于最近一次 `t.send()` 返回的 `turn.message`；`t.events` 是主 session 目前累积的完整事件流。

要结构化输出时，在 send 上声明 `output`（Standard Schema，zod 即可）。声明既是给应用的要求，也是 `turn.data` 的类型来源：

```ts theme={null}
const turn = await t.send("这张发票总额多少?", {
  output: z.object({ amount: z.number(), currency: z.string() }),
});
t.check(turn.data.amount, equals(3200));   // turn.data 是 { amount: number; currency: string }
```

adapter 返回后运行器立刻按声明校验：`data` 不匹配，本轮直接 `failed` 并报出差异。没声明 `output` 的轮次没有 `turn.data`——它不是"agent 顺手返回点什么"的口袋，而是你声明了才存在、存在就强类型的字段。

<Tip>
  `turn.succeeded()` 只看这一轮:检查这一轮的 `status`,以及是否卡在未回答的 HITL 请求上。后面几轮依赖前一轮成功时,紧跟着这一轮调它——在一个已经失败的轮次上继续挂断言,通常只会产出一堆混乱的连带失败。
</Tip>

## 带文件的一轮 —— `t.sendFile()`

`t.sendFile(path, text?)` 读一个本地文件(相对评估用例所在目录),按扩展名推断 MIME 类型,把它作为 data URL 附加到这一轮的输入里：

```ts theme={null}
const turn = await t.sendFile("fixtures/invoice.png", "这张发票的总额是多少?");
t.check(turn.message, includes("$"));
```

## 多轮对话

每次 `await t.send(...)` 都是同一个对话上的新一轮。把每轮的返回赋给局部变量,除了 run 级的 `t` 断言外还能单独断言这一轮：

```ts theme={null}
const draft = await t.send("帮我拟一封跟进邮件。");
draft.succeeded();
t.check(draft.message, includes("此致"));
draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);

await t.send("好,发出去。");
t.calledTool("send_email");
```

多轮 `t.send()` 能不能真的续上上文，取决于 Adapter 的 `send` 是否接了 `ctx.session` 的会话续接存取器（`history()` 或 `id` + `capture()`）——没接时每轮各是一场新对话。怎么接见 [Adapter](/docs/zh/explanation/adapter) 与[写 send](/docs/zh/tutorials/write-send)。

## 独立会话 —— `t.newSession()`

`t.newSession()` 开一条与主会话并行、互不干扰的第二条对话线。它返回的 session handle 带同一套 drive API(`send` / `sendFile` / `respond` / `respondAll`)和同一套作用域断言词汇,但只看这条 session 自己的事件：

```ts theme={null}
await t.send("我叫小明。");
await t.send("我叫什么?");
t.check(t.reply, includes("小明"));         // 主 session 记住了

const fresh = t.newSession();
await fresh.send("我叫什么?");
t.check(fresh.reply, satisfies((r) => !r.includes("小明"), "没有记忆泄漏"));
```

<Warning>
  常见错误是想当然认为 `t.newSession()` 天然保证隔离。运行器保证的只是新会话线的状态是空的——真正对应用开一条新对话是 adapter 的事。一个不看自己的会话状态、永远续接同一个底层上下文的 adapter,会让 `t.newSession()` 悄悄共享状态,且不会报错。写 adapter 时,先用上面这条评估用例验一下隔离,再信任它。
</Warning>

## 人工介入(HITL)

有些 agent 会在一轮执行中间停下来,等审批或缺失信息,而不是直接跑完。这时这一轮以 `status: "waiting"` 结束,并带一条或多条 `input.requested` 事件说明在等什么。完整的心智模型(握手时序、Adapter 侧义务、rejected 语义)见 [HITL](/docs/zh/explanation/hitl),这里讲评估侧怎么用。

```ts theme={null}
const draft = await t.send("拟一封跟进邮件,但先别发,等我确认。");
draft.parked();                              // t.parked() 断言 status === "waiting"

const request = t.requireInputRequest({
  prompt: /是否发送/,
  optionIds: ["approve", "reject"],
});

await t.respond({ request, optionId: "approve" });
t.calledTool("send_email");
```

`t.requireInputRequest(filter)` 把一个待处理的 HITL 请求变成可检查、可回应的具体值——如果匹配到 0 个或超过 1 个待处理请求就会抛出,所以尽量把能填的 filter 字段都填上(`id` / `prompt` / `display` / `action` / `optionIds` / `input`)来消歧。`t.respond(...)` 回答它并发出下一轮;每个参数是一条回答:字符串按待处理请求的顺序对位,`{ request, optionId }` 对象形式显式指名(多个请求并停时用它)。在底层它只是又一次普通的 `send`:回答文本进 `input.text`,同时以结构化形式逐条进 `input.responses`——命中请求选项的回答带 `{ requestId, optionId }`,自由文本回答带 `{ requestId, text }`,adapter 不用解析文本,按 `requestId` 就能对上"哪个回答给哪个请求"。每种回答到 adapter 长什么样,见[不同回答的入参](/docs/zh/explanation/adapter#不同回答的入参)。

如果当前轮有多个同类待处理请求、都该给同一个答案(比如逐个批准一批文件改动),用 `t.respondAll(optionId)` 一次性处理,不用挨个解。`optionId` 会先对每条待处理请求校验——不在请求的 `options` 里就直接抛错,打错的字不会被静默当成别的答案发出去：

```ts theme={null}
await t.send("把这批改动逐项提交审批。");
t.requireInputRequest({ display: /审批/ });

await t.respondAll("approve");
t.succeeded();
```

## 相关阅读

* [HITL](/docs/zh/explanation/hitl) — 停轮等人的完整概念：握手时序与两侧义务。
* [Assert](/docs/zh/explanation/assert) — 从 `Turn.events` 和 `Turn.data` 上读的断言词汇。
* [Judge](/docs/zh/explanation/judge) — `t.judge` / `session.judge` / `turn.judge` 各自默认评什么材料。
* [Adapter](/docs/zh/explanation/adapter) — 能力从哪来:什么解锁 `t.newSession()`、HITL 和工具相关断言。
