From bb9ed63a347b3e437106b27ff7547bd388fd5bbe Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:15:29 +0100 Subject: [PATCH] .NET: Restructure skill script schemas XML and remove resources from body (#6343) * Restore UTF-8 BOMs and fix BuildScriptSchemasBlock doc comment - Restore UTF-8 BOM on all changed files to match repo convention - Fix XML doc: -> to match emitted output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments: fix doc remarks and rename tests - Update script doc remarks to clarify only parameter schemas are included - Fix grammar: 'arguments format' -> 'argument format' - Rename misleading test methods to match actual assertions - Clarify comment about removed wrapper element Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: SergeyMenshykh Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Skills/File/AgentFileSkill.cs | 5 +- .../Skills/Programmatic/AgentClassSkill.cs | 25 ++++++++- .../Skills/Programmatic/AgentInlineSkill.cs | 14 ++++- .../AgentInlineSkillContentBuilder.cs | 54 +++++-------------- .../AgentSkills/AgentClassSkillTests.cs | 18 +++---- .../AgentSkills/AgentFileSkillScriptTests.cs | 9 ++-- .../AgentSkills/AgentInlineSkillTests.cs | 47 ++++++++-------- 7 files changed, 88 insertions(+), 84 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs index d1c792de21..8a74fa034e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs @@ -49,14 +49,13 @@ public sealed class AgentFileSkill : AgentSkill /// /// /// Returns the raw SKILL.md content. When the skill has scripts, a - /// <scripts><script name="..."><parameters_schema>...</parameters_schema></script></scripts> - /// block is appended with a per-script entry describing the expected argument format. + /// <script_schemas> block is appended describing the argument format. /// The result is cached after the first access. /// public override ValueTask GetContentAsync(CancellationToken cancellationToken = default) { var content = this._content ??= this._scripts is { Count: > 0 } - ? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptsBlock(this._scripts) + ? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptSchemasBlock(this._scripts) : this._originalContent; return new(content); } diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs index 32f461e32a..84174154a2 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs @@ -114,7 +114,6 @@ public abstract class AgentClassSkill< this.Frontmatter.Name, this.Frontmatter.Description, this.Instructions, - this.Resources, this.Scripts)); } @@ -147,11 +146,17 @@ public abstract class AgentClassSkill< /// Gets the resources associated with this skill, or if none. /// /// + /// /// The default implementation returns resources discovered via reflection by scanning /// for members annotated with . /// This discovery is compatible with Native AOT because is annotated with /// . The result is cached after the first access. /// Override this property in derived classes to provide skill-specific resources. + /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference resources by name in the skill's instructions or in other resources. + /// /// public virtual IReadOnlyList? Resources => this._resources.Value; @@ -159,11 +164,17 @@ public abstract class AgentClassSkill< /// Gets the scripts associated with this skill, or if none. /// /// + /// /// The default implementation returns scripts discovered via reflection by scanning /// for methods annotated with . /// This discovery is compatible with Native AOT because is annotated with /// . The result is cached after the first access. /// Override this property in derived classes to provide skill-specific scripts. + /// + /// + /// Only script parameter schemas are included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference scripts by name in the skill's instructions or in a resource. + /// /// public virtual IReadOnlyList? Scripts => this._scripts.Value; @@ -184,6 +195,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill resource backed by a static value. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// The static resource value. /// An optional description of the resource. @@ -194,6 +209,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill resource backed by a delegate that produces a dynamic value. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// A method that produces the resource value when requested. /// An optional description of the resource. @@ -208,6 +227,10 @@ public abstract class AgentClassSkill< /// /// Creates a skill script backed by a delegate. /// + /// + /// Only the script's parameter schema is included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference the script by name in the skill's instructions or in a resource. + /// /// The script name. /// A method to execute when the script is invoked. /// An optional description of the script. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs index 4fb2b045cb..2465431622 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs @@ -95,7 +95,7 @@ public sealed class AgentInlineSkill : AgentSkill /// public override ValueTask GetContentAsync(CancellationToken cancellationToken = default) { - return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts)); + return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._scripts)); } /// @@ -115,6 +115,10 @@ public sealed class AgentInlineSkill : AgentSkill /// /// Registers a static resource with this skill. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// The static resource value. /// An optional description of the resource. @@ -129,6 +133,10 @@ public sealed class AgentInlineSkill : AgentSkill /// Registers a dynamic resource with this skill, backed by a C# delegate. /// The delegate's parameters and return type are automatically marshaled via AIFunctionFactory. /// + /// + /// Resources are not automatically included in the skill body. + /// To enable discovery, reference the resource by name in the skill's instructions or in another resource. + /// /// The resource name. /// A method that produces the resource value when requested. /// An optional description of the resource. @@ -147,6 +155,10 @@ public sealed class AgentInlineSkill : AgentSkill /// Registers a script with this skill, backed by a C# delegate. /// The delegate's parameters and return type are automatically marshaled via AIFunctionFactory. /// + /// + /// Only the script's parameter schema is included in the skill body (as a <script_schemas> block). + /// To enable discovery, reference the script by name in the skill's instructions or in a resource. + /// /// The script name. /// A method to execute when the script is invoked. /// An optional description of the script. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs index dabf75fa1a..d2f27edadc 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs @@ -12,19 +12,17 @@ namespace Microsoft.Agents.AI; internal static class AgentInlineSkillContentBuilder { /// - /// Builds the complete skill content containing name, description, instructions, resources, and scripts. + /// Builds the complete skill content containing name, description, instructions, and script parameter schemas. /// /// The skill name. /// The skill description. /// The raw instructions text. - /// Optional resources associated with the skill. /// Optional scripts associated with the skill. /// An XML-structured content string. public static string Build( string name, string description, string instructions, - IReadOnlyList? resources, IReadOnlyList? scripts) { _ = Throw.IfNullOrWhitespace(name); @@ -39,41 +37,24 @@ internal static class AgentInlineSkillContentBuilder .Append(EscapeXmlString(instructions)) .Append("\n"); - if (resources is { Count: > 0 }) - { - sb.Append("\n\n\n"); - foreach (var resource in resources) - { - if (resource.Description is not null) - { - sb.Append($" \n"); - } - else - { - sb.Append($" \n"); - } - } - - sb.Append(""); - } - if (scripts is { Count: > 0 }) { sb.Append('\n'); - sb.Append(BuildScriptsBlock(scripts)); + sb.Append(BuildScriptSchemasBlock(scripts)); } return sb.ToString(); } /// - /// Builds a <scripts>...</scripts> XML block for the given scripts. - /// Each script is emitted as a <script name="..."> element with optional - /// description attribute and <parameters_schema> child element. + /// Builds a <script_schemas>...</script_schemas> XML block for the given scripts. + /// Each script is emitted as a <schema script="..."> element containing only + /// the parameter schema. This block serves as a reference for the model to know how to + /// format arguments when calling scripts, not as a discovery mechanism. /// /// The scripts to include in the block. - /// An XML string starting with \n<scripts>, or an empty string if the list is empty. - public static string BuildScriptsBlock(IReadOnlyList scripts) + /// An XML string starting with \n<script_schemas>, or an empty string if the list is empty. + public static string BuildScriptSchemasBlock(IReadOnlyList scripts) { _ = Throw.IfNull(scripts); @@ -83,32 +64,23 @@ internal static class AgentInlineSkillContentBuilder } var sb = new StringBuilder(); - sb.Append("\n\n"); + sb.Append("\n\n"); foreach (var script in scripts) { var parametersSchema = script.ParametersSchema; - if (script.Description is null && parametersSchema is null) + if (parametersSchema is null) { - sb.Append($" \n"); + sb.Append($" {EscapeXmlString(parametersSchema.Value.GetRawText(), preserveQuotes: true)}\n"); } } - sb.Append(""); + sb.Append(""); return sb.ToString(); } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs index 1248866a52..17bf71a388 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -51,9 +51,8 @@ public sealed class AgentClassSkillTests // Act & Assert — Content is cached Assert.Same(await skill.GetContentAsync(), await skill.GetContentAsync()); - // Act & Assert — Content includes parameter schema from typed script - Assert.Contains("parameters_schema", await skill.GetContentAsync()); - Assert.Contains("value", await skill.GetContentAsync()); + // Act & Assert — Content includes parameter schema from typed script (with preserved quotes) + Assert.Contains("\"value\"", await skill.GetContentAsync()); } [Fact] @@ -383,10 +382,9 @@ public sealed class AgentClassSkillTests // Arrange var skill = new AttributedFullSkill(); - // Act & Assert — Content includes reflected resources and scripts - Assert.Contains("", await skill.GetContentAsync()); - Assert.Contains("conversion-table", await skill.GetContentAsync()); - Assert.Contains("", await skill.GetContentAsync()); + // Act & Assert — Content no longer includes resources in body; scripts are in script_schemas + Assert.DoesNotContain("", await skill.GetContentAsync()); + Assert.Contains("", await skill.GetContentAsync()); Assert.Contains("convert", await skill.GetContentAsync()); // Act & Assert — discovered members are cached @@ -504,7 +502,7 @@ public sealed class AgentClassSkillTests } [Fact] - public async Task Content_IncludesDescription_ForReflectedResourcesAsync() + public async Task Content_DoesNotRenderResources_InBodyAsync() { // Arrange var skill = new AttributedResourcePropertiesSkill(); @@ -512,8 +510,8 @@ public sealed class AgentClassSkillTests // Act var content = await skill.GetContentAsync(); - // Assert — descriptions from [Description] attribute appear in synthesized content - Assert.Contains("Some important data.", content); + // Assert — resources are no longer rendered in body content + Assert.DoesNotContain("", content); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs index 9a27528051..aa001fd2a0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs @@ -122,11 +122,10 @@ public sealed class AgentFileSkillScriptTests // Assert — content starts with original and appends per-script entries Assert.StartsWith("Original content", content); - Assert.Contains("", content); - Assert.Contains("