diff --git a/cmd/agent/app.go b/cmd/agent/app.go index 7efc79ad..b36228f5 100644 --- a/cmd/agent/app.go +++ b/cmd/agent/app.go @@ -938,10 +938,15 @@ func (a *skillLoaderAdapter) LoadSkills(ctx context.Context, botID string) ([]fl } entries := make([]flow.SkillEntry, len(items)) for i, item := range items { + skillPath := "" + if item.SourcePath != "" { + skillPath = stdpath.Dir(item.SourcePath) + } entries[i] = flow.SkillEntry{ Name: item.Name, Description: item.Description, Content: item.Content, + Path: skillPath, Metadata: item.Metadata, } } diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 5bd888af..9f47c01c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -619,6 +619,7 @@ func (a *Agent) assembleTools(ctx context.Context, cfg RunConfig, emitter tools. skillsMap[s.Name] = tools.SkillDetail{ Description: s.Description, Content: s.Content, + Path: s.Path, } } session := tools.SessionContext{ diff --git a/internal/agent/tools/skill.go b/internal/agent/tools/skill.go index 7f592145..d0da1b4e 100644 --- a/internal/agent/tools/skill.go +++ b/internal/agent/tools/skill.go @@ -61,6 +61,7 @@ func (*SkillProvider) Tools(_ context.Context, session SessionContext) ([]sdk.To "skillName": skillName, "description": skill.Description, "content": skill.Content, + "path": skill.Path, }, nil }, }, diff --git a/internal/agent/tools/skill_test.go b/internal/agent/tools/skill_test.go new file mode 100644 index 00000000..0aa049fe --- /dev/null +++ b/internal/agent/tools/skill_test.go @@ -0,0 +1,42 @@ +package tools + +import ( + "context" + "testing" +) + +func TestUseSkillReturnsPath(t *testing.T) { + provider := NewSkillProvider(nil) + + toolset, err := provider.Tools(context.Background(), SessionContext{ + Skills: map[string]SkillDetail{ + "pdf": { + Description: "Read PDF instructions", + Content: "Use a PDF-aware workflow.", + Path: "/data/.agents/skills/pdf", + }, + }, + }) + if err != nil { + t.Fatalf("Tools returned error: %v", err) + } + if len(toolset) != 1 { + t.Fatalf("expected 1 tool, got %d", len(toolset)) + } + + result, err := toolset[0].Execute(nil, map[string]any{ + "skillName": "pdf", + "reason": "Need to process a PDF attachment", + }) + if err != nil { + t.Fatalf("Execute returned error: %v", err) + } + + payload, ok := result.(map[string]any) + if !ok { + t.Fatalf("result type = %T, want map[string]any", result) + } + if got := payload["path"]; got != "/data/.agents/skills/pdf" { + t.Fatalf("path = %#v, want %q", got, "/data/.agents/skills/pdf") + } +} diff --git a/internal/agent/tools/types.go b/internal/agent/tools/types.go index c2cf2fd0..0e6d7e4c 100644 --- a/internal/agent/tools/types.go +++ b/internal/agent/tools/types.go @@ -15,6 +15,7 @@ import ( type SkillDetail struct { Description string Content string + Path string } // StreamEventType identifies the kind of stream event emitted by tools. diff --git a/internal/agent/types.go b/internal/agent/types.go index ac09a457..885d3490 100644 --- a/internal/agent/types.go +++ b/internal/agent/types.go @@ -30,6 +30,7 @@ type SkillEntry struct { Name string Description string Content string + Path string Metadata map[string]any } diff --git a/internal/conversation/flow/resolver.go b/internal/conversation/flow/resolver.go index b94d6f8f..2c919ba2 100644 --- a/internal/conversation/flow/resolver.go +++ b/internal/conversation/flow/resolver.go @@ -44,6 +44,7 @@ type SkillEntry struct { Name string Description string Content string + Path string Metadata map[string]any } @@ -703,6 +704,7 @@ func normalizeGatewaySkill(entry SkillEntry) (agentpkg.SkillEntry, bool) { Name: name, Description: description, Content: content, + Path: strings.TrimSpace(entry.Path), Metadata: entry.Metadata, }, true } diff --git a/internal/conversation/flow/skill_test.go b/internal/conversation/flow/skill_test.go new file mode 100644 index 00000000..d4d69201 --- /dev/null +++ b/internal/conversation/flow/skill_test.go @@ -0,0 +1,18 @@ +package flow + +import "testing" + +func TestNormalizeGatewaySkillPreservesPath(t *testing.T) { + got, ok := normalizeGatewaySkill(SkillEntry{ + Name: "pdf", + Description: "Read PDF instructions", + Content: "Use a PDF-aware workflow.", + Path: " /data/.agents/skills/pdf ", + }) + if !ok { + t.Fatal("normalizeGatewaySkill returned ok=false") + } + if got.Path != "/data/.agents/skills/pdf" { + t.Fatalf("path = %q, want %q", got.Path, "/data/.agents/skills/pdf") + } +} diff --git a/internal/handlers/skills_test.go b/internal/handlers/skills_test.go index ac8b6f47..2c939c44 100644 --- a/internal/handlers/skills_test.go +++ b/internal/handlers/skills_test.go @@ -290,7 +290,7 @@ type skillsTestEnv struct { func newSkillsTestEnv(t *testing.T) *skillsTestEnv { t.Helper() - dataRoot, err := os.MkdirTemp("", "memoh-skills-") + dataRoot, err := newSkillsTestDataRoot() if err != nil { t.Fatalf("create temp data root: %v", err) } @@ -382,6 +382,18 @@ func (e *skillsTestEnv) localPath(containerPath string) string { return filepath.Join(e.dataRoot, filepath.FromSlash(strings.TrimPrefix(clean, "/"))) } +func newSkillsTestDataRoot() (string, error) { + var lastErr error + for _, dir := range []string{"/tmp", ""} { + dataRoot, err := os.MkdirTemp(dir, "mh-sk-") + if err == nil { + return dataRoot, nil + } + lastErr = err + } + return "", lastErr +} + type skillsTestDB struct { userID string botID string