diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkillsProvider.cs index cd64cdc723..460faced70 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkillsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/FileAgentSkillsProvider.cs @@ -175,15 +175,23 @@ private async Task ReadSkillResourceAsync(string skillName, string resou try { _ = string.Format(optionsInstructions, string.Empty); - promptTemplate = optionsInstructions; } catch (FormatException ex) { throw new ArgumentException( - "The provided SkillsInstructionPrompt is not a valid format string. It must contain a '{0}' placeholder and escape any literal '{' or '}' by doubling them ('{{' or '}}').", + "The provided SkillsInstructionPrompt is not a valid format string.", nameof(options), ex); } + + if (optionsInstructions.IndexOf("{0}", StringComparison.Ordinal) < 0) + { + throw new ArgumentException( + "The provided SkillsInstructionPrompt must contain a '{0}' placeholder for the generated skills list.", + nameof(options)); + } + + promptTemplate = optionsInstructions; } if (skills.Count == 0) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillsProviderTests.cs index 92dc5a5418..5da49525d4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillsProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillsProviderTests.cs @@ -127,6 +127,42 @@ public void Constructor_InvalidPromptTemplate_ThrowsArgumentException() Assert.Equal("options", ex.ParamName); } + [Fact] + public void Constructor_PromptWithoutPlaceholder_ThrowsArgumentException() + { + // Arrange -- valid format string but missing the required placeholder + var options = new FileAgentSkillsProviderOptions + { + SkillsInstructionPrompt = "No placeholder here" + }; + + var ex = Assert.Throws(() => new FileAgentSkillsProvider(this._testRoot, options)); + Assert.Contains("{0}", ex.Message); + Assert.Equal("options", ex.ParamName); + } + + [Fact] + public async Task Constructor_PromptWithPlaceholder_AppliesCustomTemplateAsync() + { + // Arrange — valid custom template with {0} placeholder + this.CreateSkill("custom-tpl-skill", "Custom template skill", "Body."); + var options = new FileAgentSkillsProviderOptions + { + SkillsInstructionPrompt = "== Skills ==\n{0}\n== End ==" + }; + var provider = new FileAgentSkillsProvider(this._testRoot, options); + var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext()); + + // Act + var result = await provider.InvokingAsync(invokingContext, CancellationToken.None); + + // Assert — the custom template wraps the skill list + Assert.NotNull(result.Instructions); + Assert.StartsWith("== Skills ==", result.Instructions); + Assert.Contains("custom-tpl-skill", result.Instructions); + Assert.Contains("== End ==", result.Instructions); + } + [Fact] public async Task InvokingCoreAsync_SkillNamesAreXmlEscapedAsync() {