Sessions

用户异步交互

在 Session 内处理审批、问题与等待用户响应的执行

用户异步交互

Interaction 表示当前 Turn 的执行需要用户参与。第一阶段支持 approvalquestion;它们使用同一套生命周期、查询和响应 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 expired / cancelled + Tool failed

订阅并响应

const unsubscribe = session.subscribe((mutation) => {
  if (
    mutation.variant !== "part" ||
    mutation.type !== "interaction" ||
    mutation.part.status !== "pending"
  ) return;

  render_interaction(mutation.part);
});

await session.respond({
  interaction_id,
  response: {
    kind: "approval",
    decision: "approved",
  },
});

subscribe() 只报告已经持久化的状态。respond() 先原子提交 Interaction 与关联 Tool, 成功后才恢复原执行。请求不存在、已经终结或响应类型不匹配时会抛出明确错误。

查询 pending Interaction

const pending = await session.interactions();

for (const { request } of pending) {
  if (request.kind === "approval") {
    render_approval(request);
  } else {
    render_questions(request.questions);
  }
}

Question 支持 textsingle_selectmulti_select。回答必须覆盖请求中的全部 question_id

await session.respond({
  interaction_id,
  response: {
    kind: "question",
    answers: [
      { question_id: "region", value: "cn" },
      { question_id: "features", value: ["search", "files"] },
    ],
  },
});

模型主动提问

ask_question 是可选 Tool,不会由 Agent 默认注册。需要该能力时由调用方显式传入:

import { Agent, Workspace } from "@downcity/agent";
import { AskQuestionsTool } from "@downcity/agent/tools";

const agent = new Agent({
  id: "repo-helper",
  workspace: new Workspace({ path: "/path/to/project" }),
  tools: {
    ask_question: AskQuestionsTool,
  },
});

模型缺少会实质影响结果的信息时,返回标准 Tool Call:

{
  "title": "选择部署区域",
  "questions": [
    {
      "question": "需要部署到哪个区域?",
      "type": "single_select",
      "options": [
        { "value": "cn", "label": "中国" },
        { "value": "us", "label": "美国" }
      ]
    }
  ]
}

questions 中每个问题都必须显式提供 type,可选值为 textsingle_selectmulti_selectquestion_id 由 Session 自动生成;选择题必须提供至少一个同时包含 valuelabeloptions,文本题不需要 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 审批

需要在执行前获得用户许可的 Tool,直接使用 AI SDK 原生 needsApproval 声明:

import { tool } from "ai";
import { z } from "zod";

const delete_file = tool({
  description: "删除工作区内的指定文件。",
  inputSchema: z.object({
    path: z.string(),
  }),
  needsApproval: true,
  execute: async ({ path }) => {
    await remove_workspace_file(path);
    return { deleted: path };
  },
});

const agent = new Agent({
  id: "repo-helper",
  workspace,
  model,
  tools: { delete_file },
});

审批策略属于 Tool,不属于 Agent。模型产生 Tool Call 后,AI SDK 先完成 schema 校验; Session 再创建 operation: "tool" 的 Approval Interaction,并等待 session.respond(...)。 批准后执行原 Tool Call,拒绝时 execute 不会运行。

普通 Tool 审批请求向 UI 提供真实调用信息:

  • source.tool_namesource.tool_call_id:实际 Tool 及调用标识。
  • validated_input:schema 校验通过后的结构化输入。
  • tool_description:Tool 定义中的稳定能力说明,可选。
  • model_explanation:模型在当前调用中生成的解释,可选,不能作为安全判断依据。

调用方不需要配置审批 titlereason。UI 应根据 Tool、已校验输入和可选模型解释 组织展示内容,不能用客户端生成的摘要替代真实输入。

Shell 审批模式

const current = await session.status();

await session.set({
  security: {
    approval_mode: "always-allow",
  },
});
模式行为
askunrestricted 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.type === "tool" 时,用 source.tool_call_id 关联 Tool Part。
  • Tool 为 waiting-user 时保持等待态,不要在客户端伪造 running 或成功。
  • resolvedexpiredcancelled 都是终态,不能重复响应。
  • Session 切换时清理旧交互 UI,不能把旧 Session 的 interaction_id 发给新 Session。

完整 Part 状态见 Message 与 Part