[Feature]: Expose ask_user_question in the DSH agent runtime (generalize the userQuestions bridge beyond plan-review)

Author: meetyou11Created Sep 29, 2026Updated Sep 29, 2026

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:

  1. The tools actually exposed in the session are only read / write / edit / read_image / pwsh / todo_write / goal series / skill / subagent series / exit_plan_mode / web_* / browser_* etc.; there is no ask_user_question at all.
  2. In the packaged product resources/app.asar.unpacked/node_modules/@cherrystudio/dsh-bridge/dist/runtime/, there is no bundle entry corresponding to tool-ask-user (in contrast: user-questions / plan-mode are both present, e.g., user-questions.mjs, plan-mode.mjs). If this plugin had been loaded, its code would be inlined into the bundle.
  3. cherry-bridge has registered the userQuestions service and forwards question/ask to the host—indicating this link is working:
javascript
n.on('user-questions/request', async e => {
  return await h.request('question/ask', { sessionId: e.agent?.id ?? '', ...E(e), questions: e.questions }, e.signal)
})
  1. However, the host-side handleQuestionAsk applies a whitelist to questions, only accepting plan-review (original source code comment):

One ctx.userQuestions ask. Only the plan-review intent is bridged: it lands on the existing approval surface as an exit_plan_mode card.

typescript
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 renders questions[] 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 — has ctx.userQuestions and user-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 then NO_PROVIDER (fail closed, no fallback).
  • Consumer: @deepseek-ai/dsh-tool-ask-user — model-facing tool ask_user_question; in execute, passes exec.agent / exec.signal to ctx.userQuestions.ask(), it does not attach an answerer itself.
  • Answer format: { answers: [{ id, selected: string[], custom?: string }] }; for single-select, custom overrides selected, for multi-select, custom supplements selected.
  • intent only affects presentation, does not change the protocol: UI that recognizes plan-review renders 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

  1. Register tool: Load @deepseek-ai/dsh-tool-ask-user in dsh-bridge's tool composition (same level as plan-mode), so that ask_user_question appears in the model's tool list.
  2. Relax host whitelist: In handleQuestionAsk, change the hard check for intent.kind === 'plan-review' to a branch—keep the existing plan-review branch (lands on exit_plan_mode approval card, behavior unchanged), and other questions go through the generic question card channel.
  3. 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 existing ask.callId association mechanism.
  4. Permission mode coordination: Please clarify whether cards should still pop up under Full Access; under Ask/Approval mode, 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, custom overrides selected.
  • User cancels the round → tool ends with ASK_ABORTED, does not pollute the session.
  • plan-review scenario (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 工具,因此无法在需要用户确认 / 选择 / 补充信息时暂停并弹卡询问。

已确认的现象:

  1. 会话实际暴露的工具只有 read / write / edit / read_image / pwsh / todo_write / goal 系列 / skill / subagent 系列 / exit_plan_mode / web_* / browser_* 等;没有任何 ask_user_question。
  2. 打包产物 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。
  3. cherry-bridge 已注册 userQuestions 服务,并向宿主转发 question/ask——说明这条链路是通的:
javascript
n.on('user-questions/request', async e => {
  return await h.request('question/ask', { sessionId: e.agent?.id ?? '', ...E(e), questions: e.questions }, e.signal)
})
  1. 但宿主侧的 handleQuestionAsk 对问题做了白名单,只接受 plan-review(源码注释原文):

One ctx.userQuestions ask. Only the plan-review intent is bridged: it lands on the existing approval surface as an exit_plan_mode card.

typescript
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

  1. 注册工具:在 dsh-bridge 的工具组合里加载 @deepseek-ai/dsh-tool-ask-user(与 plan-mode 同一层),使 ask_user_question 出现在模型工具表。
  2. 放开宿主白名单:handleQuestionAsk 中把 intent.kind === 'plan-review' 的硬校验改为分支——保留现有 plan-review 分支(落到 exit_plan_mode 审批卡,行为不变),其它问题走通用问题卡通道。
  3. 客户端渲染 + 回填:新增/复用一个能呈现多问题表单(选项 + 自由文本 + 多选)的 UI,并按 { answers: [{ id, selected, custom? }] } 回传;沿用现有 ask.callId 关联机制。
  4. 权限模式协同:请明确 完全访问 下是否仍弹卡;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 侧的工具组合 + 宿主转发策略问题,不是上游缺能力。