> ## 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 里所有断言和 judge 都从 Turn 的数据上读，具体看 [Assert](/zh/concepts/assert) 和 [Judge](/zh/concepts/judge)。

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

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

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

turn.message;   // 最后一条 assistant 消息
turn.data;      // 结构化输出(如果 agent 返回了)
turn.status;    // "completed" | "failed" | "waiting"
turn.events;    // 这一轮的 StreamEvent[]
turn.usage;     // { inputTokens, outputTokens, ... },adapter 报了才有
turn.expectOk(); // status === "failed" 时抛出;否则原样返回 turn
```

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

<Tip>
  `turn.expectOk()` 是前置条件检查,不是评分断言:它立即抛出(中止 `test(t)` 剩余部分),而不是记一条失败断言。后面几轮依赖前一轮成功时用它——在一个已经失败的轮次上继续挂断言,通常只会产出一堆混乱的连带失败。
</Tip>

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

`t.sendFile(path, text?)` 读一个本地文件(相对 eval 所在目录),按扩展名推断 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.expectOk();
t.check(draft.message, includes("此致"));
draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);

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

多轮 `t.send()` 依赖 agent 声明 `conversation` 能力——没声明时,adapter 没有义务记住上一轮说了什么,第二次 `t.send()` 可能接进一个全新、无关的上下文。完整能力列表见 [Agents & Adapters](/zh/concepts/agents-adapters)。

## 独立会话 —— `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 是否读了 `ctx.session.isNew` 并照做——一个忽略 `isNew`、永远续接同一个底层上下文的 adapter,会让 `t.newSession()` 悄悄共享状态,且不会报错。写 adapter 时,先用上面这条 eval 验一下隔离,再信任它。
</Warning>

## 人工介入(HITL)

有些 agent 会在一轮执行中间停下来,等审批或缺失信息,而不是直接跑完。这时这一轮以 `status: "waiting"` 结束,并带一条或多条 `input.requested` 事件说明在等什么。

```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(...)` 回答它并发出下一轮;在底层它只是又一次带着你的回答的普通 `send`。

如果当前轮有多个同类待处理请求、都该给同一个答案(比如逐个批准一批文件改动),用 `t.respondAll(optionId)` 一次性处理,不用挨个解：

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

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

## 相关阅读

* [Assert](/zh/concepts/assert) — 从 `Turn.events` 和 `Turn.data` 上读的断言词汇。
* [Judge](/zh/concepts/judge) — `t.judge` / `session.judge` / `turn.judge` 各自默认评什么材料。
* [Agents & Adapters](/zh/concepts/agents-adapters) — 哪些能力位解锁 `t.newSession()`、HITL 和工具相关断言。
