mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
fix(skills): tell the model where each skill body lives
The system-prompt addendum now tags every skill with a source pointer: '[builtin]' for skills embedded in the zot binary, or the SKILL.md path (HOME collapsed to ~) for user-installed ones. The body still loads on demand through the 'skill' tool by name, so no behaviour change for execution, this is pure disambiguation. Before: - write-zot-extension — Help the user create a new zot extension... After: - write-zot-extension [builtin]: Help the user create a new zot extension... - code-review [~/Library/Application Support/zot/skills/code-review/SKILL.md]: ... Why: built-in skills have no filesystem path because their markdown is embedded in the binary. Without the [builtin] tag the model had no way to distinguish them from user skills, and could mistakenly try to read a nonexistent file. The path pointer for user skills also helps the model cite where guidance came from and reason about trust (builtin vs project-local vs global). Test updated; addendum grew to ~123 tokens for a typical 3-skill setup (code-review, test-fix, write-zot-extension).
This commit is contained in:
parent
1a2ab427fe
commit
5cc54822cd
2 changed files with 43 additions and 8 deletions
|
|
@ -155,24 +155,56 @@ func scanUserSkills(zotHome, cwd, userHome string, seen map[string]*Skill) []err
|
||||||
// SystemPromptAddendum returns the text to append to the system
|
// SystemPromptAddendum returns the text to append to the system
|
||||||
// prompt when at least one skill is loaded. Empty string if none.
|
// prompt when at least one skill is loaded. Empty string if none.
|
||||||
//
|
//
|
||||||
// Format kept short and explicit so the model reliably calls the
|
// The format is deliberately compact: name, one-line description,
|
||||||
// `skill` tool with a name from the list rather than guessing.
|
// and a source pointer telling the model where the full body
|
||||||
|
// lives. Built-in skills show "builtin" since their markdown is
|
||||||
|
// embedded in the zot binary and not on the filesystem; user
|
||||||
|
// skills show their SKILL.md path (shortened with ~ for HOME).
|
||||||
|
//
|
||||||
|
// Loading still goes through the `skill` tool with just the name.
|
||||||
|
// The pointer is there so the model can (a) mention the source
|
||||||
|
// honestly in explanations and (b) distinguish between built-ins
|
||||||
|
// and user-authored instruction sets when reasoning about trust.
|
||||||
func SystemPromptAddendum(skills []*Skill) string {
|
func SystemPromptAddendum(skills []*Skill) string {
|
||||||
if len(skills) == 0 {
|
if len(skills) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("Available skills (call the `skill` tool with one of these names to load full instructions):\n")
|
sb.WriteString("Available skills (call the `skill` tool with a name from this list to load its full instructions):\n")
|
||||||
for _, s := range skills {
|
for _, s := range skills {
|
||||||
desc := strings.TrimSpace(s.Description)
|
desc := strings.TrimSpace(s.Description)
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = "(no description)"
|
desc = "(no description)"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&sb, "- %s — %s\n", s.Name, desc)
|
pointer := skillSourcePointer(s, home)
|
||||||
|
fmt.Fprintf(&sb, "- %s [%s]: %s\n", s.Name, pointer, desc)
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skillSourcePointer returns a short tag describing where a skill
|
||||||
|
// originates. Built-ins are tagged "builtin" because their markdown
|
||||||
|
// is embedded in the zot binary and not reachable through the
|
||||||
|
// filesystem. User skills are tagged with their SKILL.md path,
|
||||||
|
// collapsed to use ~ for the user home when possible.
|
||||||
|
func skillSourcePointer(s *Skill, home string) string {
|
||||||
|
if s == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
if s.Builtin {
|
||||||
|
return "builtin"
|
||||||
|
}
|
||||||
|
p := s.Path
|
||||||
|
if p == "" {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
if home != "" && strings.HasPrefix(p, home+string(filepath.Separator)) {
|
||||||
|
return "~" + p[len(home):]
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
// FindByName returns the skill with the given name, or nil.
|
// FindByName returns the skill with the given name, or nil.
|
||||||
func FindByName(skills []*Skill, name string) *Skill {
|
func FindByName(skills []*Skill, name string) *Skill {
|
||||||
for _, s := range skills {
|
for _, s := range skills {
|
||||||
|
|
|
||||||
|
|
@ -108,12 +108,15 @@ func TestVisibleSkillsHidesBuiltins(t *testing.T) {
|
||||||
|
|
||||||
func TestSystemPromptAddendum(t *testing.T) {
|
func TestSystemPromptAddendum(t *testing.T) {
|
||||||
skills := []*Skill{
|
skills := []*Skill{
|
||||||
{Name: "a", Description: "Do A."},
|
{Name: "built-a", Description: "Do A.", Builtin: true},
|
||||||
{Name: "b", Description: "Do B."},
|
{Name: "user-b", Description: "Do B.", Path: "/tmp/skills/user-b/SKILL.md"},
|
||||||
}
|
}
|
||||||
out := SystemPromptAddendum(skills)
|
out := SystemPromptAddendum(skills)
|
||||||
if want := "- a — Do A.\n- b — Do B.\n"; !contains(out, want) {
|
if !contains(out, "- built-a [builtin]: Do A.\n") {
|
||||||
t.Errorf("addendum missing entries:\n%s", out)
|
t.Errorf("builtin entry missing or wrong:\n%s", out)
|
||||||
|
}
|
||||||
|
if !contains(out, "- user-b [/tmp/skills/user-b/SKILL.md]: Do B.\n") {
|
||||||
|
t.Errorf("user entry missing path pointer:\n%s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue