From 08d179b93dd2dcc13a7076f99b8104ec886d4cb6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 22 Jun 2026 20:06:47 +0100 Subject: [PATCH 1/2] .NET: Explicitly emit available_resources and available_scripts in skill content AgentInlineSkillContentBuilder now always emits and elements, using self-closing tags when a skill has no resources or scripts. This signals to the model exactly what is callable so it does not hallucinate non-existent resource or script names. Script parameter schemas are wrapped in a nested element. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Skills/File/AgentFileSkill.cs | 4 +- .../Skills/Programmatic/AgentClassSkill.cs | 23 ++- .../Skills/Programmatic/AgentInlineSkill.cs | 17 +- .../AgentInlineSkillContentBuilder.cs | 50 +++-- .../AgentSkills/AgentClassSkillTests.cs | 15 +- .../AgentSkills/AgentFileSkillScriptTests.cs | 8 +- .../AgentInlineSkillContentBuilderTests.cs | 194 ++++++++++++++++++ .../AgentSkills/AgentInlineSkillTests.cs | 51 ++--- 8 files changed, 298 insertions(+), 64 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillContentBuilderTests.cs diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs index 8a74fa034e1..a86a0e98b3e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs @@ -49,13 +49,13 @@ internal AgentFileSkill( /// /// /// Returns the raw SKILL.md content. When the skill has scripts, a - /// <script_schemas> block is appended describing the argument format. + /// <available_scripts> 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.BuildScriptSchemasBlock(this._scripts) + ? this._originalContent + AgentInlineSkillContentBuilder.BuildAvailableScriptsBlock(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 3572bff8268..75309d759c1 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs @@ -120,6 +120,7 @@ protected AgentClassSkill(Func? argumentMarsh this.Frontmatter.Name, this.Frontmatter.Description, this.Instructions, + this.Resources, this.Scripts)); } @@ -160,8 +161,9 @@ protected AgentClassSkill(Func? argumentMarsh /// 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. + /// Resources are listed in the <available_resources> block of the skill body so the LLM + /// knows which ones can be accessed. When empty, a self-closing element is emitted to prevent + /// hallucinated resource calls. /// /// public virtual IReadOnlyList? Resources => this._resources.Value; @@ -178,8 +180,9 @@ protected AgentClassSkill(Func? argumentMarsh /// 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. + /// Scripts are listed in the <available_scripts> block of the skill body so the LLM + /// knows which ones can be called. When empty, a self-closing element is emitted to prevent + /// hallucinated script calls. /// /// public virtual IReadOnlyList? Scripts => this._scripts.Value; @@ -202,8 +205,8 @@ protected AgentClassSkill(Func? argumentMarsh /// 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 is listed in the <available_resources> block of the skill body so the LLM + /// knows it can be accessed; an empty block prevents hallucinated resource calls. /// /// The resource name. /// The static resource value. @@ -216,8 +219,8 @@ protected AgentSkillResource CreateResource(string name, object value, string? d /// 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 is listed in the <available_resources> block of the skill body so the LLM + /// knows it can be accessed; an empty block prevents hallucinated resource calls. /// /// The resource name. /// A method that produces the resource value when requested. @@ -234,8 +237,8 @@ protected AgentSkillResource CreateResource(string name, Delegate method, string /// 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 is listed in the <available_scripts> block of the skill body so the LLM + /// knows it can be called; an empty block prevents hallucinated script calls. /// /// The script name. /// A method to execute when the script is invoked. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs index ce65777b8e9..7a5093830fe 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs @@ -107,7 +107,7 @@ public AgentInlineSkill( /// public override ValueTask GetContentAsync(CancellationToken cancellationToken = default) { - return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._scripts)); + return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts)); } /// @@ -128,8 +128,9 @@ public override ValueTask GetContentAsync(CancellationToken cancellation /// 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 is listed in the <available_resources> block of the skill body so the + /// LLM knows it can be accessed. When no resources are registered, the block is emitted as a + /// self-closing element to signal that none exist, preventing hallucinated resource calls. /// /// The resource name. /// The static resource value. @@ -146,8 +147,9 @@ public AgentInlineSkill AddResource(string name, object value, string? descripti /// 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 is listed in the <available_resources> block of the skill body so the + /// LLM knows it can be accessed. When no resources are registered, the block is emitted as a + /// self-closing element to signal that none exist, preventing hallucinated resource calls. /// /// The resource name. /// A method that produces the resource value when requested. @@ -168,8 +170,9 @@ public AgentInlineSkill AddResource(string name, Delegate method, string? descri /// 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 is listed in the <available_scripts> block of the skill body so the + /// LLM knows it can be called. When no scripts are registered, the block is emitted as a + /// self-closing element to signal that none exist, preventing hallucinated script calls. /// /// The script name. /// A method to execute when the script is invoked. diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs index d2f27edadc5..95fbb3ef400 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs @@ -12,17 +12,19 @@ namespace Microsoft.Agents.AI; internal static class AgentInlineSkillContentBuilder { /// - /// Builds the complete skill content containing name, description, instructions, and script parameter schemas. + /// Builds the complete skill content containing name, description, instructions, resources, 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); @@ -37,24 +39,46 @@ public static string Build( .Append(EscapeXmlString(instructions)) .Append("\n"); + if (resources is { Count: > 0 }) + { + sb.Append("\n\n\n"); + foreach (var resource in resources) + { + sb.Append($" \n"); + } + + sb.Append(""); + } + else + { + // Emit an empty element so the model knows no resources are available and does not hallucinate resource names. + sb.Append("\n\n"); + } + if (scripts is { Count: > 0 }) { sb.Append('\n'); - sb.Append(BuildScriptSchemasBlock(scripts)); + sb.Append(BuildAvailableScriptsBlock(scripts)); + } + else + { + // Emit an empty element so the model knows no scripts are available and does not hallucinate script names. + sb.Append("\n\n"); } return sb.ToString(); } /// - /// 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. + /// Builds an <available_scripts>...</available_scripts> XML block for the given scripts. + /// Each script is emitted as a <script name="..."> element; when the script has a + /// parameter schema it is wrapped in a nested <parameters_schema> element, otherwise a + /// self-closing <script> element is used. This block lets the model know which scripts + /// can be called and how to format their arguments. /// /// The scripts to include in the block. - /// An XML string starting with \n<script_schemas>, or an empty string if the list is empty. - public static string BuildScriptSchemasBlock(IReadOnlyList scripts) + /// An XML string starting with \n<available_scripts>, or an empty string if the list is empty. + public static string BuildAvailableScriptsBlock(IReadOnlyList scripts) { _ = Throw.IfNull(scripts); @@ -64,7 +88,7 @@ public static string BuildScriptSchemasBlock(IReadOnlyList scr } var sb = new StringBuilder(); - sb.Append("\n\n"); + sb.Append("\n\n"); foreach (var script in scripts) { @@ -72,15 +96,17 @@ public static string BuildScriptSchemasBlock(IReadOnlyList scr if (parametersSchema is null) { - sb.Append($" \n"); + sb.Append($" \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 17bf71a3886..5fd1fa77bab 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs @@ -382,9 +382,10 @@ public async Task AttributedFullSkill_IncludesContentWithSchema_AndCachesMembers // Arrange var skill = new AttributedFullSkill(); - // Act & Assert — Content no longer includes resources in body; scripts are in script_schemas - Assert.DoesNotContain("", await skill.GetContentAsync()); - Assert.Contains("", await skill.GetContentAsync()); + // Act & Assert — Content includes resources in body; scripts are in available_scripts + Assert.Contains("", await skill.GetContentAsync()); + Assert.Contains("conversion-table", await skill.GetContentAsync()); + Assert.Contains("", await skill.GetContentAsync()); Assert.Contains("convert", await skill.GetContentAsync()); // Act & Assert — discovered members are cached @@ -502,7 +503,7 @@ public async Task SerializerOptions_UsedForReflectedMembersAsync() } [Fact] - public async Task Content_DoesNotRenderResources_InBodyAsync() + public async Task Content_RendersResources_InBodyAsync() { // Arrange var skill = new AttributedResourcePropertiesSkill(); @@ -510,8 +511,10 @@ public async Task Content_DoesNotRenderResources_InBodyAsync() // Act var content = await skill.GetContentAsync(); - // Assert — resources are no longer rendered in body content - Assert.DoesNotContain("", content); + // Assert — resources are rendered in body content by name; descriptions are not emitted + Assert.Contains("", content); + Assert.Contains("ref-data", content); + Assert.DoesNotContain("Some important data.", 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 aa001fd2a0c..6767b8f0f05 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs @@ -122,10 +122,10 @@ public async Task Content_WithScripts_AppendsPerScriptEntriesAsync() // Assert — content starts with original and appends per-script entries Assert.StartsWith("Original content", content); - Assert.Contains("", content); - Assert.Contains("", content); - Assert.Contains("", content); - Assert.Contains("", content); + Assert.Contains("", content); + Assert.Contains("", block); + Assert.DoesNotContain("&\"c", parametersSchema: null) }; + + // Act + var block = AgentInlineSkillContentBuilder.BuildAvailableScriptsBlock(scripts); + + // Assert + Assert.Contains("