feat(skill): hot-refresh skill catalogs

This commit is contained in:
Yichen Jiang
2026-07-27 16:50:56 +08:00
parent 79eb3a9035
commit b76659aa57
46 changed files with 2372 additions and 153 deletions
@@ -0,0 +1,46 @@
# Agent Note: Skill 目录热刷新
Status: implemented
[English](2026-07-27-skill-catalog-hot-refresh.md) | 中文
## 问题
skill(技能)摘要是模型的路由输入,但本地 skill 可在会话启动后新增、消失或重命名。IDE、Git 操作、shell 命令和其他进程都可以修改 `.agents/skills`,而不经过 harness 文件系统工具。仅在启动时构建目录,会让模型无法获知新 skill,并且仍能调用已删除的名称。反之,如果把每次指令正文编辑都视为目录修订,就会让渐进式加载与不必要的提示词频繁变化耦合。
从观察方来看,文件系统更新也不是原子完成的。编辑器或 Git 操作可能会短暂移除文件,受监视的根目录在启动时可能不存在,发现也可能暂时失败。把这些中间观察结果发布为权威空目录,比保留最后一个完整视图更糟。
## 决策
skill 服务将目录成员关系与指令正文加载分离。`ctx.skills.snapshot()` 返回摘要及一个完整性位;`ctx.skills.invalidateProvider(provider)` 只会将精确的已注册提供方标记为脏,并丢弃已经完成的目录缓存。在发现期间,如果提供方或运行时 generation 发生变化,系统会先重试再返回。不完整的观察结果不会缓存。提供方在资源释放或被替换后到达的陈旧回调不会执行任何操作,因为失效操作使用对象身份。
`@deepseek-ai/dsh-skill-local` 直接依赖 Chokidar,并观察与目录相关的宿主路径。已有根目录会监视其直属 skill bundle 目录、平铺的 Markdown 条目和直属 `SKILL.md` 条目文件。新增、移除和目录变更会使成员关系失效;文件变更还支持刷新 frontmatter 中的 `name` 和 `description`。bundle 内更深层的资源文件会被忽略。同一微任务批次中的事件会合并为一次失效。项目 watcher 使用有界集合,并按最久未观察顺序淘汰。
系统从缺失根目录最近的现有祖先开始,使用 `fs.watchFile` 每次跟进一层缺失路径片段;真实根目录出现后,再交给 Chokidar。删除根目录后,系统会重新建立祖先观察。Chokidar 配置公开原生事件或轮询模式、写入稳定性、轮询间隔、符号链接跟随选项和项目 watcher 容量。第一方 `write` 和 `edit` 工具观察会同步使相关提供方失效,因此下一个模型步骤无需等待宿主事件投递,就能看到自身改动。watcher 启动或运行失败会使发现结果不完整并触发重试;资源销毁会关闭 watcher,并忽略延迟回调。
`@deepseek-ai/dsh-tool-skill` 将初始完整目录保存在 `agent/session-prefix` 中。每个模型步骤开始前,它都会针对 `skill` 工具的精确可见性,以及按顺序渲染的名称和描述计算 digest。digest 变化时,插件通过 `agent.inject()` 追加一份持久的完整替换目录;所有 skill 消失时,也会追加显式空目录。记录的消息携带 `{ kind: 'skill-catalog', version: 1, digest }`。恢复后,最新且仍可见的替换是比较基线;如果压缩(compaction)遮蔽了替换消息,模型步骤前的观察会改以 `agent/session-prefix` 为基线,并在必要时重新发布当前完整目录。不完整的快照不会产生替换,并会保留最后一次完整的模型视图。
TUI 将同一失效通知作为界面状态而非会话历史来消费。`skills/change` 不携带 diff;TUI 会为活动会话的 cwd 重新获取 `snapshot()`,仅应用最新的完整结果,并在观测不完整时保留先前命令。完整的空结果会清除陈旧补全项。pi-tui 在其提供方被替换时会关闭自动补全,因此如果目录在用户输入斜杠命令名称期间到达,还会触发一次仅用于更新建议的当前草稿重查。
指令正文继续采用渐进式披露。每次调用 `skill(name)` 时,系统都会要求提供方重新读取并解析当前文件;不存在正文缓存、哈希、修订或主动通知。先前记录的工具结果保持不变。如果加载后的 frontmatter 名称不再匹配所选候选项,注册表会拒绝这个陈旧名称,并使该提供方失效,以便后续目录观察发布新名称。
## 验证
注册表测试固定了精确失效、监听器失败隔离、不完整快照、generation 重试和陈旧名称拒绝。local-provider 测试覆盖 bundle 与平铺文件的创建、移除和重命名,以及根目录创建/删除/重建、描述变更、仅正文编辑、第一方观察、符号链接、轮询选项、watcher 失败、事件合并、项目 watcher 容量上限、资源销毁和暂时读取。工具测试固定了完整替换消息、空 tombstone、仅修改正文时 digest 稳定、不完整状态保留、可见性和恢复元数据。TUI 测试固定了上一份完整结果保留、权威空结果清除、刷新时以最新结果为准、资源销毁和已打开斜杠草稿的竞态;一项使用真实 Loader/PTY 的 smoke 测试会在启动后添加本地 skill,并观察其补全项出现,而无需重启。一个无密钥、装配完成的 agent-spine 快照测试通过面向模型的文件系统工具创建项目 skill,观察下一次请求中的替换目录,并使用真实 `skill` 工具加载当前正文。
## 考虑过的替代方案
- **将实时目录放入 World State**:不予采纳,因为目录替换是模型可见的会话输入,必须能够从事件日志重建。持久注入历史已经提供回放、恢复、fork 和压缩语义,无需再引入一套可变状态层。
- **只依赖 `fs/observed`**:不予采纳,因为 IDE、Git、shell 命令和外部进程都不会经过该 seam。该事件仍作为第一方工具的低延迟快速路径,宿主监视则补齐覆盖。
- **为每个 `SKILL.md` 正文计算哈希或版本**:不予采纳,因为模型最初只看到名称和描述,提供方已经在每次工具调用时重新读取正文。正文修订会产生目录流量,却不会改变路由,也不足以成为改写历史工具结果的理由。
- **监视每个 bundle 资源**:不予采纳,因为参考资料、脚本和产物都是按需加载的,不影响类别列表。宽泛的递归监视会增加失效、描述符压力和平台差异,却不能改善路由。
- **将部分发现或失败发现发布为新目录**:不予采纳,因为暂时读取失败不能证明文件已删除。完整性位让面向模型的消费方保留最后一次完整目录,直到完整观察成功。
## 影响
- 新增、删除和重命名的本地 skill 会在模型步骤边界变得可见,无需重启 agent(智能体),即使 skill 根目录在启动时不存在也一样。
- TUI 的 `/skill:` 补全会收敛到同一份完整目录,而不会让每次按键都阻塞于发现;打开的斜杠命令名称草稿会在目录到达时刷新。
- 目录更新采用仅追加、日志记录和全量列表替换。它们会保持稳定的初始前缀,并显式停用陈旧名称;每次 digest 实际变化时,token 成本与当前目录大小成正比。
- 仅修改正文不会产生目录消息。后续工具调用会看到当前内容,而先前工具结果仍准确记录模型之前加载的内容。
- 缺失根目录轮询和 Chokidar 引入一个有人维护的运行时依赖、宿主 watcher 资源、有界检测延迟和部署可调参数。有界项目集合与资源销毁契约会限制这些成本。
- 远程或未来的可变提供方仍有责任通过自身观察机制调用 `invalidateProvider()`;注册表不会强制采用通用 watcher 或 TTL。