From e37428b79b9a9aa0744e3427e3a581e0f24a78e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 27 Jun 2026 03:47:22 +0000 Subject: [PATCH 1/2] Align skill asset blocks with .NET Port the .NET skill content behavior that emits explicit available resources and scripts blocks for file-based skills. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- agent/skills/fsskills/source.go | 36 ++++++++++------ agent/skills/fsskills/source_script_test.go | 34 ++++++++------- agent/skills/fsskills/source_test.go | 46 +++++++++++++++++++++ agent/skills/skills.go | 6 +-- agent/skills/skills_test.go | 2 +- 5 files changed, 93 insertions(+), 31 deletions(-) diff --git a/agent/skills/fsskills/source.go b/agent/skills/fsskills/source.go index 29f01376..4e0700c6 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,35 @@ 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 { + 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 { if script.ParametersSchema == "" { - fmt.Fprintf(&sb, " \n", xmlEscapeAttr(script.Name)) + fmt.Fprintf(&sb, " \n") } } - sb.WriteString("") + sb.WriteString("") return sb.String() } diff --git a/agent/skills/fsskills/source_script_test.go b/agent/skills/fsskills/source_script_test.go index 9e886ab8..744fd889 100644 --- a/agent/skills/fsskills/source_script_test.go +++ b/agent/skills/fsskills/source_script_test.go @@ -383,7 +383,7 @@ func TestFileScript_HasDefaultParametersSchema(t *testing.T) { } } -func TestFileSkill_WithScripts_ContentIncludesScriptSchemasBlock(t *testing.T) { +func TestFileSkill_WithScripts_ContentIncludesAvailableScriptsBlock(t *testing.T) { root := t.TempDir() createSkillDir(t, root, "schema-content-skill", "A test skill", "Instructions here.") createRelativeFile(t, filepath.Join(root, "schema-content-skill"), "build.sh", "echo build") @@ -402,17 +402,20 @@ func TestFileSkill_WithScripts_ContentIncludesScriptSchemasBlock(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.Contains(content, "") { - t.Fatalf("expected block in content, got: %s", content) + if !strings.Contains(content, "") { + t.Fatalf("expected block in content, got: %s", content) } - if !strings.Contains(content, ``) { - t.Fatalf("expected in content, got: %s", content) + if !strings.Contains(content, `\n") } 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, `