-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathskills.go
More file actions
181 lines (159 loc) · 4.64 KB
/
skills.go
File metadata and controls
181 lines (159 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package iteragent
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Skill represents a loaded agent skill.
type Skill struct {
Name string
Description string
// Content holds the full skill body (everything after the frontmatter).
// It is NOT injected into the system prompt by default — only metadata is.
Content string
// RawFrontmatter is the raw YAML frontmatter block.
RawFrontmatter string
Path string
}
// SkillSet is a collection of loaded skills.
type SkillSet struct {
Skills []Skill
}
// LoadSkills scans dirs for skill subdirectories. For each subdirectory it looks
// for SKILL.md (AgentSkills standard) first, then falls back to skill.md.
// Skills are deduplicated by name; the last directory in the list wins,
// so callers can override built-in skills by appending their own dirs.
func LoadSkills(dirs []string) (*SkillSet, error) {
// Use an ordered map: name → skill, preserving insertion order for stable output.
seen := map[string]int{} // name → index in skills slice
var skills []Skill
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// Try SKILL.md first (AgentSkills standard), then skill.md.
var skillPath string
candidates := []string{
filepath.Join(dir, entry.Name(), "SKILL.md"),
filepath.Join(dir, entry.Name(), "skill.md"),
}
for _, c := range candidates {
if _, err := os.Stat(c); err == nil {
skillPath = c
break
}
}
if skillPath == "" {
continue
}
data, err := os.ReadFile(skillPath)
if err != nil {
continue
}
raw := string(data)
name, desc, frontmatter, body := parseSkillFile(raw)
if name == "" {
name = entry.Name()
}
skill := Skill{
Name: name,
Description: desc,
Content: body,
RawFrontmatter: frontmatter,
Path: skillPath,
}
// Deduplicate: later dirs override earlier ones (last-writer-wins).
if idx, exists := seen[name]; exists {
skills[idx] = skill
} else {
seen[name] = len(skills)
skills = append(skills, skill)
}
}
}
return &SkillSet{Skills: skills}, nil
}
// parseSkillFile parses a skill file and returns (name, description, frontmatter, body).
func parseSkillFile(content string) (name, desc, frontmatter, body string) {
lines := strings.Split(content, "\n")
inFrontmatter := false
frontmatterEnd := -1
var fmLines []string
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "---" {
if !inFrontmatter && i == 0 {
inFrontmatter = true
continue
}
if inFrontmatter {
frontmatterEnd = i
inFrontmatter = false
continue
}
}
if inFrontmatter {
fmLines = append(fmLines, line)
if strings.HasPrefix(trimmed, "name:") {
name = strings.TrimSpace(strings.TrimPrefix(trimmed, "name:"))
// Remove surrounding quotes if present.
name = strings.Trim(name, `"'`)
}
if strings.HasPrefix(trimmed, "description:") {
desc = strings.TrimSpace(strings.TrimPrefix(trimmed, "description:"))
desc = strings.Trim(desc, `"'`)
}
}
}
frontmatter = strings.Join(fmLines, "\n")
if frontmatterEnd >= 0 && frontmatterEnd+1 < len(lines) {
body = strings.TrimSpace(strings.Join(lines[frontmatterEnd+1:], "\n"))
} else if frontmatterEnd < 0 {
// No frontmatter — entire file is the body.
body = strings.TrimSpace(content)
}
return
}
// parseSkillFrontmatter is kept for backward compatibility.
func parseSkillFrontmatter(content string) (name, desc string) {
name, desc, _, _ = parseSkillFile(content)
return
}
// FormatForPrompt returns an XML <available_skills> block listing skill metadata.
// Only name and description are included — the full content is NOT injected.
// The LLM can request full skill content via the read_file tool.
func (s *SkillSet) FormatForPrompt() string {
if len(s.Skills) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("\n\n<available_skills>\n")
for _, skill := range s.Skills {
sb.WriteString(fmt.Sprintf(
" <skill name=%q description=%q path=%q />\n",
skill.Name, skill.Description, skill.Path,
))
}
sb.WriteString("</available_skills>\n")
sb.WriteString("\nTo use a skill, read its full content with the read_file tool using the path above.\n")
return sb.String()
}
// Get returns the skill with the given name, or nil if not found.
func (s *SkillSet) Get(name string) *Skill {
for i := range s.Skills {
if s.Skills[i].Name == name {
return &s.Skills[i]
}
}
return nil
}
// SkillSetEmpty returns an empty SkillSet.
func SkillSetEmpty() *SkillSet {
return &SkillSet{Skills: []Skill{}}
}