Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-22-web-slash-command-dispatch.zh.md
T
_Kerman 158b8a8ead feat(gui): host goal handlers and slash command dispatch on web prompts
- web host boots CommandService + command-goal; session.prompt intercepts
  a lone text block starting with '/' and executes it as a command
  (unknown -> RPC unknown-command, usage error -> RPC command-error, so
  the composer restores the draft and shows the error strip) instead of
  sending it to the model — symmetric with the ACP adapter
- prompt response carries command result text on the wire for future UI
- api-proxy also lands the host-side goals RPC handlers
2026-07-22 21:30:30 +08:00

4.8 KiB
Raw Blame History

Agent Note: Web 斜杠命令分发

Status: implemented

English | 中文

问题

面向人类的 /goal 命令交付时有两个分发点:TUI 与 ACP 适配器拦截以 / 开头的提示词,通过命令注册表执行而不消耗模型轮次。Web UI 的整条路径——输入框、客户端运行时、RPC 与宿主——都没有任何拦截,因此 /goal fix the flaky test 会作为普通用户消息到达模型。该命令消耗一次模型轮次、不产生确定性的状态改变、还可能被重新解释,而 Web 宿主组合甚至没有挂载命令注册表和 /goal 生产方。

决策

Web 宿主在自己的适配器边界分发斜杠命令,与 ACP 对称:在 packages/host/runtime/src/api-proxy.ts 的 api-proxy sessions.prompt 处理器内、agent.send/agent.steer 之前。bootHost 在目标栈之后紧接着挂载 CommandService 与 command-goal,使注册表与生产方共享宿主组合的生命周期。

内容恰好是一个以 / 开头的文本块的提示词才是命令候选。Web 输入框只会发送这种形态,多块内容绝不会被拍平成命令行。分发与模式无关:命令不消耗轮次,因此 queue 与 steer 的执行完全一致,且都不会到达 agent。

执行结果映射到输入框编排逻辑已经理解的 RPC 结果上。命令成功时返回 ok,携带 { accepted: true, command: { kind: 'success', text? } }——草稿保持已清空状态。用法或状态错误(单独的 /goal edit、多余的 /goal pause)返回新错误码 command-error 的 RPC 错误,未识别的名称返回 unknown-command;两者都会让客户端恢复输入框草稿并在错误条上显示消息,这正是畸形命令应有的 UX。这两个错误码是 RpcErrorDetailsMap 中的新行,并配有对应的错误 schema 分支;session.prompt 的响应值在签名层与 zod schema 中都增加了可选的 command 槽位。

成功文本会在线路上传输,但 Web UI 暂时不渲染它;状态改变——目标栏出现、已暂停目标恢复——就是反馈。处理器缺陷仍会传播出 commands.execute 并成为承载层 500,这符合 api-proxy 的规则:实现绝不抛出业务错误。

测试

packages/host/runtime/tests/api-proxy-command.spec.ts 针对一个记录了 send/steer 调用的结构性空闲 agent,挂载真实的命令注册表、agent 注册表、目标服务与 /goal 生产方。它覆盖:/goal <objective> 创建目标、携带 command 槽位且不经过模型轮次;steer 下与模式无关的分发;unknown-command(带与不带尾随输入)与 command-error RPC 错误;成功但不带文本的已注册命令;非命令提示词原样到达 agent.send;退化形态——多块内容、空数组、单个非文本块——绝不会被当作命令。现有的 rpc-schemas.spec.ts 对扩展后的线路形态进行门禁。

考虑过的替代方案

  • 在浏览器客户端拦截——不予采纳,因为命令注册表与目标领域位于宿主进程;客户端没有插件运行时,在客户端复制命令所有权会与宿主的组合发生偏差。
  • 把命令输出渲染为合成的助手消息——不予采纳,因为凭空制造模型可见事件会违反“模型可见 ⟺ 已记录”规则,并引入第二份审计记录;线路上携带文本,留待未来的专用表面渲染。
  • 仅在 queue 模式下分发——不予采纳,因为命令不消耗轮次,模式对它们没有意义;ACP 同样在其提示词轮次机制之外执行命令。
  • 像 ACP 那样把多块内容拍平成命令行——不予采纳,因为 Web 输入框恰好只发送一个文本块;有损的拍平路径不会有调用方。

后果

  • /goal 以及未来注册的任何命令都可以在 Web 输入框中使用且不消耗模型轮次;未知或畸形命令会恢复草稿并报错,而不是到达模型。
  • RPC 错误词汇表新增 command-error 与 unknown-command,session.prompt 响应可以携带 command 槽位。
  • 命令成功文本已在线路上但尚未在 Web UI 渲染;专用输出表面仍然延期。
  • 多块提示词绝不会成为命令候选,因此更丰富的输入框内容不会意外触发分发。
  • Web 宿主分发的命令运行在一个新建的、永远不会被中止的 AbortController 下:异步命令(注册表允许异步命令)无法从 UI 取消,session.cancel 也触及不到它(ACP 正是为此把 controller 保存在会话记录上)。
  • 单块判定意味着任何以 / 开头但不是已注册命令的单独文本块(如 /etc/hosts、/Goal)都会以 unknown-command 被拒绝,而不会到达模型——这是有意为之,与 ACP 对称,并由草稿恢复机制缓解。