技能(skill)是可复用的、针对特定任务的指令包,智能体可以按需加载,这样模型就不必把每套流程都常驻在上下文窗口里。技能既不是代码、会话事件,也不是 World State 事实——它是一段 Markdown 指令正文,初始为空,只在被调用时注入到存活智能体的某一轮 turn 中。这条链路全部位于 packages/skill/*,并有意识地排除在核心 agent-loop 主链之外,因此提供方可以是本地的、内嵌的或远程的,而不会改变模型所看到的内容。
| 包 | 角色 | ctx key |
|---|---|---|
packages/skill/skill | 服务定义:分层提供方注册表(ctx.skills) | ctx.skills |
packages/skill/skill-filesystem | 本地提供方:扫描磁盘上的项目/用户根目录 | 注册到 ctx.skills |
packages/skill/skill-badge | 可选的 dsh-badge 内置提供方 | 注册到 ctx.skills |
packages/skill/tool-skill | 消费方:会话目录 + 面向模型的 skill 工具 | 注册到 ctx.tools |
packages/client/ui-skill | Web UI:技能加载的紧凑转录行 | 仅浏览器端 |
官方子系统参考见 docs/subsystems/skills.md;源码见 packages/skill/{skill,skill-filesystem,skill-badge,tool-skill}/src/index.ts。
技能是什么,如何声明
技能是一个带 YAML frontmatter 的 Markdown 文件。文件系统提供方接受两种磁盘形态(packages/skill/skill-filesystem/src/index.ts):
- 目录包(directory bundle)——
<skill-name>/SKILL.md; - 扁平文件——
<skill-name>.md(单个 Markdown 文件)。
技能名必须为 kebab-case(^[a-z0-9]+(?:-[a-z0-9]+)*$)。不支持递归 **/SKILL.md 发现——提供方只扫描一层。
frontmatter 契约极其精简(缺少 YAML、或缺少 name/description,该文件会被跳过并给出警告):
---
name: my-skill
description: 在目录中展示的简短路由描述。
# 可选:
whenToUse: 额外的路由指引(在渲染它的消费方中展示)。
disable-model-invocation: false # modelInvocable
user-invocable: true # 用户可用 /<name> 触发
metadata-key: any-extra-field # 作为不透明 metadata 保留
---
实际的指令正文写在这里(Markdown)。两个 frontmatter 键映射到调用策略(invocation policy)(SkillInvocationPolicy):disable-model-invocation(默认 false → 模型可调用)与 user-invocable(默认 true)。两者都以精确的 kebab-case 布尔值读取;旧的 camelCase 键(disableModelInvocation、modelInvocable、userInvocable)会被拒绝,并提示应使用的规范键。把 disable-model-invocation 设为 true、user-invocable 设为 false,则该技能只能通过受信任的 ctx.skills.get() 调用方触达。
注册表:ctx.skills
SkillRegistry(packages/skill/skill/src/index.ts)是中立于提供方的目录。提供方在 apply() 期间同步注册;发现逻辑在 list() 内被 await。注册表是分层的 host + per-scope:注册会落入其调用上下文所处 scope 的那一层,因此全局行与仓库插件落在全局层,而由智能体 preset 挂载的插件落在该 preset 的层。一次读取会合并全局层与查看智能体的链——最近的层的条目直接赢得重名。
在同一层内,重名的技能按rank、然后提供方顺序、再本地顺序解析。注册表的公开 ID:
registerProvider(create) register(skill)
list(options) -> Promise<SkillSummary[]>
snapshot(options) -> Promise<SkillCatalogSnapshot>
get(name, options) -> Promise<SkillDefinition | undefined>三个结果形态从 summary → candidate → definition 逐级扩展:
SkillSummary——与调用无关的元数据(name、description、可选whenToUse、invocation、source、provider、resourceBase)。目录渲染的就是它。SkillCandidate——summary 加上rank与透明的locator(提供方所有),注册表存储它,并在get()时交还给胜出的提供方。SkillDefinition——candidate 加上content(Markdown 正文),由get()返回。
Snapshot 区分权威缺失与瞬时失败:只有当每个注册的提供方都在没有并发目录修订的情况下完成时 complete 才为 true,且不完整的快照绝不缓存。查找对 cwd 敏感(工作区本地技能),并可通过 signal 取消。任何提供方/运行时变更后,注册表都会发出不带 diff 的 skills/change 失效事件——消费方据此重新 snapshot()。
本地发现优先级
dsh-skill-filesystem 按 rank 顺序扫描根目录。项目根是含有 .git 的最近祖先(当存在时通过 ctx.fs 探测,使沙箱/远程工作区保持一致),否则用 cwd:
| Rank | 来源 | 根 |
|---|---|---|
| 100 | project-dsh | <projectRoot>/.dsh/skills |
| 200 | project-agents | <projectRoot>/.agents/skills |
| 300 | custom | Config.customSkillDirs |
| 400 | user-dsh | <dshHome>/skills(跳过 .system 子目录) |
| 500 | user-agents | <agentsHome>/skills |
| 600 | bundled | 配置时使用 Config.bundledSkillDir |
dshHome 解析为 $DSH_HOME 或 ~/.dsh;agentsHome 解析为 $DSH_AGENTS_HOME 或 ~/.agents。Chokidar 监视现有根目录的扁平/包条目新增、移除与直接条目变更;项目级监视器使用有界 LRU(watchMaxProjects,默认 128)。面向模型的 write/edit 观察会同步使提供方失效,而 host 监视器则覆盖 IDE/Git/外部变更。监视失败会把观察标记为不完整,但不会对直接加载隐藏可读候选。
skill-badge
dsh-skill-badge 是一个提供方,而非带行为的注册表插件——它注册一个固定不变的、名为 dsh-badge 的 bundled 候选(为文档、PR/MR 描述及其他产出内容添加官方 "powered by dsh" 署名徽章),rank 为 BUNDLED_SKILL_RANK = 600,并把打包的 assets/ 目录作为 resourceBase 暴露。随附 CLI 声明该插件禁用,因此启用其组合行是显式 opt-in。get() 从 ../assets/dsh-badge.md 读取正文。
会话目录与 skill 工具
dsh-tool-skill 在存活会话首次 agent/pre-step 观察到非空、完整视图时,把初始目录作为一条持久的 <system-reminder> 用户消息注入:
<system-reminder>
A skill is a reusable set of task-specific instructions. The following skills are available in this session:
<available_skills>
- `skill-name`: description
</available_skills>
If the user names a skill ... call the `skill` tool with the exact skill name before taking task actions...
</system-reminder>这些目录是会话历史,而非 World State——它们以 source.kind === 'skill-catalog' 消息进入(一条 catalog 形式的消息,同时记录其精确 entries 供非模型消费方使用)。在后续每一步之前,插件对渲染条目做摘要(对每个 [name, description] 的 JSON 做 SHA-256);摘要变化时向该 step 的 enter 决定追加一条全量替换目录(另一条 skill-catalog 消息),删除全部技能则追加一条显式空替换。目录只携带模型可调用的 name + 限长的 description——绝无正文、路径或提供方。这部分就是喂给系统提示组装的地方:技能作为可调用指令与系统提示并列浮现,而非混入其中。
面向模型的 skill 工具(name: 'skill',一个参数 name)校验 kebab-case 名称,在与调用无关的目录中查找,并在 isModelInvocable(skill) 不允许时加以拒绝。随后针对调用智能体的 cwd 重读当前定义并返回:
{ "name": "...", "provider": "...",
"resourceBase": {"kind": "directory", "path": "..."},
"content": "完整的 markdown 正文" }每次调用都会加载——注册表从不缓存定义,因此仅正文的修改会改变之后的调用,而不会产生目录消息。resourceBase 只按需解析相对脚本/资源。还有第二个入口点:文本任意位置出现一个以空白为界的 /<skill-name> token 的用户消息(匹配用户可调用技能)是一个确定性的加载手势——正文在所有其他注入之后、最接近答案处注入。目录与 skill 工具永远看不到 disable-model-invocation 技能;用户手势是它们的唯一路径。
配置摘要
| 包 | 键 | 默认 / 说明 |
|---|---|---|
dsh-skill | collectCacheMaxEntries | 缓存的已完成的 cwd/提供方目录最大数 |
dsh-skill-filesystem | providerName、includeDefaultRoots、dshHome、agentsHome、customSkillDirs | 提供方名默认 filesystem;默认开启根目录 |
dsh-skill-filesystem | watch*、watchUsePolling、watchStabilityThresholdMs (200)、watchPollIntervalMs (100)、watchMaxProjects (128)、watchFollowSymlinks | 监视器调优 |
dsh-skill-filesystem | bundledSkillDir | 默认 $DSH_BUNDLED_SKILL_DIR |
dsh-skill-badge | (无——固定候选) | 随附 CLI 中禁用 |
dsh-tool-skill | catalogDescriptionMaxLength | 默认 500,最小 3 |
SkillConfig 包(由配置目录转发而来)把 enabled、registry、filesystem、tool 子配置合并到一个开关里。
UI:packages/client/ui-skill
该 client 包是纯 UI 插件——其 Node apply() 为空;浏览器端经包的 exports["./client"] 输送。SkillRow.tsx 在 toolview 插槽上注册一条紧凑的转录行:它仅从持久化的调用切片(绝不查询实时目录)派生显示状态(running/ok/error/stopped),从调用参数取技能名,并把确切加载的输出留在有界展开卡中。本地化字符串位于 skill 命名空间下(client/locales.ts)。
延伸阅读
- 系统提示组装——目录与 reminder 如何编入模型上下文
- 智能体循环与作用域——
agent/pre-step与分层注册表 - 工具注册表与执行流水线——
ctx.tools定义与 post-execute 钩子如何运行 docs/subsystems/skills.md——官方子系统参考与完整 APIpackages/skill/skill/src/index.ts——SkillRegistry服务定义packages/skill/tool-skill/src/index.ts——目录注入与skill工具