mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* .NET: Refactor AgentSkill API to async resource and script lookup Replace property-based AgentSkill.Content, Resources, and Scripts with async-by-name lookup methods plus boolean availability flags: - Content (string getter) -> GetContentAsync(CancellationToken) - Resources (full list) -> HasResources + GetResourceAsync(name, ct) - Scripts (full list) -> HasScripts + GetScriptAsync(name, ct) This makes the API friendlier for sources like MCP where enumerating all resources up front is expensive or impossible, and allows skill implementations to fetch content lazily. Subclass changes: - AgentFileSkill and AgentInlineSkill implement the new async API while preserving content caching. - AgentClassSkill<TSelf> keeps virtual Resources/Scripts properties for reflection-based discovery and seals the new HasResources/HasScripts/ GetResourceAsync/GetScriptAsync overrides. Its previously non-thread-safe lazy initialization is replaced with Lazy<T> (default thread-safety) wired up in a new protected constructor, so concurrent first-access from multiple threads is safe. - AgentSkillsProvider calls the new async API and exposes ead_skill_resource / load_skill / un_skill_script tools that await the per-name lookups. Includes baseline CompatibilitySuppressions.xml entries for the removed property getters. Tests: - Direct coverage for HasResources, HasScripts, GetResourceAsync, and GetScriptAsync on all three skill implementations (positive, missing-name, and no-resources/no-scripts cases). - Thread-safety regression test for AgentClassSkill<TSelf> that exercises concurrent first-access to Resources, Scripts, and GetContentAsync from many tasks and asserts all observers see the same cached instance. - Provider-level coverage for the ead_skill_resource tool (invocation + error paths) and for the previously untested error paths of load_skill and un_skill_script (empty names, skill/resource/script not found). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review comments - Move GetScriptAsync inside try/catch in RunSkillScriptAsync for error-handling parity - Remove dead _reflectedResources branch from AgentSkillTestExtensions - Fix XML docs to reference virtual Resources/Scripts properties (not sealed methods) - Add Async suffix to async test methods per naming convention - Make no-await tests synchronous to eliminate CS1998 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix formatting: add UTF-8 BOM and remove unused using Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix XML cref: Resources/Scripts are on AgentClassSkill<TSelf> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove HasResources and HasScripts properties from AgentSkill Drop the virtual HasResources and HasScripts properties from AgentSkill and all concrete subclasses (AgentFileSkill, AgentInlineSkill, AgentClassSkill). AgentSkillsProvider now always includes all three tools (load_skill, read_skill_resource, run_skill_script) and both instruction blocks, since the tools already handle missing resources/scripts gracefully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add blank line for readability in file-based skills sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix HostedAgentSkillsPatternTests for always-included tools Update assertions to expect read_skill_resource and run_skill_script tools are always present, matching the new behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
164 lines
7.9 KiB
C#
164 lines
7.9 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Shared.DiagnosticIds;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI;
|
|
|
|
/// <summary>
|
|
/// A skill defined entirely in code with resources (static values or delegates) and scripts (delegates).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// All calls to <see cref="AddResource(string, object, string?)"/>,
|
|
/// <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>, and <see cref="AddScript"/>
|
|
/// must be made before the skill's <see cref="GetContentAsync"/> is first called.
|
|
/// Calls made after that point will not be reflected in the generated
|
|
/// content. In typical usage, this means configuring all
|
|
/// resources and scripts before registering the skill with an
|
|
/// <see cref="AgentSkillsProvider"/> or <see cref="AgentSkillsProviderBuilder"/>.
|
|
/// </remarks>
|
|
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
|
public sealed class AgentInlineSkill : AgentSkill
|
|
{
|
|
private readonly string _instructions;
|
|
private readonly JsonSerializerOptions? _serializerOptions;
|
|
private List<AgentInlineSkillResource>? _resources;
|
|
private List<AgentInlineSkillScript>? _scripts;
|
|
private string? _cachedContent;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AgentInlineSkill"/> class
|
|
/// with a pre-built <see cref="AgentSkillFrontmatter"/>.
|
|
/// </summary>
|
|
/// <param name="frontmatter">The skill frontmatter containing name, description, and other metadata.</param>
|
|
/// <param name="instructions">Skill instructions text.</param>
|
|
/// <param name="serializerOptions">
|
|
/// Optional <see cref="JsonSerializerOptions"/> applied by default to all scripts and delegate resources
|
|
/// added to this skill. Individual <see cref="AddScript"/> and <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>
|
|
/// calls can override this default. When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
|
/// </param>
|
|
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions, JsonSerializerOptions? serializerOptions = null)
|
|
{
|
|
this.Frontmatter = Throw.IfNull(frontmatter);
|
|
this._instructions = Throw.IfNullOrWhitespace(instructions);
|
|
this._serializerOptions = serializerOptions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AgentInlineSkill"/> class
|
|
/// with all frontmatter properties specified individually.
|
|
/// </summary>
|
|
/// <param name="name">Skill name in kebab-case.</param>
|
|
/// <param name="description">Skill description for discovery.</param>
|
|
/// <param name="instructions">Skill instructions text.</param>
|
|
/// <param name="license">Optional license name or reference.</param>
|
|
/// <param name="compatibility">Optional compatibility information (max 500 chars).</param>
|
|
/// <param name="allowedTools">Optional space-delimited list of pre-approved tools.</param>
|
|
/// <param name="metadata">Optional arbitrary key-value metadata.</param>
|
|
/// <param name="serializerOptions">
|
|
/// Optional <see cref="JsonSerializerOptions"/> applied by default to all scripts and delegate resources
|
|
/// added to this skill. Individual <see cref="AddScript"/> and <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>
|
|
/// calls can override this default. When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
|
/// </param>
|
|
public AgentInlineSkill(
|
|
string name,
|
|
string description,
|
|
string instructions,
|
|
string? license = null,
|
|
string? compatibility = null,
|
|
string? allowedTools = null,
|
|
AdditionalPropertiesDictionary? metadata = null,
|
|
JsonSerializerOptions? serializerOptions = null)
|
|
: this(
|
|
new AgentSkillFrontmatter(name, description, compatibility)
|
|
{
|
|
License = license,
|
|
AllowedTools = allowedTools,
|
|
Metadata = metadata,
|
|
},
|
|
instructions,
|
|
serializerOptions)
|
|
{
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override AgentSkillFrontmatter Frontmatter { get; }
|
|
|
|
/// <inheritdoc/>
|
|
public override ValueTask<string> GetContentAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
return new(this._cachedContent ??= AgentInlineSkillContentBuilder.Build(this.Frontmatter.Name, this.Frontmatter.Description, this._instructions, this._resources, this._scripts));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override ValueTask<AgentSkillResource?> GetResourceAsync(string name, CancellationToken cancellationToken = default)
|
|
{
|
|
var resource = this._resources?.FirstOrDefault(r => r.Name == name);
|
|
return new(resource);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override ValueTask<AgentSkillScript?> GetScriptAsync(string name, CancellationToken cancellationToken = default)
|
|
{
|
|
var script = this._scripts?.FirstOrDefault(s => s.Name == name);
|
|
return new(script);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a static resource with this skill.
|
|
/// </summary>
|
|
/// <param name="name">The resource name.</param>
|
|
/// <param name="value">The static resource value.</param>
|
|
/// <param name="description">An optional description of the resource.</param>
|
|
/// <returns>This instance, for chaining.</returns>
|
|
public AgentInlineSkill AddResource(string name, object value, string? description = null)
|
|
{
|
|
(this._resources ??= []).Add(new AgentInlineSkillResource(name, value, description));
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a dynamic resource with this skill, backed by a C# delegate.
|
|
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
|
|
/// </summary>
|
|
/// <param name="name">The resource name.</param>
|
|
/// <param name="method">A method that produces the resource value when requested.</param>
|
|
/// <param name="description">An optional description of the resource.</param>
|
|
/// <param name="serializerOptions">
|
|
/// Optional <see cref="JsonSerializerOptions"/> for this resource's delegate marshaling.
|
|
/// When <see langword="null"/>, the skill-level default (if any) is used; otherwise <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
|
/// </param>
|
|
/// <returns>This instance, for chaining.</returns>
|
|
public AgentInlineSkill AddResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
|
{
|
|
(this._resources ??= []).Add(new AgentInlineSkillResource(name, method, description, serializerOptions ?? this._serializerOptions));
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a script with this skill, backed by a C# delegate.
|
|
/// The delegate's parameters and return type are automatically marshaled via <c>AIFunctionFactory</c>.
|
|
/// </summary>
|
|
/// <param name="name">The script name.</param>
|
|
/// <param name="method">A method to execute when the script is invoked.</param>
|
|
/// <param name="description">An optional description of the script.</param>
|
|
/// <param name="serializerOptions">
|
|
/// Optional <see cref="JsonSerializerOptions"/> for this script's delegate marshaling.
|
|
/// When <see langword="null"/>, the skill-level default (if any) is used; otherwise <see cref="AIJsonUtilities.DefaultOptions"/> is used.
|
|
/// </param>
|
|
/// <returns>This instance, for chaining.</returns>
|
|
public AgentInlineSkill AddScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
|
|
{
|
|
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description, serializerOptions ?? this._serializerOptions));
|
|
return this;
|
|
}
|
|
}
|