// 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 ReadSkillResourceAsync(IList skills, string skillName, string resourceName, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(skillName)) { return "Error: Skill name cannot be empty."; } if (string.IsNullOrWhiteSpace(resourceName)) { return "Error: Resource name cannot be empty."; } var skill = skills.FirstOrDefault(skill => skill.Frontmatter.Name == skillName); if (skill == null) { return $"Error: Skill '{skillName}' not found."; } try { var resource = await skill.GetResourceAsync(resourceName, cancellationToken).ConfigureAwait(false); if (resource is null) { return $"Error: Resource '{resourceName}' not found in skill '{skillName}'."; } return await resource.ReadAsync(serviceProvider, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { LogResourceReadError(this._logger, skillName, resourceName, ex); return $"Error: Failed to read resource '{resourceName}' from skill '{skillName}'."; } } private async Task RunSkillScriptAsync(IList skills, string skillName, string scriptName, JsonElement? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(skillName)) { return "Error: Skill name cannot be empty."; } if (string.IsNullOrWhiteSpace(scriptName)) { return "Error: Script name cannot be empty."; } var skill = skills.FirstOrDefault(skill => skill.Frontmatter.Name == skillName); if (skill == null) { return $"Error: Skill '{skillName}' not found."; } try { var script = await skill.GetScriptAsync(scriptName, cancellationToken).ConfigureAwait(false); if (script is null) { return $"Error: Script '{scriptName}' not found in skill '{skillName}'."; } return await script.RunAsync(skill, arguments, serviceProvider, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { LogScriptExecutionError(this._logger, skillName, scriptName, ex); return $"Error: Failed to execute script '{scriptName}' from skill '{skillName}'."; } } /// /// Validates that a custom prompt template contains the required placeholder tokens. /// private static void ValidatePromptTemplate(string template, string paramName) { if (template.IndexOf(SkillsPlaceholder, StringComparison.Ordinal) < 0) { throw new ArgumentException( $"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}")] private static partial void LogSkillLoading(ILogger logger, string skillName); [LoggerMessage(LogLevel.Error, "Failed to read resource '{ResourceName}' from skill '{SkillName}'")] private static partial void LogResourceReadError(ILogger logger, string skillName, string resourceName, Exception exception); [LoggerMessage(LogLevel.Error, "Failed to execute script '{ScriptName}' from skill '{SkillName}'")] private static partial void LogScriptExecutionError(ILogger logger, string skillName, string scriptName, Exception exception); }