diff --git a/pkg/agent/context.go b/pkg/agent/context.go index fb03979e5..2692bc7a1 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -786,12 +786,31 @@ func (cb *ContextBuilder) MatchSkillsInMessage(message string) []string { } // LoadSkillContext loads the full SKILL.md content for the given skill names -// via the underlying SkillsLoader. +// via the underlying SkillsLoader. It resolves frontmatter names to directory +// names so that skills whose metadata name differs from the directory name +// are loaded correctly. func (cb *ContextBuilder) LoadSkillContext(skillNames []string) string { - if cb.skillsLoader == nil { + if cb.skillsLoader == nil || len(skillNames) == 0 { return "" } - return cb.skillsLoader.LoadSkillsForContext(skillNames) + + allSkills := cb.skillsLoader.ListSkills() + nameToDir := make(map[string]string, len(allSkills)) + for _, s := range allSkills { + dirName := filepath.Base(filepath.Dir(s.Path)) + nameToDir[strings.ToLower(s.Name)] = dirName + } + + resolved := make([]string, 0, len(skillNames)) + for _, name := range skillNames { + if dir, ok := nameToDir[strings.ToLower(name)]; ok { + resolved = append(resolved, dir) + } else { + resolved = append(resolved, name) + } + } + + return cb.skillsLoader.LoadSkillsForContext(resolved) } // isSkillNameChar returns true for characters that can appear inside a skill diff --git a/pkg/agent/context_skills_test.go b/pkg/agent/context_skills_test.go index c1c9edfd7..c187af459 100644 --- a/pkg/agent/context_skills_test.go +++ b/pkg/agent/context_skills_test.go @@ -171,6 +171,35 @@ func TestLoadSkillContext(t *testing.T) { } } +func TestLoadSkillContext_MetadataNameDiffersFromDir(t *testing.T) { + tmpDir := setupWorkspace(t, nil) + defer os.RemoveAll(tmpDir) + + createTestSkill( + t, + filepath.Join(tmpDir, "skills"), + "weather-skill", + "weather", + "Get weather", + "Call the weather API (metadata name test).", + ) + + cb := NewContextBuilder(tmpDir) + + matched := cb.MatchSkillsInMessage("use the weather skill") + if len(matched) == 0 { + t.Fatal("expected to match skill by metadata name 'weather'") + } + + ctx := cb.LoadSkillContext(matched) + if ctx == "" { + t.Fatal("expected non-empty skill context when metadata name differs from directory name") + } + if !strings.Contains(ctx, "metadata name test") { + t.Errorf("skill context should contain skill body, got: %s", ctx) + } +} + func TestLoadSkillContext_NilLoader(t *testing.T) { cb := &ContextBuilder{skillsLoader: nil} ctx := cb.LoadSkillContext([]string{"weather"}) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 8da52d6a6..1a9a5d48a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -61,6 +61,7 @@ type processOptions struct { EnableSummary bool // Whether to trigger summarization SendResponse bool // Whether to send response via bus NoHistory bool // If true, don't load session history (for heartbeat) + SkillContext string // Injected SKILL.md content for matched skills } const ( @@ -802,11 +803,10 @@ func (al *AgentLoop) runAgentLoop( } // Auto-inject SKILL.md content when the user references an installed skill. - var skillCtx string if matched := agent.ContextBuilder.MatchSkillsInMessage(opts.UserMessage); len(matched) > 0 { - skillCtx = agent.ContextBuilder.LoadSkillContext(matched) + opts.SkillContext = agent.ContextBuilder.LoadSkillContext(matched) logger.DebugCF("agent", "Skills matched in user message", - map[string]any{"matched": matched, "context_len": len(skillCtx)}) + map[string]any{"matched": matched, "context_len": len(opts.SkillContext)}) } messages := agent.ContextBuilder.BuildMessages( @@ -816,7 +816,7 @@ func (al *AgentLoop) runAgentLoop( opts.Media, opts.Channel, opts.ChatID, - skillCtx, + opts.SkillContext, ) // Resolve media:// refs to base64 data URLs (streaming) @@ -1085,6 +1085,7 @@ func (al *AgentLoop) runLLMIteration( messages = agent.ContextBuilder.BuildMessages( newHistory, newSummary, "", nil, opts.Channel, opts.ChatID, + opts.SkillContext, ) continue }