// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
///
/// An that exposes agent skills from one or more instances.
///
///
///
/// This provider implements the progressive disclosure pattern from the
/// Agent Skills specification:
///
///
/// Advertise — skill names and descriptions are injected into the system prompt.
/// Load — the full skill body is returned via the load_skill tool.
/// Read resources — supplementary content is read on demand via the read_skill_resource tool.
/// Run scripts — scripts are executed via the run_skill_script tool (when scripts exist).
///
///
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed partial class AgentSkillsProvider : AIContextProvider
{
///
/// Placeholder token for the generated skills list in the prompt template.
///
private const string SkillsPlaceholder = "{skills}";
///
/// Placeholder token for the script instructions in the prompt template.
///
private const string ScriptInstructionsPlaceholder = "{script_instructions}";
///
/// Placeholder token for the resource instructions in the prompt template.
///
private const string ResourceInstructionsPlaceholder = "{resource_instructions}";
private const string DefaultSkillsInstructionPrompt =
"""
You have access to skills containing domain-specific knowledge and capabilities.
Each skill provides specialized instructions, reference documents, and assets for specific tasks.
{skills}
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}
Only load what is needed, when it is needed.
""";
private readonly AgentSkillsSource _source;
private readonly AgentSkillsProviderOptions? _options;
private readonly ILogger _logger;
private Task? _contextTask;
///
/// Initializes a new instance of the class
/// that discovers file-based skills from a single directory.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
///
/// Path to search for skills.
/// Optional delegate that runs file-based scripts. Required only when skills contain scripts.
/// Optional options that control skill discovery behavior.
/// Optional provider configuration.
/// Optional logger factory.
public AgentSkillsProvider(
string skillPath,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? fileOptions = null,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this([Throw.IfNull(skillPath)], scriptRunner, fileOptions, options, loggerFactory)
{
}
///
/// Initializes a new instance of the class
/// that discovers file-based skills from multiple directories.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
///
/// Paths to search for skills.
/// Optional delegate that runs file-based scripts. Required only when skills contain scripts.
/// Optional options that control skill discovery behavior.
/// Optional provider configuration.
/// Optional logger factory.
public AgentSkillsProvider(
IEnumerable skillPaths,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? fileOptions = null,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this(
new DeduplicatingAgentSkillsSource(
new AgentFileSkillsSource(skillPaths, scriptRunner, fileOptions, loggerFactory),
loggerFactory),
options,
loggerFactory)
{
}
///
/// Initializes a new instance of the class.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
///
/// The skills to include.
public AgentSkillsProvider(params AgentSkill[] skills)
: this(skills as IEnumerable)
{
}
///
/// Initializes a new instance of the class.
/// Duplicate skill names are automatically deduplicated (first occurrence wins).
///
/// The skills to include.
/// Optional provider configuration.
/// Optional logger factory.
public AgentSkillsProvider(
IEnumerable skills,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
: this(
new DeduplicatingAgentSkillsSource(
new AgentInMemorySkillsSource(Throw.IfNull(skills)),
loggerFactory),
options,
loggerFactory)
{
}
///
/// Initializes a new instance of the class
/// from a custom . Unlike other constructors, this one does not
/// apply automatic deduplication, allowing callers to customize deduplication behavior via the source pipeline.
///
/// The skill source providing skills.
/// Optional configuration.
/// Optional logger factory.
public AgentSkillsProvider(AgentSkillsSource source, AgentSkillsProviderOptions? options = null, ILoggerFactory? loggerFactory = null)
{
this._source = Throw.IfNull(source);
this._options = options;
this._logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger();
if (options?.SkillsInstructionPrompt is string prompt)
{
ValidatePromptTemplate(prompt, nameof(options));
}
}
///
protected override async ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
if (this._options?.DisableCaching == true)
{
return await this.CreateContextAsync(context, cancellationToken).ConfigureAwait(false);
}
return await this.GetOrCreateContextAsync(context, cancellationToken).ConfigureAwait(false);
}
private async Task CreateContextAsync(InvokingContext context, CancellationToken cancellationToken)
{
var skills = await this._source.GetSkillsAsync(cancellationToken).ConfigureAwait(false);
if (skills is not { Count: > 0 })
{
return await base.ProvideAIContextAsync(context, cancellationToken).ConfigureAwait(false);
}
return new AIContext
{
Instructions = this.BuildSkillsInstructions(skills),
Tools = this.BuildTools(skills),
};
}
private async Task GetOrCreateContextAsync(InvokingContext context, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
if (Interlocked.CompareExchange(ref this._contextTask, tcs.Task, null) is { } existing)
{
return await existing.ConfigureAwait(false);
}
try
{
var result = await this.CreateContextAsync(context, cancellationToken).ConfigureAwait(false);
tcs.SetResult(result);
return result;
}
catch (Exception ex)
{
this._contextTask = null;
tcs.TrySetException(ex);
throw;
}
}
private IList BuildTools(IList skills)
{
IList tools =
[
AIFunctionFactory.Create(
(string skillName, CancellationToken cancellationToken) => this.LoadSkillAsync(skills, skillName, cancellationToken),
name: "load_skill",
description: "Loads the full content of a specific skill"),
AIFunctionFactory.Create(
(string skillName, string resourceName, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default) =>
this.ReadSkillResourceAsync(skills, skillName, resourceName, serviceProvider, cancellationToken),
name: "read_skill_resource",
description: "Reads a resource associated with a skill, such as references, assets, or dynamic data."),
];
AIFunction scriptFunction = AIFunctionFactory.Create(
(string skillName, string scriptName, JsonElement? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) =>
this.RunSkillScriptAsync(skills, skillName, scriptName, arguments, serviceProvider, cancellationToken),
name: "run_skill_script",
description: "Runs a script associated with a skill.");
if (this._options?.ScriptApproval == true)
{
return [.. tools, new ApprovalRequiredAIFunction(scriptFunction)];
}
return [.. tools, scriptFunction];
}
private string? BuildSkillsInstructions(IList skills)
{
string promptTemplate = this._options?.SkillsInstructionPrompt ?? DefaultSkillsInstructionPrompt;
var sb = new StringBuilder();
foreach (var skill in skills.OrderBy(s => s.Frontmatter.Name, StringComparer.Ordinal))
{
sb.AppendLine(" ");
sb.AppendLine($" {SecurityElement.Escape(skill.Frontmatter.Name)}");
sb.AppendLine($" {SecurityElement.Escape(skill.Frontmatter.Description)}");
sb.AppendLine(" ");
}
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();
}
private async Task LoadSkillAsync(IList skills, string skillName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(skillName))
{
return "Error: Skill name cannot be empty.";
}
var skill = skills.FirstOrDefault(skill => skill.Frontmatter.Name == skillName);
if (skill == null)
{
return $"Error: Skill '{skillName}' not found.";
}
LogSkillLoading(this._logger, skillName);
return await skill.GetContentAsync(cancellationToken).ConfigureAwait(false);
}
private async Task