技能系统
技能是按需加载的能力描述:模型在需要时把它的指令载入上下文,而不是把每个技能的全文塞进系统提示词。这就是渐进披露:尽量少占用 token,直到真正要用那一刻才展开完整内容。
DSH 的技能 seam 是
ctx.skills(@deepseek-ai/dsh-skill)。它是 host+per-scope 分层的注册表,与工具注册的 scope 模型一致。
三个观察/加载接口
| 接口 | 作用 | token 量级 |
|---|---|---|
ctx.skills.list(...) | 当前工作区合并层的技能摘要(名字排序) | 小 |
ctx.skills.snapshot({ cwd, signal, scope }) | 调用无关的 { skills, complete } 观测 | 中 |
ctx.skills.get(name, ...) | 取 完整 技能定义(发现 + 加载) | 大,按需 |
模型通常:list() 拿名字列表 → 需要时 get(name) 展开全文。isModelInvocable(skill) / isUserInvocable(skill) 决定谁可以触发。
技能来自哪里
ctx.skills 自己不知道技能在本地文件、插件内嵌数据、还是远端:provider 提供来源:
@deepseek-ai/dsh-skill-filesystem:出厂的本地文件实现ctx.skills.register(skill):把只读的运行时内嵌技能注册进当前作用域层- 任意第三方 provider 通过
ctx.skills.registerProvider(...)接入
层语义(与工具一致):全局层 vs agent-preset 层;读时合并 global 层 + 查看 scope 的链,最近层同名即胜。
技能目录布局(skill-filesystem)
本地 provider 识别两种形态,发现只深一层:目录 bundle <root>/<name>/SKILL.md,或扁平文件 <root>/<name>.md。frontmatter 里 name、description 必填,whenToUse、metadata、disable-model-invocation、user-invocable 选填,名字必须 kebab-case。
默认根按 rank 解析:
| 排名 | 来源 | 路径 |
|---|---|---|
| 100 | project-dsh | <projectRoot>/.dsh/skills |
| 200 | project-agents | <projectRoot>/.agents/skills |
| 300 | custom | Config.customSkillDirs |
| 400 | user-dsh | <dshHome>/skills |
| 500 | user-agents | <agentsHome>/skills |
项目根 = 最近的含 .git 祖先,没有则用当前 cwd;user DSH 根跳过 .system 子目录。
调用策略
SkillSummary.invocation 是必填的策略对象,两个正向布尔独立描述"模型面"与"用户面":
| 策略 | 模型 | 用户 |
|---|---|---|
{ modelInvocable: true, userInvocable: true } | 含 | 含 |
{ modelInvocable: true, userInvocable: false } | 含 | 不含 |
{ modelInvocable: false, userInvocable: true } | 不含 | 含 |
{ modelInvocable: false, userInvocable: false } | 不含 | 不含 |
ctx.skills.get() 仍是策略中立的加载原语;每个面向模型/用户的消费者都要在自己边界上执行对应谓词再暴露或加载。frontmatter 的 disable-model-invocation: true 把技能从模型目录/加载器排除,user-invocable: false 从面向人的命令排除;缺省都放行,无效拼写或非布尔值整条丢弃该技能(失效即关闭)。
运行时注册一个技能
import { Context } from '@deepseek-ai/cordis'
export const name = 'skill-demo'
export const inject = ['skills']
export function apply(ctx: Context) {
ctx.skills.register({
name: 'my-procedure',
description: '做某件事的固定流程',
// 省略 platform 等元数据
load: async () => ({
title: '我的流程',
instructions: [
'1. 先读配置文件',
'2. 再执行校验命令',
'3. 最后把结果写进会话',
],
}),
})
}
运行时技能用 rank 250:项目 provider 能覆盖它,它又能覆盖出厂本地 provider 的 custom/user 根;同层同名先到先得。
模型通过 tool-skill 调用
出厂带 @deepseek-ai/dsh-tool-skill:模型面向的技能加载工具,负责"按需展开"。
- 目录生命周期:每次 eligible 的
agent/pre-step按会话 cwd 调snapshot(),渲染排序后的name+description;首次非空给下游enter决策加一条持久的 user-role<system-reminder>初始目录,摘要变化时追加整份替换。无模型可调用技能、或skill工具被限制/遮蔽时目录省略。 skill工具:参数name(string,必填,精确 kebab-case 名)。成功返回{ name, provider, resourceBase?, content },Native 渲染成一段<skill_content>;未知名、非法名、modelInvocable: false产生不同的错误结果。- 用户显式注入:用户消息里 whitespace 界定的
/name记号命中 user-invocable 技能,就把完整<skill_content>作为user角色指令注入该步末尾。这是disable-model-invocation技能(目录与skill工具都不暴露)的唯一入口。
badge 包
@deepseek-ai/dsh-skill-badge 是可选的内嵌 provider,贡献 dsh-badge 技能:官方 "powered by dsh" Markdown 片段 + 打包 PNG(供无法可靠拉远程图的环境)。无配置,出厂 CLI 组合挂为 disabled: true,要显式启用才进目录。
事件与失效
skills/change是未过滤的失效通知:任何 provider/runtime 注册或注销、或某 provider 内部控制失效,都会触发。它不携带目录或 diff,每个消费者自己snapshot()重新抓取。- 监听器抛错/拒绝只会被记录,不能否决注册变更。
验证
# 当前工作区能列出哪些技能(通过模型对话或工具)
# 会话日志里看 skills 相关事件
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "skills" | head
配置
| 字段 | 默认 | 作用 |
|---|---|---|
collectCacheMaxEntries | 128 | 内存中缓存的 cwd/provider 目录数上限 |
skill-filesystem 的 provider 配置:
| 字段 | 默认 | 作用 |
|---|---|---|
providerName | local | 在 ctx.skills 上注册的唯一名 |
includeDefaultRoots | true | 在 customSkillDirs 之外含项目与用户根;设 false 得隔离的 custom-root provider |
customSkillDirs | [] | 额外本地技能根,扫在项目根之后、用户根之前 |
dshHome | $DSH_HOME 或 ~/.dsh | DSH 配置根,扫描其下 skills |
agentsHome | $DSH_AGENTS_HOME 或 ~/.agents | 共享 agent 配置根 |
watch | true | 监听宿主根,目录成员/frontmatter 变化时失效 provider |
watchUsePolling | false | 用 Chokidar 轮询代替原生事件 |