Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
# Conflicts: # apps/cli/tests/web-agent-presets.e2e.ts # packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
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/architecture/2026-08-09-layered-skill-registry.md
|
||||
2026-08-09-layered-skill-registry.md: d621bb3066a7347d562a439012c0f34aab63e9fe
|
||||
2026-08-09-layered-skill-registry.zh.md: 3b324d537f3d010c894ba0375ba20272e40f5fe8
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note: The skill registry is host-held and layered per scope
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-09-layered-skill-registry.zh.md)
|
||||
|
||||
## 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 by dropping the fixture's skill root](https://github.com/deepseek-harness/deepseek-harness/pull/1538). 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
|
||||
|
||||
`SkillService` 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-local` — 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-local` 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 core-web 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.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Agent Note:skill 注册表由宿主持有并按 scope 分层
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-09-layered-skill-registry.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
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 根目录绕过](https://github.com/deepseek-harness/deepseek-harness/pull/1538)。按 preset 的 realm 注册表还让网关的 skill 列表依赖存活 agent——冷会话的 `/` 弹窗根本没有注册表可读。
|
||||
|
||||
工具注册表从未有过这个问题:它是一个宿主单例,基于 `dsh-scope` 按 scope 分层,因此部署级工具(MCP 服务器、插件 entry)注册进全局层,preset 的行注册进该 preset 的层。
|
||||
|
||||
## 决定
|
||||
|
||||
`SkillService` 采用同一形态。它持有 `ScopedLayers<SkillLayer>`;`registerProvider()` 与 `register()` 落入调用方上下文 scope 对应的层——宿主行与 repository 插件落入全局层,preset 的 `skill-local`(由常驻组合挂载,其上下文携带该 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-local` 与 `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 的目录,而宿主视图保持仅全局。
|
||||
|
||||
**层可见性与消费仍是两个独立选择。**core-web 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 里重复,冷会话依旧无处可读。否决。
|
||||
Reference in New Issue
Block a user