AskUserQuestion 工具
AskUserQuestion 工具1. 定位AskUserQuestion 让模型在被真正属于用户的决策卡住时发起多选题澄清并带着答案继续工作。工具描述开篇就划定了使用门槛Use this tool only when you are blocked on a decision that is genuinely the user’s to make: one you cannot resolve from the request, the code, or sensible defaults.反过来说有惯例默认值的选择、代码库里能自己查证的事实都不该问——“pick the obvious option, mention it in your response, and proceed”。提问资格的判据是用户的答案会不会改变你接下来做什么。它和 ExitPlanMode 有一条明确的边界描述里专门写了 Plan mode note切规划模式用 EnterPlanMode规划模式里用它在定稿前澄清需求或方案取舍严禁问 “Is my plan ready?” / “Should I proceed?”——用户在 ExitPlanMode 之前根本看不到计划拿计划当问题前提是无效提问。2. 基本信息一个不寻常的元数据组合readOnly:true,// 不触碰外部世界concurrentSafe:true,// 可以与其他调用并行发起sideEffectScope:userInteraction,// 独有的副作用类别needsApproval:true,// 但必须走权限审批requiresUserInteraction:true,// 且必须人来应答readOnly: true与needsApproval: true并存看似矛盾实际是两个维度不产生外部副作用所以 riskLevel 仍是 low但必须暂停等人。sideEffectScope: userInteraction是 40 个工具里唯一的取值——副作用就是把执行停下来要答案这件事本身。权限 key 为userInteraction.askQuestion审批理由写明 “pauses execution to collect answers from the user”。3. 核心设计两阶段执行模型AskUserQuestion 最与众不同的地方是模型发出的调用里没有答案handler 执行时输入里必须有答案。两者之间隔着一次完整的 UI 交互答案以修改后的输入形式回填。UI / 用户interaction-broker协议层执行器 / 权限流模型UI / 用户interaction-broker协议层执行器 / 权限流模型用户选择选项 / 跳过 / 加备注 / Other 自填AskUserQuestion(questions)无 answerscheckPermission → askneedsApproval: truerequestPermission按 toolName 分派interactionRequestUserInput→ elicitation_request 事件resolveInteractionanswers annotationsdecision: modifymodifiedInput { ...input, answers, annotations }handler 校验 AnsweredInputSchematool_result答案回显 后续指令阶段一权限门即提问门。needsApproval: true使权限服务返回 “ask”代理请求进入PermissionBrokerPort。协议层 broker 按工具名分派interaction-broker.ts:48-54AskUserQuestion 走requestUserInput多题交互ExitPlanMode 走 plan-approval其余工具走普通权限弹窗。宿主适配层zcodeTaskServiceAdapter.ts:3998-4003还有一处刻意的抑制注释说得很清楚AskUserQuestion/ExitPlanMode 的 permission.requested 只是 core 的等待态标记真正需要展示的问题会通过 interaction/requestUserInput 到达。继续把它投成普通权限UI 会出现 Allow/Deny 弹窗且无法把答案写回工具 input。即普通权限弹窗只有 Allow/Deny 两个出口承载不了答案回填所以问题必须走专门的 elicitation 事件带完整 questions 数组到 UI。阶段二答案回填后 handler 才可能执行。handler 里只认AskUserQuestionAnsweredInputSchemaif(input.answersundefined){context.addIssue({…,message:Answers have not been collected yet,path:[answers]});}schema 注释解释了分寸“问题用于可选澄清用户可以完整、部分或零回答只有 answers 字段完全缺失才表示 permission 阶段尚未完成必须阻止 handler 提前执行。”若 answers 缺失就执行意味着没人回答过handler 抛可恢复错误 “AskUserQuestion requires user answers before execution”。答案本身允许部分缺失——跳过是合法应答见第 5 节但空白字符串答案被拒绝。回填的实现是 broker 返回decision: modifyinteraction-broker.ts的userInputResponseToBrokerResultreturn{decision:modify,modifiedInput:{...input,...content},// content 只含归一后的 answers/annotationsresolvedAt:newDate(),};“modify” 是权限流的标准出口之一执行器用modifiedInput替换原输入后调用 handler——这就是两阶段模型的合流点。4. 输入契约选项纪律与 preview 净化契约contracts/src/tools/ask-user-question.ts对什么是有效提问有大量 schema 级约束问题1–4 个question文本必须唯一superRefine 拦重header是 ≤12 字符的短标签UI 渲染成 chipASK_USER_QUESTION_TOOL_CHIP_WIDTH 12。选项每题 2–4 个label 在同题内必须唯一禁止自带 “Other” 选项——superRefine 直接拒绝 “Do not include an Other option; clients provide it automatically”兜底自填由客户端统一提供工具描述同步告知模型 “Users will always be able to select Other”每个选项必须带description选项的含义/后果说明推荐项的约定在描述层放第一位并在 label 尾部加 “(Recommended)”。multiSelect 的双层口径Zod 里default(false)但导出给 provider 的 JSON Schema 通过withRequiredDefaultedMultiSelect把multiSelect塞进required——注释解释provider-visible schema 要求显式模型必须想清楚单选还是多选运行时 Zod default 继续兼容省略该字段的旧调用。preview 的 HTML 净化preview 允许是 HTML 片段用于 UI mockup、代码片段等视觉对比schema 用四个正则做净化validateAskUserQuestionPreview含 HTML 标记时禁止!doctype/html/body必须是 fragment、禁止script/style、且必须真的含 HTML 标签防止纯文本误标。描述层约定多行 markdown 支持等宽盒渲染任一选项带 preview 时 UI 切换为左列表右预览的并排布局previews 仅单选支持。annotations按问题文本键控的{ preview, notes }——用户选中带 preview 的选项时预览内容随之回传自由备注notes也一并承载。metadata.source可选的分析追踪标识如/remember命令发起的提问不展示给用户。5. 答案的三档语义与归一formatModelContentask-user-question.ts:131-158把回填结果翻译成三种模型可见文案语义逐档放宽应答情况模型看到的指令零回答answers 为空对象“Continue using your best judgment;do not treat this as a rejection or invent a user preference.”部分回答“answered some questions and skipped N… use your best judgment for the unanswered questions; do not invent user preferences.”全部回答“User has answered your questions: …”两处防误读设计值得注意跳过不等于拒绝、沉默不等于偏好——禁止模型把没回答脑补成用户立场。回答的展示格式把 annotations 一并带出选中 preview 时附 “selected preview:”有备注附 “user notes:”。broker 的归一层normalizeAskUserQuestionResponseContent/normalizeAskUserQuestionAnswers处理两类边界旧客户端兼容旧 UI 会同时提交answer_0/answer等兼容字段AskUserQuestionInputSchema是.strict()直接合并会炸 schema 校验——归一层只保留 schema 明确允许的answers/annotations按问题文本键归位多题按answer_index兜底单题按answer兜底。空对象与缺失的区分schema 注释原文“actionaccept content.answers{} 是 runtime 自动继续的显式成功语义必须保留空对象和 content 完全缺失旧客户端批准但未提供答案区分。” 空对象 用户看到问题后主动全部跳过 → 走零回答文案完全缺失 旧客户端路径 → 不注入 answers 字段。拒绝/取消的出口也在这里action ! accept→ broker 返回deny“AskUserQuestion was declined / cancelled”与普通权限拒绝同路。6. 无人值守自动裁决auto-resolution提问会阻塞执行无人值守场景闲时任务、手机远控离线需要自动收口。这套机制由三件东西支撑用户开关App 全局偏好askUserQuestionAutoResolutionEnabled由服务层syncAppRuntimePreferences同步到所有活动 workspacezcodeAgentService.ts不开就不自动裁决v4 交互注册表broker 注册交互时携带initialAutoResolution状态变化经onAutoResolutionUpdated回调durable 事件每次裁决状态变更经 runtime 落成UserInputAutoResolutionUpdated事件interaction-auto-resolution.ts。注释点明了两条恢复链路的一致性要求“事件必须走 runtime 的 durable append/sink 链路让desktop continuous 与 web remote replayable都恢复同一组绝对时间”——桌面实时会话和手机远控重放看到同一份裁决历史。7. 子代理提问的上抛与归属子代理subagent发起的 AskUserQuestion 需要父会话的 UI来应答。包装 brokersubagent-interaction-broker.ts做的是路由改写returnparentBroker.requestPermission({...request,sessionId:context.parentSessionId,// 路由到父会话origin:request.origin??buildSubagentInteractionOrigin(context,request.turnId),},options);注释说明了双层语义的分工sessionId由最外层离客户端最近的一层最后改写——任意深度的嵌套子代理提问最终都落到根会话的 UIorigin保留最内层已有值——归属永远是真正发起提问的那个子代理不会被外层覆盖成中间层。UI 与日志据此识别这是哪个孩子的提问。这个包装可叠加逐层收敛。8. 断线恢复按业务 id 重发悬置交互提问悬置期间 host 与 UI 的连接可能断开重连。withInteractionRequestRecoveryinteraction-broker.ts为此设置了 1 秒重发间隔INTERACTION_REQUEST_REANNOUNCE_INTERVAL_MS 1_000桌面/恢复链路里 UI 可能只从 snapshot 恢复出 pending 交互但 host 里原 protocol id 对应的内存登记已丢失。等待用户响应期间按同一业务 requestId重发现有协议请求让 host 重新登记可响应的 protocolRequestId。即重发不是重问而是让 UI 恢复出的 pending 交互重新找到应答通道。v4 竞速命中应答时内部 signal 会取消悬空的反向 RPC。9. UI 渲染提问卡renderers/ask-question.tsx渲染问题、选项、并排 preview 布局任一选项带 preview 时左列表右预览历史轮次里展示答案摘要。防御性归一lib/askUserQuestion.tsUI 助手兼容多种输入形状——旧单题管线questionoptions平铺与多题数组共用展示管线自定义输入选项的识别按中英文标签模式其他|其它|自定义|填写|other|custom|free text|specify…判定末位选项若被识别为自填项会被剥离成customInput输入框——契约禁止模型自带 Other但历史数据里可能存在UI 端兜底转成统一的自填体验。10. 相关文件索引文件职责packages/core/src/tool/handlers/ask-user-question.tshandlerAnsweredInputSchema 门禁、ToolEntry、三档答案文案packages/contracts/src/tools/ask-user-question.ts问题/选项契约、Other 禁令、preview HTML 净化、multiSelect 双层口径apps/zcode-cli/packages/bootstrap/src/zcode-protocol/interaction-broker.ts按工具名分派、答案归一、modify 回填、1s 重发恢复packages/services/src/zcode-agent/zcodeTaskServiceAdapter.ts抑制普通权限投影、pending → elicitation 事件、resolveInteraction 回执packages/core/src/runtime/helpers/subagent-interaction-broker.ts子代理提问路由到父会话sessionId 外层改写、origin 内层保留packages/core/src/runtime/methods/interaction-auto-resolution.ts自动裁决状态的 durable 事件落盘packages/services/src/zcode-agent/zcodeAgentService.tsaskUserQuestionAutoResolutionEnabled偏好同步packages/ui/src/lib/askUserQuestion.tsUI 防御性归一多形状兼容、自定义输入识别packages/ui/src/ToolCallBlocks/renderers/ask-question.tsx提问卡渲染并排 preview、答案摘要