Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Skill 处理流程
仓库里有两套并存实现:
- V1:
packages/opencode/src/skill/(index.ts、discovery.ts)+packages/opencode/src/tool/skill.ts - V2:
packages/core/src/skill.ts、packages/core/src/skill/discovery.ts、packages/core/src/skill/guidance.ts+packages/core/src/tool/skill.ts
V2 用 Effect Service + 注入 SystemContext,是后续主线。整体走"发现 → 注册到 System Context → 通过 skill 工具按需加载"三段式。
1. 数据模型
Info(统一描述):
name/description/location(SKILL.md绝对路径)/content(markdown 正文,frontmatter 已剥离)
V2 额外抽象 Source 联合:directory / url / embedded,通过 Source.equals / Source.key 做去重(packages/core/src/skill.ts:14-46)。
2. 发现(Discovery)
V1 入口 discoverSkills(packages/opencode/src/skill/index.ts:173),扫盘顺序:
- 全局外部目录:
~/.claude/skills/**/SKILL.md、~/.agents/skills/**/SKILL.md- 受
--disable-external-skills、--disable-claude-code-skills控制
- 受
- 项目向上递归:从当前目录走到
worktree根,查相同的.claude/.agents - opencode 自身配置目录里的
{skill,skills}/**/SKILL.md opencode.json的skills.paths:支持~/、相对路径skills.urls:交给Discovery.pull(url)把远程index.json描述的 skill 包下载到Global.Path.cache/skills/<name>/
远程拉取的安全校验(V2,packages/core/src/skill/discovery.ts)
isSafeSegment/isSafeRelativePath防路径穿越- 每个文件落地前用
FSUtil.contains(root, dest)二次校验必须在该 skill 根目录里 - URL origin 必须与源一致
- 缓存目录用
Bun.hash(base).toString(16)命名,已存在则跳过
3. 加载与去重
add() 用 ConfigMarkdown.parse 解析 frontmatter:
- V1 要求
name: string - V2 允许从文件名/目录推断
name:*.md顶层文件用basename,子目录里只接受SKILL.md
行为:
- 同名 skill 输出
Effect.logWarning("duplicate skill name", ...),后扫到的覆盖先扫到的 - 解析失败通过
EventV2Bridge发Session.Event.Error让 UI 看见,但不阻塞启动 - 内置 skill
customize-opencode在磁盘扫描之前先注册(skill/index.ts:278-283),磁盘同名 skill 可以覆盖它——目的是防止模型瞎写opencode.json触发硬失败
V2 list(skill.ts:140)维护 Map<Source.key, Info[]> 内存缓存;目前没有 fs watcher,源码里 QUESTION(Dax) 标注待办。
4. 实例状态
V1 Skill.layer 把 discovered 和 state 都包进 InstanceState.make(skill/index.ts:259-287):
每个工作目录(
ctx.directory+ctx.worktree)有自己的 skill 集合
符合 packages/opencode/AGENTS.md 里 "两个并存目录不能共享一个 service" 规则。所有读取(get / require / all / available)都走 InstanceState.get(state)。
5. 权限过滤
available(agent) 用 Permission.evaluate("skill", skill.name, agent.permission) 过滤,只保留 action !== "deny"。
V2 SkillV2.available(skill.ts:57)逻辑相同,是 Agent + Permission 系统的 hook 点。
6. 注入 System Context(V2 SkillGuidance)
packages/core/src/skill/guidance.ts 是把 skill 接入对话上下文的关键:
-
注册为
SystemContext.Source,key = "core/skill-guidance" -
load返回当前 agent 可见 skill 的{name, description}列表 -
baseline(skills)渲染为:Skills provide specialized instructions ... <available_skills> <skill><name>...</name><description>...</description></skill> ... </available_skills> -
只导出 name + description,body 不出现。对应
CONTEXT.md:skill bodies and locations are exposed only through the permission-checked
skilltool -
skill 集合变化时:
update渲染 "available skills have changed. This list supersedes ..."removed渲染 "skill guidance is no longer available"- 这些作为 Mid-Conversation System Message 在下一个 Safe Provider-Turn Boundary 注入历史
-
agent 切换(
AgentV2.Selection)触发新的 Context Epoch,guidance 按新 agent 重新过滤
7. skill 工具:按需读取正文
模型实际拿到 skill 内容必须经过 skill 工具,两套实现语义一致。
V2(packages/core/src/tool/skill.ts)
-
skills.list()取当前可见集合,按 name 匹配 -
permission.assert({ action: "skill", resources: [skill.name], save: [skill.name], ... })— 真正的授权点;save让用户可勾选 "always allow this skill" -
若是
SKILL.md形态,glob 同目录其它文件,最多FILE_LIMIT = 10个采样 -
toModelOutput打包成固定 schema 的文本:<skill_content name="..."> # Skill: ... <content> Base directory for this skill: file://... <skill_files> <file>...</file> </skill_files> </skill_content>
V1(packages/opencode/src/tool/skill.ts)
用 ripgrep.find 取文件清单(pattern: "!**/SKILL.md",limit: 10);
ctx.ask({ permission: "skill", patterns, always }) 弹权限;输出格式相同。
8. 实际下发给 LLM 的完整 prompt 形态
V1 渲染入口
SystemPrompt.skills(agent):packages/opencode/src/session/system.ts:94-106,内部调用Skill.fmt(list, { verbose: true })Skill.fmt:packages/opencode/src/skill/index.ts:330-355- 注释(
system.ts:102-103)明确策略:详细描述放 system prompt,工具描述只留指针——经过实验确认 agent 这样吸收得更好
V1 的 skills 段(系统消息里的一段字符串)
按 name 升序拼接:
Skills provide specialized instructions and workflows for specific tasks.
Use the skill tool to load a skill when a task matches its description.
<available_skills>
<skill>
<name>customize-opencode</name>
<description>Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.</description>
<location>file:///<built-in></location>
</skill>
<skill>
<name>some-other-skill</name>
<description>...</description>
<location>file:///abs/path/to/SKILL.md</location>
</skill>
</available_skills>
注意点:
- 前两行永远渲染(
system.ts:99-105写死);即使 0 skill,第三段会变成"No skills are currently available." - 没有
description的 skill 会被fmt过滤掉(skill/index.ts:331),不会出现在 prompt 里——但Skill.require(name)仍能加载它们 <location>来自pathToFileURL(skill.location).href;内置 skill 的 location 字段是字符串"<built-in>",被pathToFileURL转成file:///<built-in>(不是真实路径)- agent 的 permission 整体禁用
"skill"时(Permission.disabled(["skill"], agent.permission)),整段返回undefined,最终system数组里完全没有这段 - 单个 skill 被
Permission.evaluate("skill", name, ...) === "deny"时从列表剔除(skill/index.ts:314)
这段在最终 system 消息数组里的位置
packages/opencode/src/session/prompt.ts:1327-1333:
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
sys.skills(agent),
sys.environment(model),
instruction.system().pipe(Effect.orDie),
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
下发给 provider 的 system: string[] 顺序:
- provider 基础 prompt ——
SystemPrompt.provider(model)按model.api.id选出来(system.ts:25-39):gpt-4/o1/o3→beast.txt- 其它
gpt→gpt.txt(codex走codex.txt) gemini-*→gemini.txtclaude*→anthropic.txt- 名字含
trinity→trinity.txt - 名字含
kimi→kimi.txt - 其它 →
default.txt - 这一段从更早的调用点加入,不在
prompt.ts:1333拼装里
env段(SystemPrompt.environment,system.ts:55-92):包含model.api.id自我说明 +<env>块(工作目录、worktree、是否 git、平台、日期),以及<available_references>块(若有 reference)instructions段:Instruction.system()聚合的全局 + 项目AGENTS.mdskills段:上面那一坨- 可选追加
STRUCTURED_OUTPUT_SYSTEM_PROMPT(format.type === "json_schema")
工具描述(packages/opencode/src/tool/skill.txt)只有简短指针
Load a specialized skill when the task at hand matches one of the skills listed in the system prompt.
Use this tool to inject the skill's instructions and resources into current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc in the same directory as the skill.
The skill name must match one of the skills listed in your system prompt.
V2 渲染差异(packages/core/src/skill/guidance.ts:16-32)
-
没有
<location>字段,输出只含<name>+<description> -
0 skill 且
PermissionV2.evaluate("skill", "*") === "deny"时返回SystemContext.empty(整段不挂载);0 skill 但未全 deny 时仍渲染开头两行 +"No skills are currently available." -
skill 集合变化时不重发整段 baseline,而是发一条 Mid-Conversation System Message:
The available skills have changed. This list supersedes the previous available skills list. <新的整段> -
skill 集合被完全移除时发:
Skill guidance is no longer available. Do not use any previously listed skill.
一句话总结
发现服务扫盘 / 拉取 → Skill service 在 InstanceState 里按 location 维护 name → Info 表 → SkillGuidance 作为 SystemContext 源把"目录"(仅 name+description)放进模型可见上下文 → 模型看到任务匹配后调用 skill 工具 → 工具在权限通过后才把 skill 正文和同目录文件清单注入对话。