Files
Agent-Native/runtime/skills/skills.go
T
weichen 6ddd3da8df Enable canonical versioned Go module consumption
Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements.

Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate.

Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable.

Refs: Versioned dependencies U1/U3; feature/dependency-workspace
2026-10-13 15:36:10 +00:00

217 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package skills 实现 SKILL.md 技能的加载与按需展开。
//
// 目录约定:<root>/<skill-name>/SKILL.md,frontmatter 按开放规范校验
//(agentskills.io;name 必填且与目录同名、description 必填;已知键白名单;
// 不引入 YAML 依赖,用 frontmatter.go 的严格子集解析)。
// 校验失败不静默:记入 Store.Diagnostics(),Load 同名技能 fail-closed
// 带字段定位错误。
//
// 信任边界:技能正文进入模型上下文,属提示注入面——只加载组合根显式
// 配置的根目录与工作区 .agent/skills/;正文与索引总量受限。
package skills
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"git-ssh.u1680832.nyat.app/weichen/Agent-Native.git/runtime/tools"
)
const (
maxSkillsPerRoot = 100
maxBodyBytes = 64 << 10
)
// Skill 是一个可加载技能。
type Skill struct {
Name string
Description string
Body string
Source string
// Frontmatter 是校验后的头(License/Compatibility/Metadata/AllowedTools)。
Frontmatter Frontmatter
}
// Store 扫描若干根目录并按名加载技能。
type Store struct {
roots []string
cache map[string]Skill
diagnostics []ValidationError
}
func NewStore(roots ...string) *Store {
clean := make([]string, 0, len(roots))
for _, r := range roots {
if strings.TrimSpace(r) != "" {
clean = append(clean, r)
}
}
return &Store{roots: clean, cache: nil}
}
// Diagnostics 返回最近一次 List 的校验诊断(不合规技能的逐条原因)。
// 加载器不因单技能坏而中断索引,但原因必须可查(开放规范:错误上浮)。
func (s *Store) Diagnostics() []ValidationError {
out := make([]ValidationError, len(s.diagnostics))
copy(out, s.diagnostics)
return out
}
// Roots 返回配置的根目录(只读副本)。
func (s *Store) Roots() []string {
out := make([]string, len(s.roots))
copy(out, s.roots)
return out
}
// List 扫描全部根目录(同名技能先配置者优先),返回索引。
func (s *Store) List() []Skill {
s.diagnostics = nil
cache := map[string]Skill{}
var out []Skill
for _, root := range s.roots {
entries, err := os.ReadDir(root)
if err != nil {
continue // 根不存在:跳过(技能是可选能力)
}
count := 0
for _, e := range entries {
if !e.IsDir() || count >= maxSkillsPerRoot {
continue
}
path := filepath.Join(root, e.Name(), "SKILL.md")
data, err := os.ReadFile(path)
if err != nil {
continue
}
skill, err := parse(e.Name(), path, data)
if err != nil {
// 单个坏技能跳过(不拖垮索引),但原因记入诊断(错误上浮)。
if ve, ok := err.(ValidationError); ok {
s.diagnostics = append(s.diagnostics, ve)
} else {
s.diagnostics = append(s.diagnostics, ValidationError{Path: path, Field: "-", Msg: err.Error()})
}
continue
}
if _, dup := cache[skill.Name]; dup {
continue
}
cache[skill.Name] = skill
out = append(out, skill)
count++
}
}
s.cache = cache
return out
}
// Load 按名加载技能正文(fail-closed:未知名返回错误,不猜测近似名;
// 同名但校验失败时返回带字段定位的校验错误而非笼统 unknown)。
func (s *Store) Load(name string) (Skill, error) {
if s.cache == nil {
s.List()
}
if skill, ok := s.cache[name]; ok {
return skill, nil
}
// 缓存可能过期(List 之后目录变化):重扫一遍再判。
s.List()
if skill, ok := s.cache[name]; ok {
return skill, nil
}
for _, d := range s.diagnostics {
if strings.Contains(d.Path, string(filepath.Separator)+name+string(filepath.Separator)) {
return Skill{}, d
}
}
return Skill{}, fmt.Errorf("unknown skill %q (not installed in any configured root)", name)
}
// ExpandCommands replaces /skill:name with that skill's body.
// An unknown name returns an error and leaves the text unchanged.
func (s *Store) ExpandCommands(text string) (string, error) {
const marker = "/skill:"
var b strings.Builder
rest := text
for {
i := strings.Index(rest, marker)
if i < 0 {
b.WriteString(rest)
break
}
b.WriteString(rest[:i])
name := rest[i+len(marker):]
end := 0
for end < len(name) && name[end] != ' ' && name[end] != '\n' && name[end] != '\t' {
end++
}
skillName := name[:end]
skill, err := s.Load(skillName)
if err != nil {
return text, err
}
b.WriteString(skill.Body)
rest = name[end:]
}
return b.String(), nil
}
// parse 解析并校验一份 SKILL.md(开放规范:name/description 必填、
// name 与目录同名、已知键白名单、错误带字段定位)。
func parse(dirName, path string, data []byte) (Skill, error) {
if len(data) > maxBodyBytes {
data = data[:maxBodyBytes]
}
fm, body, err := parseFrontmatter(path, string(data))
if err != nil {
return Skill{}, err
}
if err := validateFrontmatter(path, dirName, fm); err != nil {
return Skill{}, err
}
return Skill{
Name: fm.Name, Description: fm.Description, Body: body,
Source: path, Frontmatter: *fm,
}, nil
}
// ---- load_skill 工具 ----
type loadSkillTool struct{ store *Store }
// RegisterLoader 注册 load_skill 工具(store 无技能时不注册,避免死工具)。
func RegisterLoader(r *tools.Registry, store *Store) {
if store == nil || len(store.List()) == 0 {
return
}
r.Register(&loadSkillTool{store: store})
}
func (t *loadSkillTool) Name() string { return "load_skill" }
func (t *loadSkillTool) Description() string {
return "Load the full instructions of a skill by name (names are listed in the system prompt skill index)."
}
func (t *loadSkillTool) Parameters() json.RawMessage {
return json.RawMessage(`{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}`)
}
func (t *loadSkillTool) Execute(_ context.Context, args json.RawMessage) (tools.Result, error) {
var obj struct {
Name string `json:"name"`
}
if err := json.Unmarshal(args, &obj); err != nil {
return tools.Result{}, err
}
skill, err := t.store.Load(obj.Name)
if err != nil {
return tools.Result{Content: err.Error(), IsError: true}, nil
}
return tools.Result{Content: "# skill: " + skill.Name + "\n\n" + skill.Body}, nil
}