2026-06-29 10:49:07 +08:00
# User Interaction
2026-07-15 23:11:25 -07:00
English | [中文 ](user-interaction.zh.md )
refactor(packages): dissolve ui/ and rename sdk/ to scaffold/
git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).
The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.
app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
2026-07-30 03:13:49 +08:00
The user-interaction seam of [dsh-user-interaction ](../../packages/interaction/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` ; the host runtime relays requests to its connected client.
2026-06-29 10:49:07 +08:00
refactor(packages): dissolve ui/ and rename sdk/ to scaffold/
git mv per the regrouping RFC: the five human-collaboration seams and
tui join packages/interaction/, app-boot becomes packages/boot/, and
jsonrpc joins the renamed scaffold/ (formerly sdk/) as its server half
beside client/protocol/create-sdk/helper/scripts/telemetry, whose
folders drop the legacy sdk- prefix. Three new group README triplets
replace the ui/ and sdk/ ones; tsconfig references/paths/globs,
knip keys, vitest globs, gate scripts, catalogs, docs, and the
lockfile follow. Adds the four settled FIXME rename markers
(dsh-sdk-server, dsh-sdk-telemetry, dsh-sdk-helper, dsh-sdk-scripts).
The scaffold folders diverge from their npm names until those renames
land, so tsconfig.base.json maps the three affected names explicitly
beside the group wildcard. Also repairs two pre-existing stale-path
classes the strengthened sweep surfaced: docs/web-styling.md's retired
web-ui host package and type-model spec fixture-literal joins.
app-boot's three Loader-composition specs time out at the default 5s
under full-suite parallel load on this filesystem (pre-existing;
pass isolated with --testTimeout=30000); interaction/scaffold/boot
suites otherwise green (687 passed).
2026-07-30 03:13:49 +08:00
Source: [`packages/interaction/user-interaction/src/index.ts` ](../../packages/interaction/user-interaction/src/index.ts )
2026-06-29 10:49:07 +08:00
## Question options
2026-08-09 15:27:21 +08:00
`AskUserQuestionOption` contains one selectable choice. `label` is the user-facing option text and also the model-facing selected value; `description` is optional UI help text.
2026-06-29 10:49:07 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** One selectable answer offered to the user. */
2026-06-29 10:49:07 +08: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
## Presentation intent
2026-08-09 15:27:21 +08:00
`AskUserQuestionIntent` optionally declares a known decision kind. 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 changes presentation only — a UI honouring it answers with the same option labels a generic UI would send, so the caller reads the same answer fields either way. `approve` names the affirmative option instead of relying on option order. `ask()` rejects the two assertions no type can carry: an `approve` naming none of its own question's options, and an intent on a question with no `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-07 14:57:06 +08:00
## Question item
2026-06-29 10:49:07 +08:00
2026-07-20 22:34:09 +08:00
`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.
2026-06-29 10:49:07 +08:00
```ts type-equiv
2026-07-20 22:34:09 +08:00
/** One question in a user-interaction request. */
2026-07-07 14:57:06 +08:00
interface AskUserQuestionItem {
2026-07-20 22:34:09 +08:00
/** Stable caller-provided question id, echoed in the answer. */
2026-07-07 14:57:06 +08:00
id: string
2026-06-29 10:49:07 +08:00
/** The question to display. */
question: string
2026-07-20 22:15:50 +08:00
/** Optional supporting detail rendered with the question but kept out of option labels. */
detail?: string
2026-06-29 10:49:07 +08:00
/** Optional short heading/group label. */
header?: string
/** Optional choices the UI can render as a menu. */
options?: AskUserQuestionOption[]
2026-07-07 14:57:06 +08:00
/** 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-07 14:57:06 +08:00
}
```
## Ask request
2026-08-08 15:30:08 +08:00
`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. When present, `agent` is the exact live caller; the interaction seam admits it only while the live registry identifies that instance as a runtime root.
2026-07-07 14:57:06 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** Request for a human answer. */
2026-07-07 14:57:06 +08: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-06-29 10:49:07 +08:00
agent?: Agent
/** Abort signal for the owning tool/step. */
signal?: AbortSignal
}
```
## Answer
2026-07-30 00:21:47 +08:00
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. 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 also use an item with empty `selected` and no `custom` to preserve a skipped question in an otherwise completed batch.
2026-07-07 14:57:06 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** Answer to one question. */
2026-07-07 14:57:06 +08: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-07 14:57:06 +08:00
selected: string[]
/** Optional free-text "Other" answer. */
custom?: string
}
```
2026-06-29 10:49:07 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** The human's answer. */
2026-06-29 10:49:07 +08:00
interface AskUserQuestionAnswer {
2026-07-07 14:57:06 +08:00
/** Structured answers keyed by question id. */
answers: AskUserQuestionAnswerItem[]
2026-06-29 10:49:07 +08:00
}
```
## Provider
Only one provider may be active in a context. Provider registration is effect-bound so HMR/disposal removes the active UI.
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** UI-side provider for user questions. */
2026-06-29 10:49:07 +08:00
interface UserInteractionProvider {
ask(request: AskUserQuestionRequest): Promise< AskUserQuestionAnswer >
}
```
## Errors
2026-07-24 01:40:25 +08:00
`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.
2026-06-29 10:49:07 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** Stable error taxonomy for user-interaction failures. */
2026-06-29 10:49:07 +08: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 -->