From e5f7b9c260961916e108ca10780988aeefd51662 Mon Sep 17 00:00:00 2001
From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:56:28 +0100
Subject: [PATCH] .NET: Support reflection for discovery of resources and
scripts in class-based skills (#5183)
* support reflection for discovery of resources and scripts in class-based skills
* fix format issues
* refactor samples to use reflection
* Validate resource member signatures during discovery
Add discovery-time validation in AgentClassSkill.DiscoverResources() to
fail fast when [AgentSkillResource] is applied to members with incompatible
signatures:
- Reject indexer properties (getter has parameters)
- Reject methods with parameters other than IServiceProvider or
CancellationToken
Throws InvalidOperationException with actionable error messages instead of
allowing silent runtime failures when ReadAsync invokes the AIFunction with
no named arguments.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* prevent duplicates
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Agent_Step03_ClassBasedSkills.csproj | 2 +-
.../Agent_Step03_ClassBasedSkills/Program.cs | 69 +-
.../Agent_Step03_ClassBasedSkills/README.md | 8 +-
.../Agent_Step04_MixedSkills.csproj | 2 +-
.../Agent_Step04_MixedSkills/Program.cs | 55 +-
.../Agent_Step05_SkillsWithDI.csproj | 2 +-
.../Agent_Step05_SkillsWithDI/Program.cs | 54 +-
.../Skills/AgentSkillsProviderBuilder.cs | 2 +-
.../Skills/Programmatic/AgentClassSkill.cs | 254 +++-
.../Programmatic/AgentInlineSkillResource.cs | 23 +
.../Programmatic/AgentInlineSkillScript.cs | 22 +
.../AgentSkillResourceAttribute.cs | 73 ++
.../Programmatic/AgentSkillScriptAttribute.cs | 72 ++
.../AgentSkills/AgentClassSkillTests.cs | 1034 +++++++++++++----
.../AgentInlineSkillResourceTests.cs | 55 +
.../AgentInlineSkillScriptTests.cs | 74 ++
.../AgentSkillResourceAttributeTests.cs | 29 +
.../AgentSkillScriptAttributeTests.cs | 29 +
.../AgentSkills/AgentSkillsProviderTests.cs | 4 +-
19 files changed, 1536 insertions(+), 327 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentSkillResourceAttribute.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentSkillScriptAttribute.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillResourceAttributeTests.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillScriptAttributeTests.cs
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj
index fd3d71fe7e..d7233702ac 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Agent_Step03_ClassBasedSkills.csproj
@@ -6,7 +6,7 @@
enableenable
- $(NoWarn);MAAI001
+ $(NoWarn);MAAI001;IDE0051
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
index 8be991598a..7f5e356a60 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/Program.cs
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
-// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill.
-// Class-based skills bundle all components into a single class implementation.
+// This sample demonstrates how to define Agent Skills as C# classes using AgentClassSkill
+// with attributes for automatic script and resource discovery.
+using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
@@ -44,17 +45,16 @@ AgentResponse response = await agent.RunAsync(
Console.WriteLine($"Agent: {response.Text}");
///
-/// A unit-converter skill defined as a C# class.
+/// A unit-converter skill defined as a C# class using attributes for discovery.
///
///
-/// Class-based skills bundle all components (name, description, body, resources, scripts)
-/// into a single class.
+/// Properties annotated with are automatically
+/// discovered as skill resources, and methods annotated with
+/// are automatically discovered as skill scripts. Alternatively,
+/// and can be overridden.
///
-internal sealed class UnitConverterSkill : AgentClassSkill
+internal sealed class UnitConverterSkill : AgentClassSkill
{
- private IReadOnlyList? _resources;
- private IReadOnlyList? _scripts;
-
///
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"unit-converter",
@@ -69,31 +69,40 @@ internal sealed class UnitConverterSkill : AgentClassSkill
3. Present the result clearly with both units.
""";
- ///
- public override IReadOnlyList? Resources => this._resources ??=
- [
- CreateResource(
- "conversion-table",
- """
- # Conversion Tables
+ ///
+ /// Gets the used to marshal parameters and return values
+ /// for scripts and resources.
+ ///
+ ///
+ /// This override is not necessary for this sample, but can be used to provide custom
+ /// serialization options, for example a source-generated JsonTypeInfoResolver
+ /// for Native AOT compatibility.
+ ///
+ protected override JsonSerializerOptions? SerializerOptions => null;
- Formula: **result = value × factor**
+ ///
+ /// A conversion table resource providing multiplication factors.
+ ///
+ [AgentSkillResource("conversion-table")]
+ [Description("Lookup table of multiplication factors for common unit conversions.")]
+ public string ConversionTable => """
+ # Conversion Tables
- | From | To | Factor |
- |-------------|-------------|----------|
- | miles | kilometers | 1.60934 |
- | kilometers | miles | 0.621371 |
- | pounds | kilograms | 0.453592 |
- | kilograms | pounds | 2.20462 |
- """),
- ];
+ Formula: **result = value × factor**
- ///
- public override IReadOnlyList? Scripts => this._scripts ??=
- [
- CreateScript("convert", ConvertUnits),
- ];
+ | From | To | Factor |
+ |-------------|-------------|----------|
+ | miles | kilometers | 1.60934 |
+ | kilometers | miles | 0.621371 |
+ | pounds | kilograms | 0.453592 |
+ | kilograms | pounds | 2.20462 |
+ """;
+ ///
+ /// Converts a value by the given factor.
+ ///
+ [AgentSkillScript("convert")]
+ [Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
private static string ConvertUnits(double value, double factor)
{
double result = Math.Round(value * factor, 4);
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md
index 3525bb7a98..028cb05a37 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step03_ClassBasedSkills/README.md
@@ -1,12 +1,16 @@
# Class-Based Agent Skills Sample
-This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`.
+This sample demonstrates how to define **Agent Skills as C# classes** using `AgentClassSkill`
+with **attributes** for automatic script and resource discovery.
## What it demonstrates
- Creating skills as classes that extend `AgentClassSkill`
-- Bundling name, description, body, resources, and scripts into a single class
+- Using `[AgentSkillResource]` on properties to define resources
+- Using `[AgentSkillScript]` on methods to define scripts
+- Automatic discovery (no need to override `Resources`/`Scripts`)
- Using the `AgentSkillsProvider` constructor with class-based skills
+- Overriding `SerializerOptions` for Native AOT compatibility
## Skills Included
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj
index 7e7e9ef0fa..01abf37da8 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Agent_Step04_MixedSkills.csproj
@@ -6,7 +6,7 @@
enableenable
- $(NoWarn);MAAI001
+ $(NoWarn);MAAI001;IDE0051
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
index ab5da71a3c..28d5cb9ee9 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step04_MixedSkills/Program.cs
@@ -8,11 +8,12 @@
// Three different skill sources are registered here:
// 1. File-based: unit-converter (miles↔km, pounds↔kg) from SKILL.md on disk
// 2. Code-defined: volume-converter (gallons↔liters) using AgentInlineSkill
-// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill
+// 3. Class-based: temperature-converter (°F↔°C↔K) using AgentClassSkill with attributes
//
// For simpler, single-source scenarios, see the earlier steps in this sample series
// (e.g., Step01 for file-based, Step02 for code-defined, Step03 for class-based).
+using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
@@ -89,13 +90,15 @@ AgentResponse response = await agent.RunAsync(
Console.WriteLine($"Agent: {response.Text}");
///
-/// A temperature-converter skill defined as a C# class.
+/// A temperature-converter skill defined as a C# class using attributes for discovery.
///
-internal sealed class TemperatureConverterSkill : AgentClassSkill
+///
+/// Properties annotated with are automatically
+/// discovered as skill resources, and methods annotated with
+/// are automatically discovered as skill scripts.
+///
+internal sealed class TemperatureConverterSkill : AgentClassSkill
{
- private IReadOnlyList? _resources;
- private IReadOnlyList? _scripts;
-
///
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"temperature-converter",
@@ -110,29 +113,27 @@ internal sealed class TemperatureConverterSkill : AgentClassSkill
3. Present the result clearly with both temperature scales.
""";
- ///
- public override IReadOnlyList? Resources => this._resources ??=
- [
- CreateResource(
- "temperature-conversion-formulas",
- """
- # Temperature Conversion Formulas
+ ///
+ /// A reference table of temperature conversion formulas.
+ ///
+ [AgentSkillResource("temperature-conversion-formulas")]
+ [Description("Formulas for converting between Fahrenheit, Celsius, and Kelvin.")]
+ public string ConversionFormulas => """
+ # Temperature Conversion Formulas
- | From | To | Formula |
- |-------------|-------------|---------------------------|
- | Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
- | Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
- | Celsius | Kelvin | K = °C + 273.15 |
- | Kelvin | Celsius | °C = K − 273.15 |
- """),
- ];
-
- ///
- public override IReadOnlyList? Scripts => this._scripts ??=
- [
- CreateScript("convert-temperature", ConvertTemperature),
- ];
+ | From | To | Formula |
+ |-------------|-------------|---------------------------|
+ | Fahrenheit | Celsius | °C = (°F − 32) × 5/9 |
+ | Celsius | Fahrenheit | °F = (°C × 9/5) + 32 |
+ | Celsius | Kelvin | K = °C + 273.15 |
+ | Kelvin | Celsius | °C = K − 273.15 |
+ """;
+ ///
+ /// Converts a temperature value between scales.
+ ///
+ [AgentSkillScript("convert-temperature")]
+ [Description("Converts a temperature value from one scale to another.")]
private static string ConvertTemperature(double value, string from, string to)
{
double result = (from.ToUpperInvariant(), to.ToUpperInvariant()) switch
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj
index 959fa29167..699672ded5 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Agent_Step05_SkillsWithDI.csproj
@@ -6,7 +6,7 @@
enableenable
- $(NoWarn);MAAI001;CA1812
+ $(NoWarn);MAAI001;CA1812;IDE0051
diff --git a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
index 50b0545be3..251503a918 100644
--- a/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
+++ b/dotnet/samples/02-agents/AgentSkills/Agent_Step05_SkillsWithDI/Program.cs
@@ -13,6 +13,7 @@
// showing that DI works identically regardless of how the skill is defined.
// When prompted with a question spanning both domains, the agent uses both skills.
+using System.ComponentModel;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
@@ -62,8 +63,8 @@ var distanceSkill = new AgentInlineSkill(
// Approach 2: Class-Based Skill with DI (AgentClassSkill)
// =====================================================================
// Handles weight conversions (pounds ↔ kilograms).
-// Resources and scripts are encapsulated in a class. Factory methods
-// CreateResource and CreateScript accept delegates with IServiceProvider.
+// Resources and scripts are discovered via reflection using attributes.
+// Methods with an IServiceProvider parameter receive DI automatically.
//
// Alternatively, class-based skills can accept dependencies through their
// constructor. Register the skill class itself in the ServiceCollection and
@@ -113,14 +114,13 @@ Console.WriteLine($"Agent: {response.Text}");
///
///
/// This skill resolves from the DI container
-/// in both its resource and script functions. This enables clean separation of
-/// concerns and testability while retaining the class-based skill pattern.
+/// in both its resource and script methods. Methods with an
+/// parameter are automatically injected by the framework. Properties and methods annotated
+/// with and
+/// are automatically discovered via reflection.
///
-internal sealed class WeightConverterSkill : AgentClassSkill
+internal sealed class WeightConverterSkill : AgentClassSkill
{
- private IReadOnlyList? _resources;
- private IReadOnlyList? _scripts;
-
///
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"weight-converter",
@@ -135,25 +135,27 @@ internal sealed class WeightConverterSkill : AgentClassSkill
3. Present the result clearly with both units.
""";
- ///
- public override IReadOnlyList? Resources => this._resources ??=
- [
- CreateResource("weight-table", (IServiceProvider serviceProvider) =>
- {
- var service = serviceProvider.GetRequiredService();
- return service.GetWeightTable();
- }),
- ];
+ ///
+ /// Returns the weight conversion table from the DI-registered .
+ ///
+ [AgentSkillResource("weight-table")]
+ [Description("Lookup table of multiplication factors for weight conversions.")]
+ private static string GetWeightTable(IServiceProvider serviceProvider)
+ {
+ var service = serviceProvider.GetRequiredService();
+ return service.GetWeightTable();
+ }
- ///
- public override IReadOnlyList? Scripts => this._scripts ??=
- [
- CreateScript("convert", (double value, double factor, IServiceProvider serviceProvider) =>
- {
- var service = serviceProvider.GetRequiredService();
- return service.Convert(value, factor);
- }),
- ];
+ ///
+ /// Converts a value by the given factor using the DI-registered .
+ ///
+ [AgentSkillScript("convert")]
+ [Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
+ private static string Convert(double value, double factor, IServiceProvider serviceProvider)
+ {
+ var service = serviceProvider.GetRequiredService();
+ return service.Convert(value, factor);
+ }
}
// ---------------------------------------------------------------------------
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs
index 0da54d0426..e49c620187 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProviderBuilder.cs
@@ -21,7 +21,7 @@ namespace Microsoft.Agents.AI;
///
///
/// Mixed skill types — combine file-based, code-defined (),
-/// and class-based () skills in a single provider.
+/// and class-based () skills in a single provider.
/// Multiple file script runners — use different script runners for different
/// file skill directories via per-source scriptRunner parameters on
/// / .
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs
index 4febb8bc79..b44f423bc2 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs
@@ -1,8 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
using System.Text.Json;
+using System.Threading;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
@@ -11,17 +15,55 @@ namespace Microsoft.Agents.AI;
///
/// Abstract base class for defining skills as C# classes that bundle all components together.
///
+///
+/// The concrete skill type. This type parameter is annotated with
+/// to ensure that the IL trimmer and Native AOT compiler
+/// preserve the members needed for attribute-based discovery.
+///
///
///
/// Inherit from this class to create a self-contained skill definition. Override the abstract
-/// properties to provide name, description, and instructions. Use ,
-/// , and to define
-/// inline resources and scripts.
+/// properties to provide name, description, and instructions.
+///
+///
+/// Scripts and resources can be defined in two ways:
+///
+///
+/// Attribute-based (recommended): Annotate methods with to define scripts,
+/// and properties or methods with to define resources. These are automatically
+/// discovered via reflection on . This approach is compatible with Native AOT.
+///
+///
+/// Explicit override: Override and , using
+/// , ,
+/// and to define inline resources and scripts. This approach is also compatible with Native AOT.
+///
+///
+///
+///
+/// Multi-level inheritance limitation: Discovery reflects only on ,
+/// so if a further-derived subclass adds new attributed members, they will not be discovered unless
+/// that subclass also uses the CRTP pattern
+/// (e.g., class SpecialSkill : AgentClassSkill<SpecialSkill>).
///
///
///
///
-/// public class PdfFormatterSkill : AgentClassSkill
+/// // Attribute-based approach (recommended, AOT-compatible):
+/// public class PdfFormatterSkill : AgentClassSkill<PdfFormatterSkill>
+/// {
+/// public override AgentSkillFrontmatter Frontmatter { get; } = new("pdf-formatter", "Format documents as PDF.");
+/// protected override string Instructions => "Use this skill to format documents...";
+///
+/// [AgentSkillResource("template")]
+/// public string Template => "Use this template...";
+///
+/// [AgentSkillScript("format-pdf")]
+/// private static string FormatPdf(string content) => content;
+/// }
+///
+/// // Explicit override approach (AOT-compatible):
+/// public class ExplicitPdfFormatterSkill : AgentClassSkill<ExplicitPdfFormatterSkill>
/// {
/// private IReadOnlyList<AgentSkillResource>? _resources;
/// private IReadOnlyList<AgentSkillScript>? _scripts;
@@ -44,15 +86,41 @@ namespace Microsoft.Agents.AI;
///
///
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
-public abstract class AgentClassSkill : AgentSkill
+public abstract class AgentClassSkill<
+ [DynamicallyAccessedMembers(
+ DynamicallyAccessedMemberTypes.PublicProperties |
+ DynamicallyAccessedMemberTypes.NonPublicProperties |
+ DynamicallyAccessedMemberTypes.PublicMethods |
+ DynamicallyAccessedMemberTypes.NonPublicMethods)] TSelf>
+ : AgentSkill
+ where TSelf : AgentClassSkill
{
+ private const BindingFlags DiscoveryBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
+
private string? _content;
+ private bool _resourcesDiscovered;
+ private bool _scriptsDiscovered;
+ private IReadOnlyList? _reflectedResources;
+ private IReadOnlyList? _reflectedScripts;
///
/// Gets the raw instructions text for this skill.
///
protected abstract string Instructions { get; }
+ ///
+ /// Gets the used to marshal parameters and return values
+ /// for scripts and resources.
+ ///
+ ///
+ /// Override this property to provide custom serialization options. This value is used by
+ /// reflection-discovered scripts and resources, and also as a fallback by
+ /// and when no
+ /// explicit is passed to those methods.
+ /// The default value is , which causes to be used.
+ ///
+ protected virtual JsonSerializerOptions? SerializerOptions => null;
+
///
///
/// Returns a synthesized XML document containing name, description, instructions, resources, and scripts.
@@ -65,6 +133,48 @@ public abstract class AgentClassSkill : AgentSkill
this.Resources,
this.Scripts);
+ ///
+ ///
+ /// Returns resources discovered via reflection by scanning for
+ /// members annotated with . This discovery is
+ /// compatible with Native AOT because is annotated with
+ /// . The result is cached after the first access.
+ ///
+ public override IReadOnlyList? Resources
+ {
+ get
+ {
+ if (!this._resourcesDiscovered)
+ {
+ this._reflectedResources = this.DiscoverResources();
+ this._resourcesDiscovered = true;
+ }
+
+ return this._reflectedResources;
+ }
+ }
+
+ ///
+ ///
+ /// Returns scripts discovered via reflection by scanning for
+ /// methods annotated with . This discovery is
+ /// compatible with Native AOT because is annotated with
+ /// . The result is cached after the first access.
+ ///
+ public override IReadOnlyList? Scripts
+ {
+ get
+ {
+ if (!this._scriptsDiscovered)
+ {
+ this._reflectedScripts = this.DiscoverScripts();
+ this._scriptsDiscovered = true;
+ }
+
+ return this._reflectedScripts;
+ }
+ }
+
///
/// Creates a skill resource backed by a static value.
///
@@ -72,7 +182,7 @@ public abstract class AgentClassSkill : AgentSkill
/// The static resource value.
/// An optional description of the resource.
/// A new instance.
- protected static AgentSkillResource CreateResource(string name, object value, string? description = null)
+ protected AgentSkillResource CreateResource(string name, object value, string? description = null)
=> new AgentInlineSkillResource(name, value, description);
///
@@ -83,11 +193,11 @@ public abstract class AgentClassSkill : AgentSkill
/// An optional description of the resource.
///
/// Optional used to marshal the delegate's parameters and return value.
- /// When , is used.
+ /// When , falls back to .
///
/// A new instance.
- protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
- => new AgentInlineSkillResource(name, method, description, serializerOptions);
+ protected AgentSkillResource CreateResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
+ => new AgentInlineSkillResource(name, method, description, serializerOptions ?? this.SerializerOptions);
///
/// Creates a skill script backed by a delegate.
@@ -97,9 +207,129 @@ public abstract class AgentClassSkill : AgentSkill
/// An optional description of the script.
///
/// Optional used to marshal the delegate's parameters and return value.
- /// When , is used.
+ /// When , falls back to .
///
/// A new instance.
- protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
- => new AgentInlineSkillScript(name, method, description, serializerOptions);
+ protected AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
+ => new AgentInlineSkillScript(name, method, description, serializerOptions ?? this.SerializerOptions);
+
+ private List? DiscoverResources()
+ {
+ List? resources = null;
+
+ var selfType = typeof(TSelf);
+
+ // Discover resources from properties annotated with [AgentSkillResource].
+ foreach (var property in selfType.GetProperties(DiscoveryBindingFlags))
+ {
+ var attr = property.GetCustomAttribute();
+ if (attr is null)
+ {
+ continue;
+ }
+
+ var getter = property.GetGetMethod(nonPublic: true);
+ if (getter is null)
+ {
+ continue;
+ }
+
+ // Indexer properties have getter parameters and cannot be used as resources
+ // because ReadAsync invokes the underlying AIFunction with no named arguments.
+ if (getter.GetParameters().Length > 0)
+ {
+ throw new InvalidOperationException(
+ $"Property '{property.Name}' on type '{selfType.Name}' is an indexer and cannot be used as a skill resource. " +
+ "Remove the [AgentSkillResource] attribute or use a non-indexer property.");
+ }
+
+ var name = attr.Name ?? property.Name;
+ if (resources?.Exists(r => r.Name == name) == true)
+ {
+ throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a resource named '{name}'. Ensure each [AgentSkillResource] has a unique name.");
+ }
+
+ resources ??= [];
+ resources.Add(new AgentInlineSkillResource(
+ name: name,
+ method: getter,
+ target: getter.IsStatic ? null : this,
+ description: property.GetCustomAttribute()?.Description,
+ serializerOptions: this.SerializerOptions));
+ }
+
+ // Discover resources from methods annotated with [AgentSkillResource].
+ foreach (var method in selfType.GetMethods(DiscoveryBindingFlags))
+ {
+ var attr = method.GetCustomAttribute();
+ if (attr is null)
+ {
+ continue;
+ }
+
+ ValidateResourceMethodParameters(method, selfType);
+
+ var name = attr.Name ?? method.Name;
+ if (resources?.Exists(r => r.Name == name) == true)
+ {
+ throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a resource named '{name}'. Ensure each [AgentSkillResource] has a unique name.");
+ }
+
+ resources ??= [];
+ resources.Add(new AgentInlineSkillResource(
+ name: name,
+ method: method,
+ target: method.IsStatic ? null : this,
+ description: method.GetCustomAttribute()?.Description,
+ serializerOptions: this.SerializerOptions));
+ }
+
+ return resources;
+ }
+
+ private static void ValidateResourceMethodParameters(MethodInfo method, Type skillType)
+ {
+ foreach (var param in method.GetParameters())
+ {
+ if (param.ParameterType != typeof(IServiceProvider) &&
+ param.ParameterType != typeof(CancellationToken))
+ {
+ throw new InvalidOperationException(
+ $"Method '{method.Name}' on type '{skillType.Name}' has parameter '{param.Name}' of type " +
+ $"'{param.ParameterType}' which cannot be supplied when reading a resource. " +
+ "Resource methods may only accept IServiceProvider and/or CancellationToken parameters. " +
+ "Remove the [AgentSkillResource] attribute or change the method signature.");
+ }
+ }
+ }
+
+ private List? DiscoverScripts()
+ {
+ List? scripts = null;
+
+ foreach (var method in typeof(TSelf).GetMethods(DiscoveryBindingFlags))
+ {
+ var attr = method.GetCustomAttribute();
+ if (attr is null)
+ {
+ continue;
+ }
+
+ var name = attr.Name ?? method.Name;
+ if (scripts?.Exists(s => s.Name == name) == true)
+ {
+ throw new InvalidOperationException($"Skill '{this.Frontmatter.Name}' already has a script named '{name}'. Ensure each [AgentSkillScript] has a unique name.");
+ }
+
+ scripts ??= [];
+ scripts.Add(new AgentInlineSkillScript(
+ name: name,
+ method: method,
+ target: method.IsStatic ? null : this,
+ description: method.GetCustomAttribute()?.Description,
+ serializerOptions: this.SerializerOptions));
+ }
+
+ return scripts;
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillResource.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillResource.cs
index 5e032f073f..556cfdc781 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillResource.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillResource.cs
@@ -2,6 +2,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -54,6 +55,28 @@ internal sealed class AgentInlineSkillResource : AgentSkillResource
this._function = AIFunctionFactory.Create(method, options);
}
+ ///
+ /// Initializes a new instance of the class from a .
+ /// The method is invoked via an each time is called,
+ /// producing a dynamic (computed) value.
+ ///
+ /// The resource name.
+ /// A method that produces the resource value when requested.
+ /// The target instance for instance methods, or for static methods.
+ /// An optional description of the resource.
+ ///
+ /// Optional used to marshal the method's parameters and return value.
+ /// When , is used.
+ ///
+ public AgentInlineSkillResource(string name, MethodInfo method, object? target, string? description = null, JsonSerializerOptions? serializerOptions = null)
+ : base(name, description)
+ {
+ Throw.IfNull(method);
+
+ var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
+ this._function = AIFunctionFactory.Create(method, target, options);
+ }
+
///
public override async Task