用户异步交互
在 Session 内处理审批、问题与等待用户响应的执行
Interaction 表示当前 Turn 的执行需要用户参与。第一阶段支持 approval 与
question;它们使用同一套生命周期、查询和响应 API。
Interaction 是 Assistant Message 的 Part,不是新的顶层 Message,也不会创建新 Turn。 当 Interaction 来自 Tool 时,Tool 与 Interaction 在同一个 Assistant revision 中原子更新:
创建:Tool ready -> waiting-user + Interaction pending
响应:Interaction resolved + Tool running / failed
结束:Interaction cancelled + Tool failed订阅并响应
const unsubscribe = session.subscribe((mutation) => {
if (mutation.variant !== "part" || mutation.part.type !== "tool") return;
const interaction = (mutation.part.interactions ?? []).find(
(item) => item.status === "pending",
);
if (!interaction) return;
render_interaction(interaction);
});
await session.respond({
interaction_id,
response: {
type: "approval",
outcome: "resolved",
payload: { decision: "approved" },
},
});subscribe() 只报告已经持久化的状态。respond() 先原子提交 Interaction 与关联 Tool,
成功后才恢复原执行。请求不存在、已经终结或响应类型不匹配时会抛出明确错误。
查询 pending Interaction
const pending = await session.interactions();
for (const request of pending) {
if (request.type === "approval") {
render_approval(request);
} else {
render_questions(request.payload.questions);
}
}approval 请求同时携带 response_schema,声明响应必须满足的结构;Shell 审批与 Tool
审批共用同一份默认 schema,可直接从 SDK 导入 SESSION_APPROVAL_RESPONSE_SCHEMA
(要求 decision 为 approved 或 denied)。
Question 支持 text、single_select 与 multi_select。回答必须覆盖请求中的全部
question_id:
await session.respond({
interaction_id,
response: {
type: "question",
outcome: "resolved",
payload: { answers: [
{ question_id: "region", value: "cn" },
{ question_id: "features", value: ["search", "files"] },
] },
},
});模型主动提问
ask_question 是可选 Tool,不会由 Agent 默认注册。需要该能力时由调用方显式传入:
import { Agent } from "@downcity/agent";
import { Workspace } from "@downcity/city";
import { AskQuestionsTool } from "@downcity/agent/tools";
const agent = new Agent({
id: "repo-helper",
tools: {
ask_question: AskQuestionsTool,
},
});
const workspace = new Workspace({
id: "project",
path: "/path/to/project",
});模型缺少会实质影响结果的信息时,返回标准 Tool Call:
{
"title": "选择部署区域",
"questions": [
{
"question": "需要部署到哪个区域?",
"type": "single_select",
"options": [
{ "value": "cn", "label": "中国" },
{ "value": "us", "label": "美国" }
]
}
]
}questions 中每个问题都必须显式提供 type,可选值为 text、single_select 和
multi_select。question_id 由 Session 自动生成;选择题必须提供至少一个同时包含
value 和 label 的 options,文本题不需要 options。
Session 会把该 Tool 与 Question Interaction 原子提交为 waiting-user / pending。调用方
使用上面的 session.respond(...) 提交完整回答后,Tool Result 返回:
{
"status": "resolved",
"answers": [{ "question_id": "question:generated-id", "value": "cn" }]
}模型随后在同一个 Turn 的下一 Step 继续执行。普通 assistant 文本中的问句不会进入该 生命周期:它会正常结束当前 Turn,用户的下一条消息会创建新 Turn。
普通 Tool 审批
需要用户许可的 AgentTool,在自己的执行里向 Session 的审批端口请求审批:
import { define_agent_tool } from "@downcity/type";
import { z } from "zod";
const delete_file = define_agent_tool({
description: "删除工作区内的指定文件。",
input_schema: z.object({
path: z.string(),
}),
execute: async ({ path }, context) => {
// 审批端口由 Agent 在执行时注入;ask 模式等待用户,always-allow 直接放行。
const decision = await context.interactions.approval.request({
turn_id: context.turn_id,
tool_call_id: context.tool_call_id,
tool_name: "delete_file",
payload: { operation: "tool", validated_input: { path } },
});
if (!decision.approved) {
return { success: false, error: "denied" };
}
await remove_workspace_file(path);
return { deleted: path };
},
});
const agent = new Agent({
id: "repo-helper",
model,
tools: { delete_file },
});
// Session 在创建时选择 Workspace。审批策略属于 Tool,不属于 Agent。模型产生 Tool Call 后,Agent 先完成 schema 校验,
随后进入 Tool 自己的 execute;Tool 在这里通过审批入口创建 operation: "tool" 的
Approval Interaction,并等待 session.respond(...)。批准后继续执行副作用,拒绝时不再执行。
内核不再有工具级审批钩子:工具自己决定「哪一步需要有人拍板」,Session 决定「这个拍板
怎么回答」。审批模式(ask / always-allow)在审批入口内生效,Tool 不自己读模式;
always-allow 时不创建 Interaction,直接返回放行。
interactions.request() 是通用原语,interactions.approval 是审批专用入口。发起任意交互
只需提供语义字段,interaction_id / created_at / source 由原语补齐:
const handle = await session.interactions.request({
type: "question",
turn_id,
tool_call_id,
payload: { questions },
});
const result = await handle.result; // 生产者自己解释 payload审批请求向 UI 提供真实调用信息:
source.tool_name与source.tool_call_id:实际 Tool 及调用标识。payload.validated_input:schema 校验通过后的结构化输入。payload.tool_description:Tool 定义中的稳定能力说明,可选。payload.model_explanation:模型在当前调用中生成的解释,可选,不能作为安全判断依据。
调用方不需要配置审批 title 或 reason。UI 应根据 Tool、已校验输入和可选模型解释
组织展示内容,不能用客户端生成的摘要替代真实输入。
Shell 审批模式
const current = await session.status();
await session.set({
security: {
approval_mode: "always-allow",
},
});| 模式 | 行为 |
|---|---|
ask | host Shell 请求创建 approval Interaction。 |
always-allow | 当前 Session 后续 Shell 请求自动批准。 |
审批模式是 Shell Adapter 的执行策略,不是通用 Interaction 状态。它不改变 sandbox, 不影响其他 Session,也不会处理已经 pending 的 Interaction。
set() 会接受 configured 值,并把一个 SessionCommand 加入 Session 有序输入队列。
当前 Provider 请求与 Tool callback 继续使用已经捕获的策略,新模式在下一个 Session Step
检查点生效。如果当前 Turn 没有下一个 Step,该命令会在下一条 prompt 开始前提交。
通过 status() 读取状态:security.approval_mode 是 configured 值,
security.effective_approval_mode 是执行面当前值;两者不同时表示修改仍在排队。Command
成功提交后,Session 会持久化一条 Session configuration updated completed Action。
初始化或恢复审批模式时,可以通过 set() 的第二个参数关闭 Action 和 Mutation;配置仍会正常写入并在检查点生效:
await session.set(
{ security: { approval_mode: "always-allow" } },
{ persist_action: false, publish_mutation: false },
);相同审批模式的幂等设置不会生成新的配置 Action。审批模式会随 Session metadata 恢复。
UI 实现要点
- 使用
interaction_id作为交互卡片稳定 key。 - 任何带有
source.tool_call_id的来源,都用该 Tool Call ID 关联 Tool Part。 - Tool 为
waiting-user时保持等待态,不要在客户端伪造running或成功。 resolved、denied、cancelled、failed都是终态,不能重复响应。Interaction 没有超时:它只能由用户响应,或随 Turn/Session 结束而被取消。- Session 切换时清理旧交互 UI,不能把旧 Session 的
interaction_id发给新 Session。
完整 Part 状态见 Message 与 Part。