Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 3 additions & 36 deletions dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,6 @@ public sealed partial class AgentSkillsProvider : AIContextProvider
/// </summary>
private const string SkillsPlaceholder = "{skills}";

/// <summary>
/// Placeholder token for the script instructions in the prompt template.
/// </summary>
private const string ScriptInstructionsPlaceholder = "{script_instructions}";

/// <summary>
/// Placeholder token for the resource instructions in the prompt template.
/// </summary>
private const string ResourceInstructionsPlaceholder = "{resource_instructions}";

private const string DefaultSkillsInstructionPrompt =
"""
You have access to skills containing domain-specific knowledge and capabilities.
Expand All @@ -62,8 +52,9 @@ You have access to skills containing domain-specific knowledge and capabilities.
When a task aligns with a skill's domain, follow these steps in exact order:
- Use `load_skill` to retrieve the skill's instructions.
- Follow the provided guidance.
{resource_instructions}
{script_instructions}
- Use `read_skill_resource` to read any referenced resources, using the name exactly as listed
(e.g. `"style-guide"` not `"style-guide.md"`, `"references/FAQ.md"` not `"FAQ.md"`).
- Use `run_skill_script` to run referenced scripts, using the name exactly as listed.
Only load what is needed, when it is needed.
""";

Expand Down Expand Up @@ -258,18 +249,8 @@ private IList<AIFunction> BuildTools(IList<AgentSkill> skills)
sb.AppendLine(" </skill>");
}

const string ResourceInstruction =
"""
- Use `read_skill_resource` to read any referenced resources, using the name exactly as listed
(e.g. `"style-guide"` not `"style-guide.md"`, `"references/FAQ.md"` not `"FAQ.md"`).
""";

const string ScriptInstruction = "- Use `run_skill_script` to run referenced scripts, using the name exactly as listed.";

return new StringBuilder(promptTemplate)
.Replace(SkillsPlaceholder, sb.ToString().TrimEnd())
.Replace(ResourceInstructionsPlaceholder, ResourceInstruction)
.Replace(ScriptInstructionsPlaceholder, ScriptInstruction)
.ToString();
}

Expand Down Expand Up @@ -378,20 +359,6 @@ private static void ValidatePromptTemplate(string template, string paramName)
$"The custom prompt template must contain the '{SkillsPlaceholder}' placeholder for the generated skills list.",
paramName);
}

if (template.IndexOf(ResourceInstructionsPlaceholder, StringComparison.Ordinal) < 0)
{
throw new ArgumentException(
$"The custom prompt template must contain the '{ResourceInstructionsPlaceholder}' placeholder for resource instructions.",
paramName);
}

if (template.IndexOf(ScriptInstructionsPlaceholder, StringComparison.Ordinal) < 0)
{
throw new ArgumentException(
$"The custom prompt template must contain the '{ScriptInstructionsPlaceholder}' placeholder for script instructions.",
paramName);
}
}

[LoggerMessage(LogLevel.Information, "Loading skill: {SkillName}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ public AgentSkillsProviderBuilder UseSource(AgentSkillsSource source)
/// <summary>
/// Sets a custom system prompt template.
/// </summary>
/// <param name="promptTemplate">The prompt template with <c>{skills}</c> placeholder for the skills list,
/// <c>{resource_instructions}</c> for optional resource instructions,
/// and <c>{script_instructions}</c> for optional script instructions.</param>
/// <param name="promptTemplate">The prompt template with <c>{skills}</c> placeholder for the skills list.</param>
/// <returns>This builder instance for chaining.</returns>
public AgentSkillsProviderBuilder UsePromptTemplate(string promptTemplate)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ public sealed class AgentSkillsProviderOptions
{
/// <summary>
/// Gets or sets a custom system prompt template for advertising skills.
/// The template must contain <c>{skills}</c> as the placeholder for the generated skills list,
/// <c>{resource_instructions}</c> for resource instructions,
/// and <c>{script_instructions}</c> for script instructions.
/// The template must contain <c>{skills}</c> as the placeholder for the generated skills list.
/// When <see langword="null"/>, a default template is used.
/// </summary>
public string? SkillsInstructionPrompt { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public void Build_FluentChaining_ReturnsSameBuilder()
var result = builder
.UseSource(source)
.UseScriptApproval(false)
.UsePromptTemplate("Skills:\n{skills}\n{resource_instructions}\n{script_instructions}");
.UsePromptTemplate("Skills:\n{skills}");

// Assert
Assert.Same(builder, result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public async Task InvokingCoreAsync_CustomPromptTemplate_UsesCustomTemplateAsync
this.CreateSkill("custom-prompt-skill", "Custom prompt", "Body.");
var options = new AgentSkillsProviderOptions
{
SkillsInstructionPrompt = "Custom template: {skills}\n{resource_instructions}\n{script_instructions}"
SkillsInstructionPrompt = "Custom template: {skills}"
};
var provider = new AgentSkillsProvider(new AgentFileSkillsSource(this._testRoot, s_noOpExecutor), options);
var inputContext = new AIContext();
Expand All @@ -122,7 +122,7 @@ public void Constructor_PromptWithoutSkillsPlaceholder_ThrowsArgumentException()
// Arrange
var options = new AgentSkillsProviderOptions
{
SkillsInstructionPrompt = "No skills placeholder here {resource_instructions} {script_instructions}"
SkillsInstructionPrompt = "No skills placeholder here"
};

// Act & Assert
Expand All @@ -133,28 +133,12 @@ public void Constructor_PromptWithoutSkillsPlaceholder_ThrowsArgumentException()
}

[Fact]
public void Constructor_PromptWithoutRunnerInstructionsPlaceholder_ThrowsArgumentException()
public void Constructor_PromptWithOnlySkillsPlaceholder_Succeeds()
{
// Arrange
var options = new AgentSkillsProviderOptions
{
SkillsInstructionPrompt = "Has skills {skills} but no runner instructions {resource_instructions}"
};

// Act & Assert
var ex = Assert.Throws<ArgumentException>(() =>
new AgentSkillsProvider(new AgentFileSkillsSource(this._testRoot, s_noOpExecutor), options));
Assert.Contains("{script_instructions}", ex.Message);
Assert.Equal("options", ex.ParamName);
}

[Fact]
public void Constructor_PromptWithBothPlaceholders_Succeeds()
{
// Arrange
var options = new AgentSkillsProviderOptions
{
SkillsInstructionPrompt = "Skills: {skills}\nResources: {resource_instructions}\nRunner: {script_instructions}"
SkillsInstructionPrompt = "Skills: {skills}"
};

// Act — should not throw
Expand All @@ -165,19 +149,25 @@ public void Constructor_PromptWithBothPlaceholders_Succeeds()
}

[Fact]
public void Constructor_PromptWithoutResourceInstructionsPlaceholder_ThrowsArgumentException()
public async Task InvokingCoreAsync_CustomTemplateWithLegacyPlaceholders_RendersThemLiterallyAsync()
{
// Arrange
// Arrange — template contains legacy placeholder tokens that are no longer substituted
this.CreateSkill("literal-test-skill", "Literal test", "Body.");
var options = new AgentSkillsProviderOptions
{
SkillsInstructionPrompt = "Has skills {skills} and runner {script_instructions} but no resource instructions"
SkillsInstructionPrompt = "Skills: {skills}\nRes: {resource_instructions}\nScript: {script_instructions}"
};
var provider = new AgentSkillsProvider(new AgentFileSkillsSource(this._testRoot, s_noOpExecutor), options);
var inputContext = new AIContext();
var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, inputContext);

// Act & Assert
var ex = Assert.Throws<ArgumentException>(() =>
new AgentSkillsProvider(new AgentFileSkillsSource(this._testRoot, s_noOpExecutor), options));
Assert.Contains("{resource_instructions}", ex.Message);
Assert.Equal("options", ex.ParamName);
// Act
var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);

// Assert — legacy tokens render literally, not substituted
Assert.NotNull(result.Instructions);
Assert.Contains("{resource_instructions}", result.Instructions);
Assert.Contains("{script_instructions}", result.Instructions);
}

[Fact]
Expand Down
Loading