diff --git a/agent/skills/fsskills/source.go b/agent/skills/fsskills/source.go index 29f01376..e3f30a12 100644 --- a/agent/skills/fsskills/source.go +++ b/agent/skills/fsskills/source.go @@ -242,9 +242,8 @@ func (s *Source) parseSkillDirectory(skillFS fs.FS, logPath string) *skills.Skil return } raw := string(data) - if schemasBlock := buildScriptSchemasBlock(scripts); schemasBlock != "" { - raw += schemasBlock - } + raw += "\n" + buildAvailableResourcesBlock(resources) + raw += "\n" + buildAvailableScriptsBlock(scripts) cachedContent = raw }) return cachedContent, contentErr @@ -562,24 +561,44 @@ type discoveredSkillDir struct { path string } -// buildScriptSchemasBlock returns a XML block listing each -// script with its parameter schema. Scripts with no schema emit a self-closing -// element; scripts with a schema emit the JSON inline. -// Returns an empty string when scripts is empty. -func buildScriptSchemasBlock(scripts []skills.Script) string { +func buildAvailableResourcesBlock(resources []skills.Resource) string { + if len(resources) == 0 { + return "\n" + } + var sb strings.Builder + sb.WriteString("\n\n") + for _, resource := range resources { + if resource.Description != "" { + fmt.Fprintf(&sb, " \n", + xmlEscapeAttr(resource.Name), xmlEscapeAttr(resource.Description)) + } else { + fmt.Fprintf(&sb, " \n", xmlEscapeAttr(resource.Name)) + } + } + sb.WriteString("") + return sb.String() +} + +func buildAvailableScriptsBlock(scripts []skills.Script) string { if len(scripts) == 0 { - return "" + return "\n" } var sb strings.Builder - sb.WriteString("\n\n") + sb.WriteString("\n\n") for _, script := range scripts { + namePart := fmt.Sprintf(" \n") } } - sb.WriteString("") + sb.WriteString("") return sb.String() } diff --git a/agent/skills/fsskills/source_block_builder_test.go b/agent/skills/fsskills/source_block_builder_test.go new file mode 100644 index 00000000..fa809d54 --- /dev/null +++ b/agent/skills/fsskills/source_block_builder_test.go @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft. All rights reserved. + +package fsskills + +import ( + "strings" + "testing" + + "github.com/microsoft/agent-framework-go/agent/skills" +) + +func TestBuildAvailableResourcesBlock_WithDescription_EmitsDescriptionAttribute(t *testing.T) { + resources := []skills.Resource{ + {Name: "docs/guide.md", Description: "The user guide"}, + } + got := buildAvailableResourcesBlock(resources) + if !strings.Contains(got, ``) { + t.Fatalf("expected description attribute in resource element, got: %s", got) + } +} + +func TestBuildAvailableResourcesBlock_WithoutDescription_OmitsDescriptionAttribute(t *testing.T) { + resources := []skills.Resource{ + {Name: "docs/guide.md"}, + } + got := buildAvailableResourcesBlock(resources) + if !strings.Contains(got, ``) { + t.Fatalf("expected self-closing resource without description attribute, got: %s", got) + } + if strings.Contains(got, "description=") { + t.Fatalf("expected no description attribute when description is empty, got: %s", got) + } +} + +func TestBuildAvailableResourcesBlock_DescriptionIsXmlEscaped(t *testing.T) { + resources := []skills.Resource{ + {Name: "data.xml", Description: `A "quoted" & resource`}, + } + got := buildAvailableResourcesBlock(resources) + if strings.Contains(got, `"A "quoted"`) { + t.Fatalf("expected description to be XML-escaped, got: %s", got) + } + if !strings.Contains(got, `description="A "quoted" & <tagged> resource"`) { + t.Fatalf("expected XML-escaped description attribute, got: %s", got) + } +} + +func TestBuildAvailableScriptsBlock_WithDescription_NoSchema_EmitsDescriptionAttribute(t *testing.T) { + scripts := []skills.Script{ + {Name: "scripts/run.py", Description: "Runs the pipeline"}, + } + got := buildAvailableScriptsBlock(scripts) + if !strings.Contains(got, `