Files
deepseek-harness/docs/core-data-structures/user-interaction.md
creatixchu 89d30b0ef7 feat(user-interaction): declare a plan-review presentation intent on questions
A question may now carry `intent`, a tagged declaration that it IS a decision
of a known shape, so a UI that recognises the tag can present it as such
instead of as a generic option list. The one member is
`{ kind: 'plan-review', approve }`, which plan-mode sets on the exit_plan_mode
review.

An intent shapes presentation only: a UI honouring it answers with the same
option labels a generic UI would send, so the tool reads one answer shape
either way, and a UI that does not know the tag renders the generic flow.
`approve` names the affirmative option rather than relying on option order;
since no type can tie that label to the question's own option list, `ask()`
rejects a mismatch as BAD_INTENT, and the wire schema rejects an unknown tag
outright rather than silently rendering generic.

plan-mode also stops reporting a dismissed review as "the user cancelled
ask_user_question" — a tool it never called. A dismissal now tells the model
the user took the turn back to speak, and to stay in plan mode and wait; every
other ask failure keeps its own message.
2026-07-30 19:09:19 +08:00

5.5 KiB

User Interaction

English | 中文

The user-interaction seam of dsh-user-interaction. It is the provider-neutral vocabulary a tool or permission plugin uses when it needs the human to answer before the agent can continue. UI surfaces provide the active UserInteractionProvider; dsh-tui uses keyboard-driven overlays and the host runtime relays requests to its connected client.

Source: packages/ui/user-interaction/src/index.ts

Question options

AskUserQuestionOption is the selectable-choice shape. label is the user-facing option text and also the model-facing selected value; description is optional UI help text.

/** One selectable answer offered to the user. */
interface AskUserQuestionOption {
  /** User-facing label. */
  label: string
  /** Optional extra context rendered by capable UIs. */
  description?: string
}

Presentation intent

AskUserQuestionIntent is the optional declaration that a question IS a decision of a known shape. It is tagged on kind so intents can be added; a UI that does not recognise a tag renders the generic option list. An intent shapes presentation only — a UI honouring it answers with the same option labels a generic UI would send, so the caller reads one answer shape either way. approve names the affirmative option instead of relying on option order, and ask() rejects an approve naming none of its own question's options.

/**
 * A caller-declared presentation intent: the question IS a decision of this
 * shape, so a UI that recognises the tag may present it as such instead of as a
 * generic option list. Tagged so further intents can be added; a UI that does
 * not know a tag renders the generic flow, and the answer encoding is identical
 * either way — an intent shapes presentation only, never the protocol.
 */
type AskUserQuestionIntent = {
  /** A plan submitted for review: `detail` is the plan markdown, and the decision approves or declines it. */
  kind: 'plan-review'
  /**
   * The option label that approves the plan; every other option declines it.
   * Named rather than positional so no UI infers the verdict from option order.
   * An `approve` naming no option of its own question is rejected at `ask()`.
   */
  approve: string
}

Question item

AskUserQuestionItem is one question in a request. The caller supplies a stable id, which is echoed back with the answer so batched questions remain routable. Optional detail carries supporting text that providers render with the question but keep out of selectable option labels.

/** One question in a user-interaction request. */
interface AskUserQuestionItem {
  /** Stable caller-provided question id, echoed in the answer. */
  id: string
  /** The question to display. */
  question: string
  /** Optional supporting detail rendered with the question but kept out of option labels. */
  detail?: string
  /** Optional short heading/group label. */
  header?: string
  /** Optional choices the UI can render as a menu. */
  options?: AskUserQuestionOption[]
  /** Whether more than one option may be selected. Defaults to single-select. */
  multiSelect?: boolean
  /** Optional presentation intent for capable UIs; absent asks for the generic option list. */
  intent?: AskUserQuestionIntent
}

Ask request

AskUserQuestionRequest is the cross-package request. questions is an array so a UI can present related prompts in one flow while preserving a stable id per answer.

/** Request for a human answer. */
interface AskUserQuestionRequest {
  /** Questions to display. */
  questions: AskUserQuestionItem[]
  /** Calling agent, when the request came from an agent tool call. */
  agent?: Agent
  /** Abort signal for the owning tool/step. */
  signal?: AbortSignal
}

Answer

Providers return one answer item per question id. selected contains selected option labels, and custom carries a free-form "Other" answer when the user typed one. When custom is present, selected is empty; custom text is an answer override, not a supplement to selected choices. A UI may also use an item with empty selected and no custom to preserve a skipped question in an otherwise completed batch.

/** Answer to one question. */
interface AskUserQuestionAnswerItem {
  /** The answered question id. */
  id: string
  /** Selected option labels. Empty for custom or unanswered choices. */
  selected: string[]
  /** Optional free-text "Other" answer. */
  custom?: string
}
/** The human's answer. */
interface AskUserQuestionAnswer {
  /** Structured answers keyed by question id. */
  answers: AskUserQuestionAnswerItem[]
}

Provider

Only one provider may be active in a context. Provider registration is effect-bound so HMR/disposal removes the active UI.

/** UI-side provider for user questions. */
interface UserInteractionProvider {
  ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
}

Errors

UserInteractionError extends HarnessError, so ctx.tools.execute() preserves { name, code } for model-facing tool failures such as EMPTY_QUESTIONS, NO_PROVIDER, ASK_ABORTED, or UI-side cancellation.

/** Stable error taxonomy for user-interaction failures. */
class UserInteractionError extends HarnessError {
  constructor(message: string, code: string, options?: ErrorOptions) {
    super(message, code, options)
    this.name = 'UserInteractionError'
  }
}