docs(skill): document the user-explicit invocation path

Bilingual README updates for the four touched packages (ui-skill's claim
flow and deterministic-injection model experience, the apiproxy skills
domain, the shared renderSkillContent seam export, the catalog stitch
sentence), the implemented Agent Note triplet recording the decision and
its peer-product evidence, and the regenerated catalogs/graphs.
This commit is contained in:
Yichen Jiang
2026-08-08 01:52:43 +08:00
parent db146f0eba
commit 3584d8e088
19 changed files with 121 additions and 31 deletions

View File

@@ -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-08-08-user-explicit-skill-invocation.md
2026-08-08-user-explicit-skill-invocation.md: 9249ee5c9c712e9c6aa827e97178f352728ed927
2026-08-08-user-explicit-skill-invocation.zh.md: f15975c3b13fbf76e036fcece30253e78e7b417d

View File

@@ -0,0 +1,36 @@
# Agent Note: User-explicit skill invocation over skill.invoke
Status: implemented
English | [中文](2026-08-08-user-explicit-skill-invocation.zh.md)
## Problem
A `disable-model-invocation: true` skill is user-only by design: it never enters the model-facing catalog and the `skill` tool refuses to load it. Its only legitimate entry point is an explicit user gesture — yet the web client had none. `skill.list` filtered to the model-and-user intersection (hiding user-only skills from the menu), an entered `/name` line rode into the default prompt sink as plain text, and the model it reached was forbidden to load the skill — so it degraded to `read`-ing the SKILL.md file or ignoring the gesture (issue #1470). Even for ordinary skills, the decision-21 plain-text reference made user invocation a collaboration cue the model could ignore, not a guarantee.
## Decision
User-explicit invocation is a deterministic host-side injection, uniform for every user-invocable skill:
- `skill.invoke { sessionId, name, text? }` (host apiproxy) enforces user-invocation policy at the operation boundary (`skill-not-found` / `skill-not-invocable`), renders the skill with the shared `renderSkillContent`, appends the optional trailing text after a blank line, and injects the whole as one user-role message carrying the new `skill-invocation` `MessageSource` kind (`{ name, args? }`) before starting a turn through the same route-served gate as `session.prompt`.
- `renderSkillContent` moved from `dsh-tool-skill` to the `dsh-skill` seam: the `skill` tool result and the injection share one verbatim `<skill_content>` shape, and the catalog text gained the seam rule — an inline-injected skill must be followed, not re-loaded through the tool.
- `skill.list` serves every user-invocable skill and carries `modelInvocable`, so the browser menu lists user-only skills with a marker (description prefix — the `hint` field is claim-state ghost text the menu never renders).
- ui-skill claims a menu pick or an entered `/name [args]` into the invoke transaction (`matchEnter` strong-waits the catalog; unknown names stay plain prompts). The unreached legacy `<skill>name</skill>` reference codec is removed.
- The transcript materializes the injection as a dedicated `skill-invocation` node from source metadata (never re-parsed from the body) and renders a right-aligned bubble: `/name` chip, trailing text, and the injected block collapsed behind a disclosure.
Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous: user-explicit triggering is programmatic injection as a user-role message with zero model participation on every product, prompt-guided tool loading exists only on the model-autonomous track, and the disable-model-invocation equivalents gate only the model-side surfaces. Kimi's origin-metadata rendering and the Claude Code/Kimi no-reload prompt rule translate directly onto `MessageSource` and the catalog sentence.
## Alternatives considered
- **`agent.inject()` context injection** — no peer precedent; the gesture is a user turn, not an environment notice, and context-row presentation, compaction, and attribution all mismatch. Rejected.
- **A host `/skill <name>` command** (command registry, plan-mode precedent) — two-token UX, no name completion, and user-only skills stay undiscoverable in the menu; the per-cwd skill catalog also fits the static command registry poorly. Rejected.
- **Client-side expansion** (fetch body, splice into the prompt) — authorization becomes bypassable client courtesy, the log loses the invocation semantics, and Codex deleted its equivalent mechanism (custom prompts) in favor of core injection. Rejected.
- **Host prompt-pipeline scanning for `/name`** (Codex `$name` core mentions) — duplicates the adjudication layer and risks swallowing literal slashes in prose; the claim path already covers the need. Rejected.
- **Per-injection preamble line** (Kimi's `User activated the skill …`) — dropped in favor of a one-time catalog sentence: same context, paid once, and the injected block stays byte-identical with the tool result.
## Consequences
- Decision 21's plain-text reference path is superseded at submission: the draft still carries plain text and lexicon-derived chip visuals, but submit claims into a deterministic injection instead of shipping the literal and hoping. The model-autonomous track (catalog + `skill` tool) is unchanged.
- Every user-invocable skill invocation now costs its full rendered body unconditionally — the price of determinism the peer survey showed everyone pays.
- The `skill-invocation` source rides `user/message`, so Model-visible ⟺ logged holds with no new event type, and replay/UI read metadata rather than text markers.
- TUI and ACP can adopt `skill.invoke` later for the same semantics; until then the TUI's client-side expansion remains its own path.

View File

@@ -0,0 +1,36 @@
# Agent Note: 经 skill.invoke 的用户显式 skill 调用
Status: implemented
[English](2026-08-08-user-explicit-skill-invocation.md) | 中文
## 问题
`disable-model-invocation: true` 的 skill技能在设计上就是仅限用户的它绝不进入面向模型的目录`skill` 工具也拒绝加载它。它唯一正当的入口是一次显式的用户手势——而 web 客户端此前没有这个入口。`skill.list` 过滤到模型与用户的交集(把仅限用户的 skill 挡在菜单之外),回车提交的 `/name` 一行以纯文本落入默认提示词 sink而这行文本到达的模型又被禁止加载该 skill——于是退化为模型去 `read` 那份 SKILL.md 文件或者干脆无视这次手势issue #1470)。即使对普通 skill决策 21 的纯文本引用也让用户调用只是模型可以忽略的协作线索,而不是保证。
## 决策
用户显式调用是一次确定性的宿主侧注入,对每一个用户可调用的 skill 一致:
- `skill.invoke { sessionId, name, text? }`(宿主 apiproxy在操作边界强制执行用户调用策略`skill-not-found`/`skill-not-invocable`),用共享的 `renderSkillContent` 渲染该 skill在一个空行之后追加可选的尾随文本并把整体作为一条携带新增 `skill-invocation` `MessageSource` kind`{ name, args? }`)的 user 角色消息注入,随后经由与 `session.prompt` 相同的「路由是否有适配器在服务」闸门开启一个轮次。
- `renderSkillContent``dsh-tool-skill` 移入 `dsh-skill` seam`skill` 工具结果与注入共享同一份逐字一致的 `<skill_content>` 形态,目录文本则新增了这条 seam 规则——已内联注入的 skill 必须被遵循,而不是再经工具重新加载。
- `skill.list` 提供每一个用户可调用的 skill 并携带 `modelInvocable`,因此浏览器菜单会带标记地列出仅限用户的 skill描述前缀——`hint` 字段是认领态的 ghost text菜单从不渲染它
- ui-skill 把菜单 pick 或回车提交的 `/name [args]` 认领进 invoke 事务(`matchEnter` 强等目录;未知名称保持为普通提示词)。已不可达的旧 `<skill>name</skill>` 引用 codec 被移除。
- transcript文本记录依据来源元数据把这次注入物化为专用的 `skill-invocation` 节点(绝不从正文重新解析),并渲染为一个右对齐气泡:`/name` chip、尾随文本以及收在 disclosure 之后的注入块。
同类产品调研Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出结论一致在每个产品上用户显式触发都是以 user 角色消息做程序化注入、模型零参与提示词引导的工具加载只存在于模型自主轨道上disable-model-invocation 的对应物只把关模型侧表层。Kimi 的来源元数据渲染与 Claude Code/Kimi 的禁止重载提示词规则,可直接平移到 `MessageSource` 与目录那句话上。
## 考虑过的替代方案
- **`agent.inject()` 上下文注入**——没有同类产品先例这次手势是一个用户轮次不是环境通知而且上下文行呈现、压缩compaction与归属全都不匹配。否决。
- **宿主 `/skill <name>` 命令**命令注册表plan 模式先例)——两 token 的 UX、没有名称补全、仅限用户的 skill 在菜单里仍不可发现;按 cwd 的 skill 目录也与静态命令注册表格格不入。否决。
- **客户端展开**(拉取正文、拼进提示词)——授权沦为可被绕过的客户端善意,日志失去调用语义,而且 Codex 已删除其等价机制custom prompts转向核心注入。否决。
- **宿主提示词流水线扫描 `/name`**Codex 的 `$name` core mentions——重复了裁决层还有吞掉普通行文中字面斜杠的风险认领路径已经覆盖了这一需求。否决。
- **每次注入一条前导语**Kimi 的 `User activated the skill …`)——弃用,改为一次性的目录句子:同样的上下文、只支付一次,且注入块与工具结果保持逐字节一致。
## 后果
- 决策 21 的纯文本引用路径在提交处被取代:草稿仍承载纯文本与 lexicon 派生的 chip 视觉,但提交会认领进一次确定性注入,而不是把字面文本发出去再碰运气。模型自主轨道(目录 + `skill` 工具)不变。
- 每一次用户可调用 skill 的调用现在都无条件付出其完整渲染正文的成本——这是确定性的代价,同类调研表明所有产品都在支付。
- `skill-invocation` 来源搭乘 `user/message`,因此「模型可见 ⟺ 已记录」在不新增事件类型的情况下继续成立,回放与 UI 读取的是元数据而非文本标记。
- TUI 与 ACP 之后可以为同样的语义采用 `skill.invoke`在那之前TUI 的客户端展开仍是它自己的路径。