[Feature]: Expose ask_user_question in the DSH agent runtime (generalize the userQuestions bridge beyond plan-review)
This issue was translated automatically.
Environment
- Cherry Studio: 2.1.2 (Windows x64)
- Agent runtime mode: DeepSeek Harness (dsh), main model
deepseek-reasoner - DSH peer:
@deepseek-ai/dsh-*0.2.0-rc.1
Current behavior
In Cherry's dsh agent sessions, the model cannot see the ask_user_question tool, so it cannot pause and pop up a card to ask when user confirmation/selection/additional information is needed.
Confirmed phenomena:
- The tools actually exposed in the session are only
read/write/edit/read_image/pwsh/todo_write/goalseries /skill/subagentseries /exit_plan_mode/web_*/browser_*etc.; there is noask_user_questionat all. - In the packaged product
resources/app.asar.unpacked/node_modules/@cherrystudio/dsh-bridge/dist/runtime/, there is no bundle entry corresponding totool-ask-user(in contrast:user-questions/plan-modeare both present, e.g.,user-questions.mjs,plan-mode.mjs). If this plugin had been loaded, its code would be inlined into the bundle. cherry-bridgehas registered theuserQuestionsservice and forwardsquestion/askto the host—indicating this link is working:
n.on('user-questions/request', async e => {
return await h.request('question/ask', { sessionId: e.agent?.id ?? '', ...E(e), questions: e.questions }, e.signal)
})- However, the host-side
handleQuestionAskapplies a whitelist to questions, only accepting plan-review (original source code comment):
One
ctx.userQuestionsask. Only the plan-review intent is bridged: it lands on the existing approval surface as anexit_plan_modecard.
const review = ask.questions.length === 1 ? ask.questions[0] : void 0
const intent = review?.intent
if (!review || intent?.kind !== 'plan-review' || typeof review.detail !== 'string')
return Promise.reject(new Error('only plan-review questions are bridged to the host'))That is: even if ask_user_question is registered, general questions would be rejected on the spot.
Expected behavior
- In dsh sessions, the model can call
ask_user_question; Cherry rendersquestions[]as a question card (header/question/detail/options[]/multi_select), and after the user answers or fills in freely, the tool result returns the canonical answer and continues the round. - Questions block tool calls until the user answers (this is DSH's design, no extra token cost during waiting); when the user cancels the round, it ends with
ASK_ABORTED, not considered a tool failure.
Upstream contract (DSH 0.2.0-rc.1)
- Service Definition:
@deepseek-ai/dsh-user-questions— hasctx.userQuestionsanduser-questions/request(Agent-scoped Cordis waterfall). This service does not render UI;ask()dispatches to waterfall, returning the answer means claimed, if no one claims thenNO_PROVIDER(fail closed, no fallback). - Consumer:
@deepseek-ai/dsh-tool-ask-user— model-facing toolask_user_question; inexecute, passesexec.agent/exec.signaltoctx.userQuestions.ask(), it does not attach an answerer itself. - Answer format:
{ answers: [{ id, selected: string[], custom?: string }] }; for single-select,customoverridesselected, for multi-select,customsupplementsselected. intentonly affects presentation, does not change the protocol: UI that recognizesplan-reviewrenders as plan review, UI that doesn't recognize it renders as a generic option list, the fields read by the caller are exactly the same.
Sub-agent boundaries (do not relax during implementation)
ask() identity verification is based on runtime root ownership, not persistent session lineage:
| Case | Error code |
|---|---|
| The passed agent is not an exact live instance in the registry | CALLER_NOT_LIVE |
| Is a live instance but owned by another live agent (non-root) | DELEGATED_CALLER |
Owned sub-agents can never ask humans (otherwise they would block forever), and must pass pending questions back to the parent agent in the final result. Cherry is just a forwarder, do not pop cards on behalf of these two types of requests.
Suggested changes
- Register tool: Load
@deepseek-ai/dsh-tool-ask-userin dsh-bridge's tool composition (same level asplan-mode), so thatask_user_questionappears in the model's tool list. - Relax host whitelist: In
handleQuestionAsk, change the hard check forintent.kind === 'plan-review'to a branch—keep the existing plan-review branch (lands onexit_plan_modeapproval card, behavior unchanged), and other questions go through the generic question card channel. - Client rendering + backfill: Add/reuse a UI that can present multi-question forms (options + free text + multi-select), and return answers as
{ answers: [{ id, selected, custom? }] }; continue using the existingask.callIdassociation mechanism. - Permission mode coordination: Please clarify whether cards should still pop up under
Full Access; underAsk/Approvalmode, cards should pop up and wait normally.
Acceptance criteria
- In dsh sessions, the model can call
ask_user_question, UI pops up a question card, and the round continues after answering. - Multi-select returns multiple
selected; for single-select + free text,customoverridesselected. - User cancels the round → tool ends with
ASK_ABORTED, does not pollute the session. -
plan-reviewscenario (exit_plan_mode) behavior is exactly the same as current, no regression. - Owned sub-agent calls → still
DELEGATED_CALLER(no card popped on its behalf).
Non-goals
- No requirement to support richer interaction forms (file picker, diff preview confirmation)—upstream seam currently only has "question form" vocabulary.
- No requirement to make the above sub-agent identity boundary configurable.
References
- DSH source code:
packages/interaction/user-questions/,packages/interaction/tool-ask-user/,packages/plan/plan-mode/ - Tool catalog:
deepseek-harness.github.io/deepseek-harness/reference/tool-catalog - Plugin pages:
dsh.pub/zh/plugins/tool-ask-user/,dsh.pub/zh/plugins/user-questions/ - Service seam explanation:
deepseekdocs.com/docs/learn/core/user-question-seam
Note: The standalone DSH's ~/.dsh/profiles/web/cordis.yml has tool-ask-user loaded by default, and its Web UI's answerer can pop up cards normally—this indicates it's a Cherry-side tool composition + host forwarding strategy issue, not a lack of capability upstream.
Original Content
环境 Environment
- Cherry Studio: 2.1.2(Windows x64)
- Agent 运行模式: DeepSeek Harness (dsh),主模型
deepseek-reasoner - DSH peer:
@deepseek-ai/dsh-*0.2.0-rc.1
现状 Current behavior
Cherry 的 dsh agent 会话里,模型看不到 ask_user_question 工具,因此无法在需要用户确认 / 选择 / 补充信息时暂停并弹卡询问。
已确认的现象:
- 会话实际暴露的工具只有
read/write/edit/read_image/pwsh/todo_write/goal系列 /skill/subagent系列 /exit_plan_mode/web_*/browser_*等;没有任何ask_user_question。 - 打包产物
resources/app.asar.unpacked/node_modules/@cherrystudio/dsh-bridge/dist/runtime/里不存在tool-ask-user对应的 bundle 条目(对比:user-questions/plan-mode都在,例如user-questions.mjs、plan-mode.mjs)。若该插件被加载过,其代码会被内联进 bundle。 cherry-bridge已注册userQuestions服务,并向宿主转发question/ask——说明这条链路是通的:
n.on('user-questions/request', async e => {
return await h.request('question/ask', { sessionId: e.agent?.id ?? '', ...E(e), questions: e.questions }, e.signal)
})- 但宿主侧的
handleQuestionAsk对问题做了白名单,只接受 plan-review(源码注释原文):
One
ctx.userQuestionsask. Only the plan-review intent is bridged: it lands on the existing approval surface as anexit_plan_modecard.
const review = ask.questions.length === 1 ? ask.questions[0] : void 0
const intent = review?.intent
if (!review || intent?.kind !== 'plan-review' || typeof review.detail !== 'string')
return Promise.reject(new Error('only plan-review questions are bridged to the host'))即:即使 ask_user_question 被注册,通用问题也会当场被 reject。
期望 Expected behavior
- dsh 会话中模型可调用
ask_user_question;Cherry 以问题卡渲染questions[](header/question/detail/options[]/multi_select),用户作答或自由填写后,工具结果返回规范答案并继续该轮。 - 问题阻塞工具调用直至用户作答(这是 DSH 的设计,等待期间不产生额外 token 开销);用户取消该轮时以
ASK_ABORTED结束,不视为工具失败。
上游契约 Upstream contract(DSH 0.2.0-rc.1)
- Service Definition:
@deepseek-ai/dsh-user-questions— 拥有ctx.userQuestions与user-questions/request(Agent 作用域 Cordis waterfall)。该服务不渲染 UI;ask()分发 waterfall,返回答案即认领,无人认领则NO_PROVIDER(fail closed,不降级)。 - Consumer:
@deepseek-ai/dsh-tool-ask-user— 模型面工具ask_user_question;execute里把exec.agent/exec.signal传给ctx.userQuestions.ask(),它自己不挂 answerer。 - 答案形态:
{ answers: [{ id, selected: string[], custom?: string }] };单选题custom覆盖selected,多选题custom补充selected。 intent只影响呈现,不改变协议:认识plan-review的 UI 按计划审阅渲染,不认识的 UI 渲染通用选项列表,调用方读到的字段完全一致。
子代理边界(实现时请勿放宽)
ask() 的身份校验基于运行时根归属,不是持久会话血缘:
| 情形 | 错误码 |
|---|---|
| 传入的 agent 不是 registry 里确切 live 实例 | CALLER_NOT_LIVE |
| 是 live 实例但被另一个 live agent 拥有(非根) | DELEGATED_CALLER |
被拥有的子代理永远问不到人(否则会永久阻塞),必须在最终结果里把未决问题回传父代理。Cherry 侧只是转发者,不要把这两类请求代为弹卡。
建议改动 Suggested changes
- 注册工具:在 dsh-bridge 的工具组合里加载
@deepseek-ai/dsh-tool-ask-user(与plan-mode同一层),使ask_user_question出现在模型工具表。 - 放开宿主白名单:
handleQuestionAsk中把intent.kind === 'plan-review'的硬校验改为分支——保留现有 plan-review 分支(落到exit_plan_mode审批卡,行为不变),其它问题走通用问题卡通道。 - 客户端渲染 + 回填:新增/复用一个能呈现多问题表单(选项 + 自由文本 + 多选)的 UI,并按
{ answers: [{ id, selected, custom? }] }回传;沿用现有ask.callId关联机制。 - 权限模式协同:请明确
完全访问下是否仍弹卡;ask/审批模式下应正常弹卡等待。
验收标准 Acceptance criteria
- dsh 会话里模型可调用
ask_user_question,UI 弹出问题卡,作答后该轮继续。 - 多选题返回多个
selected;单选 + 自由文本时custom覆盖selected。 - 用户取消该轮 → 工具以
ASK_ABORTED结束,不污染会话。 -
plan-review场景(exit_plan_mode)行为与现状完全一致,无回归。 - 被拥有的子代理调用 → 仍是
DELEGATED_CALLER(不代为弹卡)。
非目标 Non-goals
- 不要求支持更丰富的交互形态(文件选择器、diff 预览确认)——上游 seam 目前只有"问题表单"词汇。
- 不要求把上述子代理身份边界改为可配置。
参考 References
- DSH 源码:
packages/interaction/user-questions/、packages/interaction/tool-ask-user/、packages/plan/plan-mode/ - 工具目录:
deepseek-harness.github.io/deepseek-harness/reference/tool-catalog - 插件页:
dsh.pub/zh/plugins/tool-ask-user/、dsh.pub/zh/plugins/user-questions/ - 服务缝讲解:
deepseekdocs.com/docs/learn/core/user-question-seam
补充:独立版 DSH 的 ~/.dsh/profiles/web/cordis.yml 默认已加载 tool-ask-user,其 Web UI 的 answerer 可正常弹卡——这说明是 Cherry 侧的工具组合 + 宿主转发策略问题,不是上游缺能力。
Source: CherryHQ/cherry-studio