# Agent Note: 插件拥有的人类命令注册

Status: implemented

[English](2026-07-19-plugin-command-registration.md) | 中文

## 问题

TUI 拥有七个斜杠命令，而 ACP 定义了标准命令目录与调用形态。如果命令名、帮助文本、自动补全、分派和取消都留在各适配器内部，每个新命令都需要修改适配器，可选插件无法贡献命令，两个前端也会逐渐偏离。把斜杠输入当作普通模型提示同样不安全：用户可见的直接操作可能意外消耗 token，或让模型重新解释未知命令。

共享机制必须仍是 UI 关注点，而不是模型工具或智能体循环分支。它还需要精确的逐智能体可见性、可安全 HMR 移除、逐会话 ACP 发现、直接结果渲染和请求作用域取消，同时不得把命令文本或输出加入模型历史。

## 决策

位于 `packages/ui/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。终端与 ACP 应用 bundle（组合包）把它挂载在消费该服务的前端旁，SDK 项目 helper（辅助器）在直接搭建 ACP 时也会生成同一服务；无执行器、无 UI 的智能体 spine（主干）保持独立。TUI 与 ACP 注入该服务，命令生产者只依赖注册表及其操作的领域。

### 注册表契约

`CommandDefinition` 包含不带 `/` 的小写名称、非空描述、可选的非结构化输入提示，以及可取消处理器。注册会校验并分离元数据、冻结有效定义，并返回准确的 Cordis effect disposer（副作用释放器）。同一层中的重复名称会失败。每个消费该注册表的适配器都能看到所有有效定义；若命令插件无法在某种部署中运行，它就不在该部署中注册，而不是把适配器身份编码进共享领域。

`list(agent)` 在作用域遮蔽后返回不可变、按名称排序的描述符。`find(agent, name)` 解析有效定义。`execute(agent, line, signal)` 解析并运行已知定义，返回分离后的 `success` 或 `error` 结果；无效语法和未知名称返回 `undefined`，由适配器拥有直接错误文本。

`parseCommand(line)` 要求 `/` 位于第零字节，后接由字母、数字、`_` 或 `-` 组成的小写 ASCII 名称，并以空白或输入末尾结束。它把适配器交付的完整后缀保留为 `rawInput`，包括分隔空白。每个命令插件自行拥有后续语法决策。

### 作用域与生命周期

无作用域注册是全局注册。挂载在智能体上下文之下并注入 `commands` 的插件会继承该智能体的作用域键与生命周期，因此其定义仅为该准确智能体遮蔽同名全局定义。子插件自行声明 `commands` 注入，因为 `agent.ctx` 有意只继承核心智能体循环的依赖界面；仅为了实现作用域注册而让循环依赖 UI 服务会倒置依赖图。

注册和移除会发出未过滤、不可否决的 `commands/change` 注册表通知。适配器重新计算每个实时智能体的有效视图，而不尝试推断某次变更影响哪些会话。注册表会分别隔离并记录每个观察者失败，因此损坏的 UI 刷新无法回滚另一插件的变更，也无法阻止后续观察者。Cordis 所有权会在生产者、UI 实例或智能体作用域卸载时移除定义，因此 HMR 不会留下陈旧的发现项或处理器。

### 直接分派与取消

命令在仅面向人类的命令平面中运行。输入不会成为 `user/message`，输出不会成为会话事件，两者都不会发送给模型。处理器接收准确的目标智能体、原始输入和请求拥有的 `AbortSignal`。信号中止时，注册表不再等待不合作的处理器；处理器仍负责停止已经启动的外部副作用。

预期的处理器失败返回 `CommandResult.error`。抛出的异常或格式错误的结果仍是适配器可见的命令失败，而不是模型消息。该边界有意分离 UI 输出与持久领域变更：例如目标命令可以改变 `ctx.goals`，但持久状态由目标服务拥有。

### TUI 映射

TUI 把 `help`、`clear`、`cancel`、`reasoning`、`tools`、`redraw` 和 `exit` 注册为智能体作用域命令定义，不再对字符串执行 switch。自动补全与帮助视图读取实时目录，因此插件命令会随其副作用出现和消失。任何以 `/` 开头的提交行都留在命令平面；未知输入产生终端警告，不会落入 `Agent.send()` 或 `Agent.steer()`。

每个提交的命令拥有一个 `AbortController`。TUI 释放会中止未完成的分派、移除本地定义，并等待命令生产者 fiber（纤程）后再完成清理。

### ACP 映射

桥接遵循当前的 [ACP v1 斜杠命令契约](https://agentclientprotocol.com/protocol/v1/slash-commands)。`session/new` 与 `session/load` 发出准确智能体的完整 `available_commands_update` 快照；新会话的 RPC 响应会先引入服务端生成的 id，随后快照才会入队。每次注册表变更都会为每个实时会话发出替换快照。名称、描述和可选非结构化输入提示直接映射到 `AvailableCommand`。

ACP 允许命令提示携带额外的受支持内容块。桥接应用普通的无损 `text` 与 `resource_link` 扁平化，然后在结果以 `/` 开头时进入命令平面。不支持的提示块由现有能力边界拒绝。已知命令直接执行；未知或格式错误的斜杠输入返回直接错误，绝不会到达模型。成功文本、预期错误和抛出失败的诊断作为实时 `agent_message_chunk` 输出流式发送，并以 `end_turn` 结束请求。

每个 ACP 会话同时只能有一个模型提示或直接命令进行中，各会话彼此独立。当直接命令拥有请求时，`session/cancel` 会中止它；只有智能体提示才调用 `Agent.cancel()`，因此取消命令不会销毁无关的排队或注入智能体工作。连接清理会先中止命令，再释放所拥有的智能体。

## 测试

注册表测试覆盖语法边界、不可变规范化、运行时元数据校验、确定性排序、全局与作用域遮蔽、重复拒绝、准确释放、变更通知失败隔离、直接调用、预期和格式错误结果、同步与异步失败，以及每种中止时序边沿；该源文件达到逐文件 100% 语句、分支、函数和行覆盖率。

TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与自动补全刷新、直接结果、未知命令拒绝、原始输入交付、定义移除、启动回滚和释放取消。ACP 测试使用真实 SDK 连接、智能体工厂、循环与 JSONL 持久化，验证创建/加载快照、动态更新、作用域多会话目录、受支持块扁平化、直接成功/错误/失败、未知命令隔离、取消，以及不存在模型请求或会话消息。SDK helper 测试固定直接 ACP 组合。无密钥 ACP 与终端快照固定新的协议和渲染记录形态。

## 考虑过的替代方案

- **保留适配器本地 switch**——不予采纳，因为可选插件无法贡献发现与行为，除非修改每个前端。
- **把人类命令表示为模型工具**——不予采纳，因为发现与直接调用属于人类 UI 行为；经由模型路由会增加延迟、token 成本和重新解释。
- **把注册表放入核心智能体主干**——不予采纳，因为无头和 JSON-RPC 智能体不消费它，而两个 UI 应用组合包可以显式组合它。
- **让 `dsh-agent-loop` 注入 commands**——不予采纳，因为循环不执行也不发现人类命令。智能体作用域生产者改为在子插件中声明 UI 依赖。
- **为每个定义附加适配器掩码**——不予采纳，因为支持能力是组合事实，而不是命令领域状态。每个已组合适配器都暴露已注册命令；不兼容插件不会在该部署中注册。
- **把未知斜杠输入发送给模型**——不予采纳，因为输入错误或不可用的直接操作必须可预测地失败，而不能改变执行平面。
- **持久化通用命令输入与输出**——不予采纳，因为适配器提示不是模型可见状态。改变持久行为的处理器会调用拥有该状态的领域 API，由后者记录自己的事件。
- **把 ACP 命令限制为单个文本块**——不予采纳，因为 ACP v1 允许附带内容，而桥接已有无损的已接纳块转换。

## 后果

- 命令生产者是普通的可移除插件，TUI 与 ACP 共享一个经过校验的目录和分派契约。
- 智能体特定定义保留现有扁平作用域与遮蔽语义，不引入核心到 UI 的依赖。
- 未知斜杠输入与命令输出是确定性 UI 行为，直接模型 token 成本为零。
- ACP 客户端在创建、加载、注册和 HMR 移除后收到当前的逐会话快照。
- 直接命令取消与模型轮次取消彼此隔离。

## 已知限制与延期工作

- 输入元数据仅为 ACP 当前的非结构化文本提示。类型化表单、参数模式和补全提供器仍由命令拥有，或需要后续协议扩展。
- 通用命令输出仅实时存在，TUI 重启或 ACP 重新连接后不会重建。
- 注册表取消会立即停止等待，但外部工作只有在处理器配合信号时才会停止。
- 无头 CLI 与 JSON-RPC SDK 前端不暴露命令平面；只有 TUI 和 ACP 消费它。
