Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md
T

9.3 KiB

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

Status: implemented

English | 中文

问题

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) 解析并运行已知定义,返回分离后的 successerror 结果;无效语法和未知名称返回 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 把 helpclearcancelreasoningtoolsredrawexit 注册为智能体作用域命令定义,不再对字符串执行 switch。自动补全与帮助视图读取实时目录,因此插件命令会随其副作用出现和消失。任何以 / 开头的提交行都留在命令平面;未知输入产生终端警告,不会落入 Agent.send()Agent.steer()

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

ACP 映射

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

ACP 允许命令提示携带额外的受支持内容块。桥接应用普通的无损 textresource_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 消费它。