2026-07-15 23:11:25 -07:00
# 用户交互
[English ](user-interaction.md ) | 中文
2026-08-09 11:02:16 +08:00
[dsh-user-interaction ](../../packages/interaction/user-interaction ) 的用户交互 seam。它是工具或权限插件需要人类回答后 agent(智能体)才能继续时所使用的、提供方无关的词汇。UI 界面提供活跃的 `UserInteractionProvider` ;host 运行时把请求转发给其连接的客户端。
2026-07-15 23:11:25 -07:00
2026-07-30 03:13:49 +08:00
源码:[`packages/interaction/user-interaction/src/index.ts` ](../../packages/interaction/user-interaction/src/index.ts )
2026-07-15 23:11:25 -07:00
## 问题选项
2026-08-09 15:27:21 +08:00
`AskUserQuestionOption` 包含一个可供选择的选项。`label` 是面向用户的选项文字,同时也是面向模型的选中值;`description` 是可选的 UI 帮助文本。
2026-07-15 23:11:25 -07:00
```ts type-equiv
2026-07-22 22:58:05 +08:00
/** One selectable answer offered to the user. */
2026-07-15 23:11:25 -07:00
interface AskUserQuestionOption {
/** User-facing label. */
label: string
/** Optional extra context rendered by capable UIs. */
description?: string
}
` ``
2026-07-30 19:09:19 +08:00
## 呈现意图
2026-08-12 12:30:19 +08:00
` AskUserQuestionIntent` 可选地声明一种已知的决策类型。它按 ` kind` 打标签,因此可以增加新的意图;不认识某个标签的 UI 渲染通用选项列表。意图只改变呈现方式——遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此调用方两种情况下读到的回答字段相同。` approve` 指名肯定选项,而不依赖选项顺序。` ask()` 会拒绝两种无法由类型系统表达的情况:` approve` 未指向该问题自身的任何选项,以及为没有 ` detail` 的问题指定意图。
2026-07-30 19:09:19 +08:00
` ``ts type-equiv
/**
2026-08-09 15:27:21 +08:00
* 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
2026-07-30 19:09:19 +08:00
* 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
2026-08-09 15:27:21 +08:00
* either way — an intent changes presentation only, never the protocol.
2026-07-30 19:09:19 +08:00
*/
type AskUserQuestionIntent = {
2026-07-30 19:38:18 +08:00
/** A plan submitted for review: ` detail` is the plan markdown ` ask()` requires, and the decision approves or declines it. */
2026-07-30 19:09:19 +08:00
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
}
` ``
2026-07-15 23:11:25 -07:00
## 问题条目
2026-08-12 12:30:19 +08:00
` AskUserQuestionItem` 是请求中的一个问题。调用方提供稳定的 ` id`,它会随答案原样返回,使批量问题仍可路由。可选的 ` detail` 携带辅助文本;提供方会将其随问题渲染,但不会放入可选选项标签。
2026-07-15 23:11:25 -07:00
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** One question in a user-interaction request. */
2026-07-15 23:11:25 -07:00
interface AskUserQuestionItem {
2026-07-22 22:58:05 +08:00
/** Stable caller-provided question id, echoed in the answer. */
2026-07-15 23:11:25 -07:00
id: string
/** The question to display. */
question: string
2026-07-22 22:58:05 +08:00
/** Optional supporting detail rendered with the question but kept out of option labels. */
detail?: string
2026-07-15 23:11:25 -07:00
/** 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
2026-07-30 19:09:19 +08:00
/** Optional presentation intent for capable UIs; absent asks for the generic option list. */
intent?: AskUserQuestionIntent
2026-07-15 23:11:25 -07:00
}
` ``
## 提问请求
2026-08-09 11:02:16 +08:00
` AskUserQuestionRequest` 是跨包请求。` questions` 是数组,这样 UI 可以在一个流程中呈现相关提示,同时保持每个回答有稳定的 id。如提供 ` agent`,它必须与存活调用方是同一实例;只有当当前注册表将该实例识别为运行时根时,交互 seam 才会接纳该 agent。
2026-07-15 23:11:25 -07:00
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** Request for a human answer. */
2026-07-15 23:11:25 -07:00
interface AskUserQuestionRequest {
/** Questions to display. */
questions: AskUserQuestionItem[]
2026-08-08 15:30:08 +08:00
/** Exact live calling agent, when the request came from an agent tool call. */
2026-07-15 23:11:25 -07:00
agent?: Agent
/** Abort signal for the owning tool/step. */
signal?: AbortSignal
}
` ``
## 回答
2026-07-30 00:21:47 +08:00
提供方为每个问题 id 返回一个回答项。` selected` 包含选中的选项标签,` custom` 在用户输入自由文本时携带「其他」回答。对于单选题,` custom` 会覆盖选中的选项,且 ` selected` 为空。对于多选题,` custom` 可以补充 ` selected` 中的标签。UI 也可以使用 ` selected` 为空且不含 ` custom` 的回答项,在其余问题均已完成的批次中保留被跳过的问题。
2026-07-15 23:11:25 -07:00
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** Answer to one question. */
2026-07-15 23:11:25 -07:00
interface AskUserQuestionAnswerItem {
/** The answered question id. */
id: string
2026-07-30 00:21:47 +08:00
/** Selected option labels. May accompany custom text for a multi-select question. */
2026-07-15 23:11:25 -07:00
selected: string[]
/** Optional free-text "Other" answer. */
custom?: string
}
` ``
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** The human's answer. */
2026-07-15 23:11:25 -07:00
interface AskUserQuestionAnswer {
/** Structured answers keyed by question id. */
answers: AskUserQuestionAnswerItem[]
}
` ``
## 提供方
2026-07-22 03:06:01 -07:00
同一上下文中只能有一个活跃的提供方。提供方注册绑定到 effect,因此 HMR(热模块替换)或 dispose(资源释放)会移除当前活跃的 UI。
2026-07-15 23:11:25 -07:00
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** UI-side provider for user questions. */
2026-07-15 23:11:25 -07:00
interface UserInteractionProvider {
ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
}
` ``
## 错误
2026-07-24 23:43:10 +08:00
` UserInteractionError` 继承 ` HarnessError`,因此 ` ctx.tools.execute()` 会保留 ` { name, code }`,用于面向模型的工具失败,如 ` EMPTY_QUESTIONS`、` NO_PROVIDER`、` ASK_ABORTED` 或 UI 侧取消。
2026-07-15 23:11:25 -07:00
` ``ts type-equiv
2026-07-22 22:58:05 +08:00
/** Stable error taxonomy for user-interaction failures. */
2026-07-15 23:11:25 -07:00
class UserInteractionError extends HarnessError {
constructor(message: string, code: string, options?: ErrorOptions) {
super(message, code, options)
this.name = 'UserInteractionError'
}
}
` ``
2026-07-30 21:40:58 +08:00
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
2026-07-24 19:54:25 +08:00
## Cordis API
2026-07-30 21:40:58 +08:00
2026-07-24 19:54:25 +08:00
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`) — this section is byte-identical in both language sides of the page. Signature blocks use a ` ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited ` ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
2026-07-30 21:40:58 +08:00
<a id="ctxuserinteraction--userinteractionservice"></a>
### ` ctx.userInteraction` — ` UserInteractionService`
2026-07-24 19:54:25 +08:00
` ctx.userInteraction`: one active UI provider plus an ` ask()` API.
2026-07-30 21:40:58 +08:00
` ``ts cordis-catalog
/**
* Register the UI provider. Only one provider may be active in a context.
*
* @param provider UI-side implementation that collects answers.
* @returns Disposer that unregisters this provider.
*/
registerProvider(provider: UserInteractionProvider): () => void
/**
* Ask the active UI provider 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 {UserInteractionError} code ` 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>
` ``
Source: [` packages/interaction/user-interaction/src/index.ts:51`](../../packages/interaction/user-interaction/src/index.ts)
<!-- END GENERATED cordis-surface -->