feat(skill): hot-refresh skill catalogs
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md
|
||||
2026-07-27-skill-catalog-hot-refresh.md: f818766eb55f237e21aa3da9586887e493b9de75
|
||||
2026-07-27-skill-catalog-hot-refresh.zh.md: 3f0be2e760f4a18504e18803bcb8a8e47acffa5d
|
||||
@@ -0,0 +1,46 @@
|
||||
# Agent Note: Skill catalog hot refresh
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-27-skill-catalog-hot-refresh.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Skill summaries are model routing input, but local skills can appear, disappear, or be renamed after a session starts. IDEs, Git operations, shell commands, and other processes can all mutate `.agents/skills` without going through the harness filesystem tools. A startup-only catalog leaves the model unaware of new skills and able to call deleted names. Treating every instruction-body edit as a catalog revision would instead couple progressive loading to unnecessary prompt churn.
|
||||
|
||||
Filesystem updates are also non-atomic from the observer's perspective. An editor or Git operation may briefly remove a file, a watched root may not exist at startup, and discovery may fail transiently. Publishing those intermediate observations as authoritative empty catalogs would be worse than retaining the last complete view.
|
||||
|
||||
## Decision
|
||||
|
||||
The skill capability separates catalog membership from instruction-body loading. `ctx.skills.snapshot()` returns summaries plus a completeness bit, while `ctx.skills.invalidateProvider(provider)` dirties only the exact registered provider and discards completed catalog caches. A provider or runtime generation change during discovery retries before returning. Incomplete observations are not cached. A stale provider callback after disposal or replacement is a no-op because invalidation uses object identity.
|
||||
|
||||
`@deepseek-ai/dsh-skill-local` directly depends on Chokidar and observes catalog-relevant host paths. Existing roots watch direct skill bundle directories, flat Markdown entries, and direct `SKILL.md` entry files. Additions, removals, and directory changes invalidate membership; file changes support frontmatter `name` and `description` refresh. Resource files below a bundle are ignored. Events in one microtask batch coalesce to one invalidation. Project watchers use a bounded least-recently-observed set.
|
||||
|
||||
A missing root is followed from its nearest existing ancestor one absent segment at a time with `fs.watchFile`, then handed to Chokidar once the real root exists. Deleting a root re-establishes ancestor observation. Chokidar configuration exposes native-versus-polling mode, write stability, polling interval, symlink following, and project watcher capacity. First-party `write` and `edit` tool observations synchronously invalidate a relevant provider, so the next model step sees its own mutation without waiting for host delivery. Watch startup/runtime failures make discovery incomplete and retry; teardown closes watchers and ignores late callbacks.
|
||||
|
||||
`@deepseek-ai/dsh-tool-skill` keeps the initial complete catalog in `agent/session-prefix`. Before every model step it computes a digest over exact `skill` tool visibility and the ordered rendered names and descriptions. A changed digest appends a durable, complete replacement catalog through `agent.inject()`, including an explicit empty catalog when all skills disappear. The logged message carries `{ kind: 'skill-catalog', version: 1, digest }`, so a still-visible replacement supplies the baseline across replay or plugin reload. If compaction shadows it, the next pre-step falls back to the loop's initial-prefix baseline and re-establishes the current catalog when needed. An incomplete snapshot emits no replacement and preserves the last-good model view.
|
||||
|
||||
The TUI consumes the same invalidation as presentation state, not session history. `skills/change` carries no diff; the TUI refetches `snapshot()` for the active session cwd, applies only the latest complete result, and retains the previous commands across incomplete observations. A complete empty result clears stale completions. Because pi-tui closes autocomplete when its provider is replaced, a catalog that arrives while the user is typing a slash-command name also triggers a suggestion-only re-query of the current draft.
|
||||
|
||||
Instruction bodies keep progressive disclosure. Every `skill(name)` call asks the provider to reread and parse the current file; there is no body cache, hash, revision, or proactive notification. Previously logged tool results remain unchanged. If the loaded frontmatter name no longer matches the selected candidate, the registry rejects the stale name and invalidates that provider so a later catalog observation can publish the new name.
|
||||
|
||||
## Verification
|
||||
|
||||
Registry tests pin exact invalidation, contained observer failures, incomplete snapshots, generation retries, and stale-name rejection. Local-provider tests cover bundle and flat-file creation, removal, rename, root creation/deletion/recreation, description changes, body-only edits, first-party observation, symlinks, polling options, watcher failures, event coalescing, bounded projects, teardown, and transient reads. Tool tests pin full replacement messages, empty tombstones, digest stability for body-only edits, incomplete-state retention, visibility, and resume metadata. TUI tests pin last-complete retention, authoritative empty removal, latest-wins refresh, teardown, and the already-open slash-draft race; a real Loader/PTY smoke adds a local skill after startup and observes its completion without restarting. A keyless assembled agent-spine snapshot creates a project skill through model-facing filesystem tools, observes its replacement catalog on the next request, and loads its current body with the real `skill` tool.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Put the live catalog in World State** — rejected because catalog replacements are model-visible session inputs and must be reconstructable from the event log. Durable injected history already provides replay, resume, fork, and compaction semantics without another mutable state plane.
|
||||
- **Rely only on `fs/observed`** — rejected because IDEs, Git, shell commands, and external processes do not cross that seam. The event remains a latency fast path for first-party tools, while host watching supplies coverage.
|
||||
- **Hash or version every `SKILL.md` body** — rejected because the model initially sees only names and descriptions, and the provider already rereads the body on each tool call. Body revisions would create catalog traffic without changing routing and would not justify rewriting historical tool results.
|
||||
- **Watch every bundle resource** — rejected because references, scripts, and assets are loaded on demand and do not affect the category list. Broad recursive watching would add invalidations, descriptor pressure, and platform variability without improving routing.
|
||||
- **Publish partial or failed discovery as the new catalog** — rejected because a transient read failure is not evidence of deletion. The completeness bit lets the model-facing consumer preserve its last-good catalog until a full observation succeeds.
|
||||
|
||||
## Consequences
|
||||
|
||||
- New, deleted, and renamed local skills become visible at model-step boundaries without restarting the agent, including when the skills root did not exist at startup.
|
||||
- The TUI's `/skill:` completions converge on the same complete catalog without blocking each keystroke on discovery; an open slash-name draft refreshes when the catalog arrives.
|
||||
- Catalog updates are append-only, logged, and whole-list replacements. They preserve the stable initial prefix and retire stale names explicitly, at token cost proportional to the current catalog on each actual digest change.
|
||||
- Body-only edits produce no catalog message. A subsequent tool call sees current content, while prior tool results remain an accurate record of what the model previously loaded.
|
||||
- Missing-root polling and Chokidar add one maintained runtime dependency, host watcher resources, bounded detection latency, and deployment tunables. The bounded project set and teardown contract contain those costs.
|
||||
- Remote or future mutable providers remain responsible for calling `invalidateProvider()` from their own observation mechanism; the registry does not impose a universal watcher or TTL.
|
||||
@@ -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。
|
||||
Reference in New Issue
Block a user