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

Status: implemented

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

## 问题

TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派和取消都留在适配器内部，每个新命令都需要修改 TUI，可选插件也无法贡献命令。把斜杠输入当作普通模型提示词同样不安全：用户可见的直接操作可能意外消耗 token，或让模型重新解释未知命令。

共享机制必须仍是 UI 关注点，而不是模型工具或 agent loop（智能体循环）分支。它还需要精确到每个 agent（智能体）的可见性、HMR（热模块替换）安全移除、直接结果渲染和请求作用域取消，同时不会自动把命令文本或输出加入模型历史。

## 决策

位于 `packages/interaction/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用组合包把它挂载在消费该服务的前端旁；[仅面向自动化的 ACP（Agent Client Protocol）应用](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)和无执行器、无 UI 的 agent spine 都省略该服务。TUI 注入该服务，命令生产者只依赖注册表及其操作的领域。

### 注册表约定

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

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

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

### 作用域与生命周期

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

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

### 直接分派与取消

命令在仅面向人类的命令平面中运行。注册表不会把输入转成 `user/message`，输出不会成为会话事件，两者都不会隐式发送给模型。处理器接收对应的同一个目标 agent、原始输入和本次请求持有的 `AbortSignal`；生产者可以通过该 agent 显式调度单独的模型可见工作，随后由生产者负责其日志记录和生命周期约定。信号中止时，注册表不再等待不合作的处理器；处理器仍负责停止已经启动的外部副作用。

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

### TUI 映射

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

每次提交命令都会创建一个专属 `AbortController`。TUI 释放会中止未完成的分派、移除本地定义，并等待命令生产者的 fiber 结束后再完成清理。

## 测试

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

TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与自动补全刷新、直接结果、未知命令拒绝、原始输入交付、定义移除、启动回滚和释放取消。无密钥终端快照固定渲染后的帮助、错误与命令结果形态。

## 考虑过的替代方案

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

## 后果

- 命令生产者是普通的可移除插件，TUI 消费其经过校验的目录与分派约定。
- 特定于 agent 的定义保留现有扁平作用域与遮蔽语义，不引入核心到 UI 的依赖。
- 未知斜杠输入与命令输出是确定性 UI 行为，直接模型 token 成本为零。
- 直接命令取消与模型轮次取消彼此隔离。

## 已知限制与暂缓事项

- 输入元数据仅限非结构化文本提示。类型化表单、参数 schema 和补全提供方仍由命令拥有，或需要后续注册表或消费方扩展。
- 通用命令输出仅实时存在，TUI 重启后不会重建。
- 注册表取消会立即停止等待，但外部工作只有在处理器配合信号时才会停止。
- ACP 自动化服务器、无头 CLI（命令行界面）与 JSON-RPC SDK 运行入口不暴露命令平面；只有 TUI 消费它。
