- Claude Code:跑一条有风险的命令、改一个权限外的文件之前,会停下来问”允许吗?“——批准了才执行,拒绝了就换路。
- Codex:审批模式(suggest / auto-edit 等)决定哪些动作要先经人确认,本质是同一扇门。
- AI SDK 应用:工具不带
execute时,工具调用会浮到前端,由界面上的人确认后把结果交回(addToolResult),模型再继续——这是自研应用里最常见的审批门写法。
t.parked())、检查它在等什么(t.requireInputRequest())、替人回答(t.respond())。批准、拒绝、补充信息,从此都是能写进 eval 的路径。
停轮如何表达:waiting + input.requested
send 返回的 Turn.status 有三个值:"completed"(跑完了)、"failed"(出错了)、"waiting"——这一轮没跑完,agent 停在等人输入上。waiting 不是失败,是挂起:回答到达后同一轮接着跑,不重发请求、不开新对话。
停下的轮次还要同时说明”在等什么”:每个待回答的问题吐一条 input.requested 事件,带一个稳定的请求 id、停在哪个动作上(action)、可选的裁决项(options,如 approve / deny)。状态和事件缺一不可——waiting 让 eval 知道该回答了,input.requested 让回答有的放矢。
一次完整握手
t.respond 底层没有专用通道——它只是又一次普通的 send,人的裁决以结构化形式随输入到达:
- 回答轮不是新对话。 它发生在同一条会话线上,
ctx.session还是同一个——Adapter 靠它找回上一轮挂起的现场。 - 回答不用从文本里猜。
input.responses逐请求带着{ requestId, optionId }(自由文本回答则是{ requestId, text }),命中选项的optionId在 eval 侧已校验过存在,打错的字直接抛错而不是静默传给应用。每种回答到 Adapter 长什么样,见不同回答的入参。
eval 侧:三个动作
t.parked()断言这一轮确实以 waiting 停下;反过来t.succeeded()在 waiting 轮上会失败——停轮是显式状态,不会被当成”跑完了”。t.requireInputRequest(filter)从待处理请求里精确取一个(按id/prompt/action/optionIds等字段匹配),匹配到 0 个或超过 1 个都会抛,多个请求并停时靠它消歧。t.respond(...)回答并发出下一轮;同类请求要给同一个答案时(比如逐个批准一批改动),t.respondAll(optionId)一次处理完。
Adapter 侧:两条义务,一次续跑
HITL 对 Adapter 的全部要求就三件事——前两件发生在停下的那一轮,第三件发生在回答到达的下一轮:- 停轮时如实返回
status: "waiting",不要把挂起报成"completed",否则parked()失效、succeeded()假通过; - 每个待回答的问题吐一条
input.requested,id稳定、字段尽量填全——eval 侧的检查和对位全靠它们; - 下一次
send先交裁决、再续跑:从input.responses按requestId把裁决交回应用(不要按顺序猜),然后接着上一轮挂起的地方继续,而不是重发请求。
ctx.session 上——停轮时 ctx.session.hold(现场),回答轮 ctx.session.take() 取回,取到即清除。怎么和流式驱动拼起来,见写 send 第五步的完整骨架。
和 NiceEval 的其它能力一样,HITL 没有布尔声明:做到了就是有。send 返回过 "waiting" 并吐了 input.requested,t.respond 就能工作;没做到,eval 会在第一个 parked() 或 requireInputRequest() 上明确失败,而不是静默假通过。能力如何从构造中来,见 Adapter。
拒绝不是故障
人否决和工具报错是两回事,事件流里用不同的status 区分:被人拒绝的调用,action.result 的 status 是 "rejected" 而不是 "failed"。这样 t.noFailedActions() 不会被一次正当的人工否决误伤,而 t.calledTool("deploy", { status: "rejected" }) 可以精确断言”发起了调用、被人拦下了”——这正是拒绝分支的 eval 要写的断言。
哪些 agent 用不到 HITL
不是每个 agent 都有这条分支,没有就整个跳过——Adapter 不用管waiting,eval 里也不会出现 t.respond:
- 每轮一口气跑完的 agent:问答、检索、翻译这类单发即回的服务,执行中没有等人的环节。
- 审批门被关掉的运行形态:Claude Code、Codex 产品本身是 HITL 的,但作为被测对象在沙箱里通常全自动跑(跳过权限确认)——内置的
claude-code/codex/bubsandbox agent 就是这样,所以它们都不做 HITL。 - 审批发生在执行循环之外:比如工单系统里人工复核 agent 的产出。那是另一个系统的流程,不经过
send,eval 评不到也不该评。
相关阅读
- Drive — eval 侧的完整用法:
t.parked()、t.requireInputRequest()、t.respond()/t.respondAll()。 - Adapter — 回答到 Adapter 的结构化入参,四种典型形态。
- 写 send — Adapter 侧实操:
ctx.session.hold/take与流式 + HITL 的完整骨架。 - 接入你的 Agent — 接入全景:最小接入、参数通道与增量地图。