The human-collaboration plane
packages/interaction/ hosts the services and plugins through which a human collaborates with a running agent — questions, approvals, permission presets, and slash commands. These are all product packages: real interfaces a person drives. They integrate through existing agent and session contracts rather than changing the loop; interactive applications supply the concrete adapters, while automation uses acp/.
| Package | Role | ctx key |
|---|---|---|
@deepseek-ai/dsh-commands | Human-command registry + dispatch for interactive adapters | ctx.commands |
@deepseek-ai/dsh-user-approval | One-shot approval decisions | ctx.approval |
@deepseek-ai/dsh-permission-presets | User-facing permission preset bundles | ctx.permissionPresets |
@deepseek-ai/dsh-user-questions | Neutral human question/answer seam | ctx.userQuestions |
@deepseek-ai/dsh-tool-ask-user | Exposes questions to the model | registers on ctx.tools |
The command plane
A human command is a slash-prefixed instruction interpreted and executed by a human-facing adapter through ctx.commands, without becoming a model message. The glossary draws a sharp three-way line: a human command is distinct from a model-facing tool and from shell command execution through ctx.shell. The command plane is discovery, parsing, dispatch, cancellation, and result rendering owned by UI adapters and command plugins. Command output is UI state unless a handler separately mutates a durable domain.
Service contract (packages/interaction/commands/src/index.ts)
ctx.commands.register(definition)— registers a lowercase command name, description, optional unstructured-input hint, optionalrecordInputpolicy, and an abortable handler. A plain-context registration is global; a plugin mounted beneathagent.ctxdeclares its owncommandsinjection and creates an exact agent-scoped definition that shadows a global one with the same name. Duplicate names within a layer fail.list(agent)/find(agent, name)— immutable, name-sorted discovery after scoped shadowing; registration/removal notifies everycommands/changeobserver so live adapters refresh.execute(agent, line, signal)— parses and runs only a known command, returning a settledCommandExecution(orundefinedfor invalid syntax/unknown names).
parseCommand() recognizes a slash at byte zero, a lowercase name containing letters/digits/_/-, and end-of-input or whitespace; it returns every byte after the name as rawInput.
Command output is UI state
A resolved command's lifecycle is logged as the log-only pair command/run (before the handler, with a minted commandId, the parser's structured name, the issuing CommandSource, and args unless recordInput is false) and command/done (at settlement, with the outcome kind and verbatim text). Both are direct standalone appends on the receiving agent's session — no turn wraps them. A successful result may name an earlier non-command authoritative domain event through sourceEventSeq (e.g. /compact names its compaction/summary). Handlers return success or error plus optional UI text; results are rendered directly by the adapter and never enter model history. The registry never submits rawInput to the agent implicitly.
Cancellation
There is no command-cancel package in this revision. Cancellation is cooperative: dispatch races the handler against the command's abort signal, and the registered handler is expected to honor it. An aborted handler settles as command/done with kind: 'error'. An uncooperative handler may continue its own external side effects after the caller stops awaiting it — a documented limitation. Command-producing plugins like /compact and /goal forward the UI's cancellation signal to their underlying seam (compactNow/compactRegion signal, set reversal).
How /commands differ from tools
A slash command executes in the UI command plane without a model turn; it does not appear in the tool catalog, costs zero model tokens, and its input/output are not submitted to the model. A model-facing tool (e.g. ask_user_question, get_goal) is callable by the model inside a step and its arguments/results live in history. Some plugins expose both — /goal (human command) and get_goal/create_goal/update_goal (model tools) drive the same goal domain.
Asking the user: the question seam
@deepseek-ai/dsh-user-questions owns ctx.userQuestions, the neutral service a model-facing tool or permission plugin uses to pause work and ask the human for a decision. It is a Service Definition; consumer tools depend on it, and the UI/Web host runtime supplies the provider.
API:
registerProvider(provider)— one active provider per context;ask()throwsNO_PROVIDERwhen none (fails closed),DUPLICATE_PROVIDERon a second.ask(request): Promise<AskUserQuestionAnswer>— request{ questions: [{ id, question, detail?, header?, options?, multiSelect?, intent? }], agent?, signal? }.
Key types:
| Type | Shape |
|---|---|
AskUserQuestionOption | { label, description? } |
AskUserQuestionIntent | { kind: 'plan-review', approve } — tagged presentation intent |
AskUserQuestionAnswer | { answers: [{ id, selected, custom? }] } |
UserQuestionProvider | UI implementation with ask(request) |
UserQuestionError | codes: EMPTY_QUESTIONS, BAD_INTENT, NO_PROVIDER, DUPLICATE_PROVIDER, ASK_ABORTED, CALLER_NOT_LIVE, DELEGATED_CALLER, … |
For a single-select question custom overrides selected; for multi-select it supplements. When an agent is supplied, ask() authenticates the exact live runtime root via the AgentRegistry; a live child owned by another agent is rejected (DELEGATED_CALLER) even if its durable lineage depth is zero. intent changes presentation only — a UI that honours it answers with the same labels a generic UI sends. ask() rejects BAD_INTENT when approve names none of the question's own options, or when an intent sits on a question with no detail.
ask_user_question (model-facing)
@deepseek-ai/dsh-tool-ask-user is the Consumer: it translates model arguments into an AskUserQuestionRequest and returns the human answer. The tool calls ctx.userQuestions.ask() and returns canonical { answers: [{ id, selected, custom? }] }. Arguments (full questions) stay in the assistant tool-call history; after the human answers, the next step sees the compact JSON result. Waiting for the human adds no tokens. A pending question blocks the tool call until answered; cancellation rides the turn's exec.signal (no timeout-policy). The tool never renders UI; the Native renderer preserves the compact JSON shape.
The end-to-end question flow
model calls ask_user_question
└─> dsh-tool-ask-user -> ctx.userQuestions.ask(request)
└─> active provider (Web UI) renders the question
└─> human answers
└─> ask() resolves with AskUserQuestionAnswer
└─> compact JSON tool result -> resume the loopApprovals: one-shot gates
@deepseek-ai/dsh-user-approval is a channel-neutral, one-shot approval seam. ctx.approval.request(req) returns allowed-once, rejected, cancelled, or unavailable; missing or failing answerers fail closed, and a grant applies only to the requested action.
Each request must belong to an open agent turn. The service appends a paired approval/asked and approval/decided audit record (both log-only — the model sees only the asking consumer's resulting tool outcome). An aborted request resolves cancelled; an audit append failure before commit rejects rather than returning an unlogged decision.
Answerers are approval/request waterfall listeners: return an outcome for an owned agent, or call next() to delegate. Agent-scoped listeners receive only that agent's requests; compose one terminal answerer per deployment. The ACP automation bridge supplies one-shot machine decisions for sessions it owns. ApprovalPolicy is 'ask' or 'never'; the effective value is the last approval/policy event falling back to config, with setApprovalPolicy() as the write path. 'never' rejects before interactive dispatch.
The tools pipeline routes ask decisions through this seam and fails closed when absent; the sandboxed bash tool also uses it for escalated retries. Cross-reference /security/permissions for the full permission model.
Approval-policy runtime context contributions:
Under "ask":
Approval policy: ask. Operations that require approval may ask through the configured answerers;
without an available answerer, the request fails closed.
Under "never":
Approval prompts are disabled in this session: actions that require approval are rejected
automatically — do not request sandbox escalation (do not set `sandbox_permissions`).Permission presets (ctx.permissionPresets)
@deepseek-ai/dsh-permission-presets bundles each named preset with sandbox/mode + approval/policy. Defaults: workspace-write (workspace-write + ask) and danger-full-access (danger-full-access + never). set(session, name) records a changed selection in the log-only permission/preset event, then calls each knob's setter only when its effective value changes. current(events) returns the still-matching recorded selection, the first matching table entry, or custom. custom is derived-only — a caller can switch away from an unmatched combination but cannot target or persist it. The service owns the permission Settings namespace, whose defaultPreset applies to future sessions (creation pins the choice into the session). Two optional children ship product surfaces: a permissions session-projection unit and the /permission command.
UI side (brief)
packages/client/ui-user-questions is the Web provider for the question seam; packages/client/ui-input-trigger provides slash-command input capture in the Web UI that dispatches lines through ctx.commands.execute(). packages/client/ui-commands and the modules under it render command output. These client packages consume the seam's canonical shapes (AskUserQuestionAnswer, CommandExecution) and never redefine them.
Command lifecycle timeline
The command plane's event sequence is worth isolating because each micro-step has a distinct owner and persistence story:
UI captures "/compact" ui-input-trigger
│
▼
ctx.commands.execute(agent, line, signal)
│ parseCommand() → { name, rawInput }
│ find(agent, name) → registered handler (or undefined)
▼
append command/run { commandId, name, source, args? } ← log-only
│
▼
run handler (raced against the abort signal; UI may cancel)
│
▼
append command/done { commandId, kind, text, sourceEventSeq? } ← log-only
│
▼
adapter renders CommandResult text (UI state, never model history)
│
└ if handler mutated a durable domain (goals / plan / compaction),
that domain's own event owns the authoritative recordKey boundaries: command/run precedes the handler and mints commandId; command/done settles with an outcome kind; a successful result may name an earlier authoritative domain event via sourceEventSeq (e.g. /compact → its compaction/summary). Neither joins model history. If the handler explicitly schedules agent work through the receiving Agent (as /plan <message> does), that producer owns the resulting message contract.
Model-experience summary
- Commands: discovery, execution, and direct output add zero model tokens; input/output are not submitted. Only a command producer that explicitly steers the agent adds tokens, equal to an ordinary agent input.
- Questions: a pending
ask_user_questionadds no tokens while waiting; the full arguments stay in history, the answer returns as compact JSON, and the loop resumes. - Approvals: the
approval/asked/approval/decidedaudit is log-only; the model sees only the asking consumer'sallowed-once/rejected/cancelled/unavailableoutcome. Theask/neverpolicy contributes a concise runtime-context message on the first request and on an effective change.
All three planes keep model history clean: human command text, question UI, and approval audit are UI/audit state, while each authoritative decision lives in the durable domain it mutates.
Further reading
- Permissions & Approval — the full permission model behind the one-shot approval gate.
- Goals & Goal Rounds —
/goalcommand and goal tools as command/tool consumers. - Plan Mode —
/planand theplan-reviewquestion intent. docs/glossary.md— human command and command plane definitions.packages/interaction/commands/src/index.ts— theparseCommand()grammar andcommand/run/command/donelifecycle.docs/subsystems/commands.md,user-questions.md,approval.md— the subsystem references.