The skill registry is host-held and layered per scope
skill 注册表由宿主持有并按 scope 分层
The agent-preset stack moved the whole skill capability — registry, local provider, and the `skill` tool — into each preset's `isolate` realm, because "which skills an agent has" is an agent-plane choice. That framing conflated two different questions: which skills a *deployment* supplies, and whether an *agent* consumes them. A repository plugin's prepared wrapper declares `inject: ['skills']` and mounts its skill r
English
Problem
The agent-preset stack moved the whole skill capability — registry, local provider, and the skill tool — into each preset's isolate realm, because "which skills an agent has" is an agent-plane choice. That framing conflated two different questions: which skills a deployment supplies, and whether an agent consumes them. A repository plugin's prepared wrapper declares inject: ['skills'] and mounts its skill root as a host-plane provider; with no host registry composed in the web and headless profiles, that wrapper waited forever and the repository-plugin e2e hung, which was bypassed at the time by dropping the fixture's skill root. A per-preset realm registry also made the gateway's skill listing depend on a live agent — a cold session's / popup had no registry to read at all.
The tools registry never had this problem: it is one host singleton layered per scope over dsh-scope, so deployment-level tools (MCP servers, plugin entries) register globally while a preset's rows register into that preset's layer.
Decision
SkillRegistry adopts the same shape. It holds ScopedLayers<SkillLayer>; registerProvider() and register() file into the layer of the calling context's scope, so host rows and repository plugins land in the global layer while a preset's skill-filesystem — mounted by the standing composition, whose context carries the preset's scope key — lands in that preset's layer. Provider names are unique per layer rather than process-wide, which is what lets every preset mount its own local provider.
Reads take the viewing scope through SkillViewOptions (the calling agent, which is its own scope key). The registry merges the global layer with the scope's chain: the nearest layer wins a duplicate name outright, and rank decides duplicates only within one layer — the tools registry's shadowing rule. Rank-pooling across layers was considered and rejected: ranks were designed to order sources that know about each other, and under a global pool a later-installed repository plugin could silently displace a preset's own same-named skill by registration-order tiebreak, changing a preset's behavior remotely. Nearest-wins keeps a composition's behavior decided by its author.
Discovery caches are keyed by the resolved scope chain plus one revision counter, so a blank-session recompose — which re-parents the agent's scope key without touching the registry — is visible to the next read.
The composition moves with it: the web-app bundle re-enables the base skill registry row (only skill-filesystem and tool-skill stay preset-owned), and preset compositions drop their isolate: skills realm for bare rows over the host registry. The gateway's skills domain reads the host registry in the presenter scope — the live agent, else the recorded preset's standing key — so a cold session lists the catalog its composition actually serves instead of failing; the serviceFor branch stays for compositions that still realm-mount their own registry.
Consequences
A deployment-level skill reaches every preset-composed session that mounts tool-skill. The repository-plugin e2e's skill root and assertions are restored; the shipped-Web e2e proves the badge row (the same host-registration shape) merges into a standard-preset agent's catalog while the host view stays global-only.
Layer visibility and consumption stay separate choices. A minimal agent can read the global layer in principle, but composes no skill tool — whether an agent has skills at all remains the preset's decision, made by mounting or omitting tool-skill.
Provider options are still the borrowed caller object. SkillViewOptions extends SkillLookupOptions; the registry consumes scope and providers read only their own contract from the same readonly object, preserving the existing borrow-identity guarantee.
The TUI profile is unaffected. With every row at host, there is exactly one (global) layer and the merged view equals the old single-registry view, ranks and all.
Shadowing across layers is silent. Within a layer the loser is logged as before; a nearer layer replacing a farther name follows the tools registry's convention and logs nothing. The registry still exposes no API to inspect shadowed definitions.
Alternatives considered
Rank pool across all visible layers. Faithful to the single-registry precedence, but cross-layer ties break on registration order (boot-time providers always beat standing mounts), and a preset's own skill could be displaced by a deployment change it never sees. Rejected for composition stability; see Decision.
Keep per-preset realm registries and deliver repository skills as directories a preset's provider scans. Leaves the wrapper's inject: ['skills'] contract broken (or forks the wrapper per profile), duplicates discovery configuration into every preset, and still gives cold sessions nothing to read. Rejected.
中文
问题
agent-preset stack 曾把整个 skill 能力——注册表、本地提供方和 skill 工具——搬进每个 preset 的 isolate realm,理由是"agent 拥有哪些 skill"属于 agent 平面的选择。这一框架混淆了两个不同的问题:部署供给哪些 skill,与agent是否消费它们。repository 插件的 prepared wrapper 声明 inject: ['skills'] 并把它的 skill 根目录挂载为宿主平面的提供方;web 与 headless profile 不再组合宿主注册表后,该 wrapper 永远等待,repository-plugin e2e 因而挂死,当时通过删掉 fixture 的 skill 根目录绕过。按 preset 的 realm 注册表还让网关的 skill 列表依赖存活 agent——冷会话的 / 弹窗根本没有注册表可读。
工具注册表从未有过这个问题:它是一个宿主单例,基于 dsh-scope 按 scope 分层,因此部署级工具(MCP 服务器、插件 entry)注册进全局层,preset 的行注册进该 preset 的层。
决定
SkillRegistry 采用同一形态。它持有 ScopedLayers<SkillLayer>;registerProvider() 与 register() 落入调用方上下文 scope 对应的层——宿主行与 repository 插件落入全局层,preset 的 skill-filesystem(由常驻组合挂载,其上下文携带该 preset 的 scope key)落入该 preset 的层。提供方名称在每层内唯一而非进程级唯一,这正是让每个 preset 都能挂载自己的 local 提供方的前提。
读取通过 SkillViewOptions 携带观察 scope(调用中的 agent,agent 本身就是自己的 scope key)。注册表将全局层与该 scope 的链合并:最近层直接赢得重名,rank 只在单层内裁决重名——即工具注册表的遮蔽规则。曾考虑跨层 rank 合池并予以否决:rank 的设计前提是各来源彼此知情;在全局池下,后安装的 repository 插件可能凭注册顺序平手规则静默顶掉 preset 自带的同名 skill,远程改变 preset 的行为。最近层优先让组合的行为由其作者决定。
发现缓存以解析后的 scope 链加一个修订计数为键,因此空会话重组——只重设 agent scope key 的父级、不触碰注册表——对下一次读取立即可见。
组合随之调整:web-app bundle 重新启用 base 的 skill 注册表行(只有 skill-filesystem 与 tool-skill 仍归 preset),preset 组合拆掉 isolate: skills realm,改为直接落在宿主注册表上的平铺行。网关的 skills 域以 presenter scope 读取宿主注册表——存活 agent,否则记录在案的 preset 的 standing key——冷会话由此列出其组合真正供给的目录而不再报错;serviceFor 分支保留,兼容仍以 realm 自挂注册表的组合。
影响
**部署级 skill 会到达每个挂载 tool-skill 的 preset 会话。**repository-plugin e2e 的 skill 根目录与断言已恢复;shipped-Web e2e 证明 badge 行(同一种宿主注册形态)汇入 standard preset agent 的目录,而宿主视图保持仅全局。
层可见性与消费仍是两个独立选择。 minimal agent 原则上可读全局层,但不组合 skill 工具——agent 是否拥有 skill 依旧由 preset 通过挂载或省略 tool-skill 决定。
提供方选项仍是借用的调用方对象。SkillViewOptions 扩展 SkillLookupOptions;注册表消费 scope,提供方只从同一个只读对象中读取自己的契约,保持既有的借用恒等保证。
**TUI profile 不受影响。**所有行都在宿主时只有一个(全局)层,合并视图等于旧的单注册表视图,rank 行为不变。
**跨层遮蔽是静默的。**层内败者照旧记录日志;较近层顶替较远层的名称沿用工具注册表的惯例,不记录。注册表仍不提供检查被遮蔽定义的 API。
曾考虑的替代方案
**跨全部可见层的 rank 合池。**忠实于单注册表的优先级,但跨层平手按注册顺序裁决(启动期提供方永远赢过常驻挂载),preset 自带 skill 可能被它看不见的部署变更顶掉。因组合稳定性否决;见"决定"。
**保留按 preset 的 realm 注册表,把 repository skill 作为目录交给 preset 的提供方扫描。**wrapper 的 inject: ['skills'] 契约仍然破损(或者按 profile 分叉 wrapper),发现配置在每个 preset 里重复,冷会话依旧无处可读。否决。