English | 中文
Abstract user-interaction seam. It owns ctx.userInteraction, the service a model-facing tool or permission plugin uses when it needs to pause work and ask the human for a decision.
UserInteractionService (ctx key: userInteraction)ctx.userInteraction.registerProvider(provider): () => void Register the UI-side provider. Only one provider may be active in a context; disposal unregisters it.ctx.userInteraction.ask(request): Promise<AskUserQuestionAnswer> Ask the active provider and wait for the answer.AskUserQuestionRequest — { questions: [{ id, question, detail?, header?, options?, multiSelect?, intent? }], agent?, signal? }; detail supplies supporting text that providers render with the question without turning it into an option label.AskUserQuestionOption — { label, description? }.AskUserQuestionIntent — { kind: 'plan-review', approve }; the tagged presentation intent below.AskUserQuestionAnswer — { answers: [{ id, selected, custom? }] }.UserInteractionProvider — UI implementation with ask(request).UserInteractionError — HarnessError subclass with codes such as EMPTY_QUESTIONS, BAD_INTENT, NO_PROVIDER, DUPLICATE_PROVIDER, and ASK_ABORTED.For a single-select question, custom overrides the selected choice and selected is empty. For a multi-select question, custom may supplement the labels in selected. A UI may preserve a skipped item as { id, selected: [] }, keeping the existing answer shape while retaining other answers in the batch.
intent declares that a question IS a decision of a known shape, so a UI that recognises the tag may present it as such — plan-review says detail is a plan under review, and dsh-plan-mode sets it on the exit_plan_mode question. An intent shapes presentation only: a UI honouring it answers with the same option labels a generic UI would send, and a UI that does not know the tag renders the generic option list, so callers read one answer shape either way. approve names the label that approves rather than relying on option order. ask() rejects with BAD_INTENT the two assertions no type can carry: an approve naming none of that question's own options, and an intent on a question with no detail — the thing it declares itself a review of.
This is the interface package. Model-facing consumers such as @deepseek-ai/dsh-tool-ask-user depend on this seam; the Web host runtime provides the shipped interactive implementation. The loop stays unchanged: a tool call awaits a promise, and the tool result resumes the normal agent loop.
Indirectly, through dsh-tool-ask-user, which retains a successful provider answer as compact JSON or one of these failures: Error: ask_user_question was aborted before the user answered, Error: ask_user_question requires at least one question, Error: no user-interaction provider is registered, or Error: <message>. Waiting for the human adds no tokens.
No direct invalidation; the named consumer owns any request-prefix changes.
DUPLICATE_PROVIDER, and with none registered ask() throws NO_PROVIDER rather than degrading.