QP内容库 Logo
首页
文章
文档
默认分类
关于
登录 →
QP内容库 Logo
首页 文章
文档
默认分类 关于
登录
  1. 首页
  2. 文档
  3. DeepSeek Harness
  4. 子系统
  5. 用户交互

用户交互

  • 子系统
  • 发布于 2026-09-30
  • 0 次阅读
目录
当前文章没有目录

English | 中文

dsh-user-questions 的用户交互 seam。它是工具或权限插件需要人类回答后 agent(智能体)才能继续时所使用的、提供方无关的词汇。Agent-scoped waterfall listener 组合可用的 UI 界面,其中包括转发到已连接 client 的 listener。

源码:packages/interaction/user-questions/src/index.ts

问题选项

AskUserQuestionOption 包含一个可供选择的选项。label 是面向用户的选项文字,同时也是面向模型的选中值;description 是可选的 UI 帮助文本。

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

呈现意图

AskUserQuestionIntent 可选地声明一种已知的决策类型。它以 kind 作为标记,以便后续加入更多意图;不认识某个标记的 UI 会渲染通用选项列表。意图只改变呈现——遵循它的 UI 回答的选项标签与通用 UI 相同,因此调用方读到的答案字段一致。approve 指明表示同意的选项,而不依赖选项顺序。ask() 拒绝两种类型无法表达的断言:approve 未命名该问题自己的任何选项,以及没有 detail 的问题声明了意图。

/**
 * A caller-declared presentation intent: the question IS this kind of
 * decision, 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 changes presentation only, never the protocol.
 */
type AskUserQuestionIntent = {
  /** A plan submitted for review: `detail` is the plan markdown `ask()` requires, 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
  /** Logged tool invocation whose arguments contain the reviewed plan. */
  callId?: ToolCallId
}

问题条目

AskUserQuestionItem 是请求中的一个问题。调用方提供稳定的 id,它会随答案原样返回,使批量问题仍可路由。可选的 detail 携带辅助文本;提供方会将其随问题渲染,但不会放入可选选项标签。

/** One question in a user-questions 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
}

提问请求

AskUserQuestionRequest 是跨包请求。questions 是数组,这样 UI 可以在一个流程中呈现相关提示,同时保持每个回答有稳定的 id。如提供 agent,它必须与存活调用方是同一实例;只有当当前注册表将该实例识别为运行时根时,交互 seam 才会接纳该 agent。

/** Request for a human answer. */
interface AskUserQuestionRequest extends AskUserQuestionRequestEvent {}

回答

提供方为每个问题 id 返回一个回答项。selected 包含选中的选项标签,custom 在用户输入自由文本时携带「其他」回答。对于单选题,custom 会覆盖选中的选项,且 selected 为空。对于多选题,custom 可以补充 selected 中的标签。UI 也可以使用 selected 为空且不含 custom 的回答项,在其余问题均已完成的批次中保留被跳过的问题。

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

计时回答器收到 wait: { callId, timed: true }。调用 id 标识卡片及前台等待。Client 调用 attachWait 接手等待并取得剩余时长;传输层转发原事件,不检查或改写这些字段。不带 timed 的 wait 表示无限期等待的具名卡片请求;没有 wait 则表示不带卡片 key 的阻塞请求。

/** Client-safe payload declared for the user-question answerer waterfall. */
interface AskUserQuestionRequestEvent {
  /** Questions to display. */
  questions: AskUserQuestionItem[]
  /** Agent identity projected to the corresponding Client Context in transit. */
  agent?: Agent
  /** Cancellation lifetime of the pending request. */
  signal?: AbortSignal
  /**
   * Tool call the Client card is keyed by. Timed answerers attach to the
   * business wait stream before starting their local countdown.
   */
  wait?: {
    /** Tool call the Client card is keyed by. */
    callId: ToolCallId
    /** True for a foreground wait that requires a Client claim; absent for indefinite waits. */
    timed?: boolean
  }
}

计时问题与迟到回复

计时问题在有界的前台窗口内等待,然后让 agent 继续独立工作。用户在窗口内回答,工具返回整批答案;否则 TimedUserQuestionResult 为 { pending: true, callId }。pending 表示问题仍可回答,不是空答案、拒绝、确认或授权。

/** Timed ask result returned when the foreground answer window closes. */
type TimedUserQuestionResult = AskUserQuestionAnswer | { pending: true; callId: ToolCallId }

Client 接手通过业务 Remote stream 表达,不等同于 Gateway 投递。TimedQuestionWait 保存原 Host deadline,仅在没有接手记录时运行计时器。attachWait stream 发出当前剩余时长,Client 用本地时钟倒计时。聚焦、编辑和「慢慢回答」状态仍归本地所有。stream 取消释放相应接手记录;最后一个接手方离开后,Host 恢复原 deadline。无人接手时超时中止前台 waterfall 专属的 signal 并返回 pending,不取消 Turn。Gateway 与 api-remotes 不包含问答专属计时策略。

userQuestions Session 投影把现有事件折叠成可回答的问题集合与已结算的集合;不记录任何等待状态、聚焦状态或 deadline。每个 request/header 都记录了模型看到的确切工具 schema。只有请求头声明了带 timeout 参数的 timed ask_user_question schema,投影才跟踪原生调用;阻塞式 legacy 工具从不声明它。原生 tool/call 打开一个问题。tool/result 只在两种情况下把它标为 continued:带 pending 载荷,或是 Session resume 修复补写的 TOOL_OUTCOME_UNKNOWN 合成结果;回答批次把它连同该批次移入 settled,其他结果或失败都丢弃它。run_code 的 PTC 子调用若在 tool/ptc-dispatch 结果中返回 pending,也会进入投影,尽管请求头只列出 run_code。迟到回复在消息获准成为 user/message 时,才把已继续的问题连同消息中的回答批次移入 settled:计时调用自己的结果记录的是超时,transcript 行无处可读用户最终的选择。排队的回复可在准入前取消,此时问题仍可回答。回复正文不含可读批次时,该调用以零答案结算;回复指向的调用已不在可回答集合中时不改变任何内容。

/** `open` while the tool call may still return the answer; `continued` once the answer can only arrive as a new turn. */
type UserQuestionState = 'open' | 'continued'
/** One unanswered timed `ask_user_question` call reconstructed from the Session log. */
interface PendingUserQuestion {
  readonly callId: ToolCallId
  readonly questions: readonly AskUserQuestionItem[]
  readonly state: UserQuestionState
}
/**
 * One timed `ask_user_question` call and the answers it settled with: the
 * batch its own result carried when the user answered inside the window,
 * otherwise the batch its late reply carried, because that call's own result
 * recorded the timeout. A transcript row reads what the user finally answered
 * from here, and only a call listed here was a timed one.
 */
interface SettledUserQuestion {
  /** The settled call. */
  readonly callId: ToolCallId
  /** The recorded batch, one entry per question; empty when the late reply carried none. */
  readonly answers: readonly AskUserQuestionAnswerItem[]
}
/**
 * Both halves of one Session's timed `ask_user_question` state, as every
 * Client reads them. A call made while the blocking legacy tool was in
 * effect appears in neither half.
 */
interface UserQuestionProjectionView {
  /** Calls that can still take an answer, in ask order. */
  readonly active: readonly PendingUserQuestion[]
  /** Calls an answer settled, in settlement order. */
  readonly settled: readonly SettledUserQuestion[]
}

开放中的问题只能通过 waterfall 回答。问题变为 continued 后,answer Remote 方法向所属 agent 排入一条用户消息,并在其空闲时唤醒它。消息的 source 为 user-question-reply,outcome 为 answered,正文是 answer_to_pending_question 载荷。开放或未知调用返回 false;回复仍在排队时的第二次提交以 REPLY_QUEUED 拒绝,Host 重启后也一样;遗漏或重复问题的回答批次以 BAD_ANSWER 拒绝。

收起 Client 面板不会发送内容。前台超时只结束工具等待。已继续的问题没有期限;在回复进入会话前,问题仍可回答。

Host 重启后,Session 重开时 Remote 层会恢复根 agent。之后的迟到回复会作为新一轮进入。

错误

UserQuestionError 继承 HarnessError,因此 ctx.tools.execute() 会保留 { name, code },用于面向模型的工具失败,如 EMPTY_QUESTIONS、BAD_INTENT、NO_PROVIDER、ASK_ABORTED 或 UI 侧取消。

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

Cordis API

Generated from source by scripts/gen-cordis-catalog.ts (verified fresh by pnpm run verify-cordis-catalog in doc-sync; regenerate with pnpm run gen-cordis-catalog) — the language sides differ only in locale-specific paired document paths. Signature blocks use a ts cordis-catalog fence and keep the original source JSDoc; dispatch modes are defined in the primer, and the framework-inherited ctx API lives in cordis-api/inherited.md.

ctx.userQuestions — UserQuestionService

ctx.userQuestions: validation plus the scoped answerer waterfall.

/**
 * Answer a continued question. The reply is steered into the agent as a
 * user message whose source names the call; that message is also the
 * record that closes the question in the projection.
 * @param agent - Live root agent for the owning Session.
 * @param callId - Continued question identity.
 * @param answer - Complete structured answer batch, one item per question of the call.
 * @returns Whether the question is still continued; an accepted reply stays
 *   queued until the agent admits its user message.
 * @throws {UserQuestionError} `BAD_ANSWER` when the batch does not name each
 *   question of the call exactly once, or `REPLY_QUEUED` when a reply is
 *   already waiting for admission.
 */
@Remote answer(agent: Agent, callId: ToolCallId, answer: AskUserQuestionAnswer): boolean

/**
 * Let one answer UI hold a live timed wait. Closing the stream releases its claim.
 * @param agent - Live root agent owning the question.
 * @param callId - Foreground tool call to attach to.
 * @param signal - Remote stream cancellation, including Client disconnect.
 * @returns One Host-computed remaining duration, or no frames once the wait ended.
 */
@Remote({ mode: 'stream' }) async *attachWait(agent: Agent, callId: ToolCallId, signal: AbortSignal): AsyncIterable<{ remainingMs: number }>

/**
 * Foreground wait whose first settlement the Client decides: the Client
 * rejects with `ASK_TIMED_OUT` when its countdown ends, and this method maps
 * that code to the pending result.
 * @param request - Questions, live owner agent, and abort signal.
 * @param callId - Tool call identity the Client card is keyed by.
 * @param timeoutMs - Positive foreground wait in milliseconds.
 * @returns The answer when it arrives inside the window, otherwise a pending
 *   result, also when no connected Client claimed the request by the deadline.
 * @throws {UserQuestionError} `BAD_TIMEOUT` for a non-integer, non-positive,
 *   or oversized wait.
 */
async askTimed( request: AskUserQuestionRequest & { agent: Agent }, callId: ToolCallId, timeoutMs: number, ): Promise<TimedUserQuestionResult>

/**
 * Ask the scoped answerer waterfall and wait for the user's answer.
 *
 * When a caller supplies an agent, human interaction is valid only for the
 * exact live runtime root. Runtime ownership, not durable session lineage,
 * decides this boundary: an owned child has no human answerer and would
 * block forever, while a lineage-bearing session resumed as a new runtime
 * root may ask normally.
 *
 * @param request Questions, owner agent, and abort signal.
 * @returns The answer chosen or typed by the human.
 * @throws {UserQuestionError} code `ASK_ABORTED` when the supplied signal
 *   is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent
 *   is not the registry's exact live instance, or `DELEGATED_CALLER` when
 *   that live agent is owned by another agent.
 */
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>

Types: Agent · ToolCallId

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

user-questions/* events

user-questions/request — waterfall

Ask composed answerers for structured user input. Return an answer to claim the request or call next() to delegate. Scope-filtered dispatch (@deepseek-ai/dsh-scope): agent-scoped listeners receive only that agent.

/**
 * Ask composed answerers for structured user input. Return an answer to
 * claim the request or call `next()` to delegate. Scope-filtered dispatch
 * (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
 * @param request - pending user-question request.
 * @mode waterfall
 */
'user-questions/request'( this: Scoped<Agent>, request: AskUserQuestionRequestEvent, next: () => Promise<AskUserQuestionAnswer>, ): Promise<AskUserQuestionAnswer>

Types: Agent · Scoped

Source: packages/interaction/user-questions/src/types.ts

相关文章

工作区

English | 中文 工作区(workspace)是用户工作目录的持久记录:一个建立在规范路径之上的稳定 id、一个显示标题,以及归属于它的会话的有序账本。该子系统是单个包(package)(dsh-workspace,ctx.workspaceRegistry)——一项宿主侧可选能力,不属于

工作流

English | 中文 工作流 seam 允许 agent(智能体)运行由模型编写、会启动 subagent 的编排脚本。与 subagent 一样,它是一项可选能力,不属于 agent loop,因此其类型和操作记录在此处,而非 core.md。与 bash 一样,每个上下文只允许一个引擎实现提

Webhook runtime

English | 中文 Webhook 子系统会把已通过身份验证的外部交付转换为可选的普通根 Session。提供方适配器拥有身份验证与通用 JSON 接收;受信任的程序化规则拥有条件与外部调用;ctx.webhookRuntime 拥有回调生命周期以及基于 Workspace 的 Session

Web 访问

English | 中文 Web 访问 seam 是一个能力 seam,在同一个 ctx.web 服务上横跨两项操作(search 与 fetch),并拆分到多个包:Service Definition(dsh-web,ctx.web + 提供方注册表)、Service Provider(dsh-w

HTTP 服务器

English | 中文 dsh-host-webserver 是 GUI Host 的浏览器 HTTP 载体:它是一个提供 ctx.webServer 的 node:http 插件,包含具名路由注册表、可选的 gzip 响应压缩、index.html 转换回调,以及一个可由插件认领的回退处理器。它

Web Client 架构

English | 中文 Web Client 是由独立加载插件组装而成的浏览器侧 Cordis 应用。它有四个可复用底座:Client Modules 加载插件图,API Gateway 提供类型化 Host 通信,Slots 组合 React UI,Conversation 把 Session

目录
当前文章没有目录

星沉月落夜闻香,素手出锋芒。

Copyright © 2026 quping.com All Rights Reserved. Powered by Halo.
鲁ICP备09092435号