.NET: Support custom types in skill resource and script functions (#5152)

* .NET: Add JsonSerializerOptions support to programmatic skill APIs

Allow callers to pass custom JsonSerializerOptions when creating inline
resources and scripts via AgentInlineSkill, AgentClassSkill,
AgentInlineSkillResource, and AgentInlineSkillScript. A skill-level
default can be set on AgentInlineSkill and overridden per-resource/
script call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/TestSkillTypes.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2026-04-08 10:55:27 +01:00
committed by GitHub
Unverified
parent 18d1ba3624
commit 7010dd7439
10 changed files with 433 additions and 18 deletions
@@ -1,6 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,Microsoft.Extensions.AI.AdditionalPropertiesDictionary)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddResource(System.String,System.Delegate,System.String)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddScript(System.String,System.Delegate,System.String)</Target>
<Left>lib/net10.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
@@ -36,6 +64,34 @@
<Right>lib/net10.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,Microsoft.Extensions.AI.AdditionalPropertiesDictionary)</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddResource(System.String,System.Delegate,System.String)</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddScript(System.String,System.Delegate,System.String)</Target>
<Left>lib/net472/Microsoft.Agents.AI.dll</Left>
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
@@ -71,6 +127,34 @@
<Right>lib/net472/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,Microsoft.Extensions.AI.AdditionalPropertiesDictionary)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddResource(System.String,System.Delegate,System.String)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddScript(System.String,System.Delegate,System.String)</Target>
<Left>lib/net8.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
@@ -106,6 +190,34 @@
<Right>lib/net8.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,Microsoft.Extensions.AI.AdditionalPropertiesDictionary)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddResource(System.String,System.Delegate,System.String)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddScript(System.String,System.Delegate,System.String)</Target>
<Left>lib/net9.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
@@ -141,6 +253,34 @@
<Right>lib/net9.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(System.String,System.String,System.String,System.String,System.String,System.String,Microsoft.Extensions.AI.AdditionalPropertiesDictionary)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddResource(System.String,System.Delegate,System.String)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentInlineSkill.AddScript(System.String,System.Delegate,System.String)</Target>
<Left>lib/netstandard2.0/Microsoft.Agents.AI.dll</Left>
<Right>lib/netstandard2.0/Microsoft.Agents.AI.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])</Target>
@@ -2,6 +2,8 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
@@ -13,7 +15,7 @@ namespace Microsoft.Agents.AI;
/// <para>
/// Inherit from this class to create a self-contained skill definition. Override the abstract
/// properties to provide name, description, and instructions. Use <see cref="CreateResource(string, object, string?)"/>,
/// <see cref="CreateResource(string, Delegate, string?)"/>, and <see cref="CreateScript"/> to define
/// <see cref="CreateResource(string, Delegate, string?, JsonSerializerOptions?)"/>, and <see cref="CreateScript"/> to define
/// inline resources and scripts.
/// </para>
/// </remarks>
@@ -79,9 +81,13 @@ public abstract class AgentClassSkill : AgentSkill
/// <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"/> used to marshal the delegate's parameters and return value.
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
/// </param>
/// <returns>A new <see cref="AgentSkillResource"/> instance.</returns>
protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null)
=> new AgentInlineSkillResource(name, method, description);
protected static AgentSkillResource CreateResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
=> new AgentInlineSkillResource(name, method, description, serializerOptions);
/// <summary>
/// Creates a skill script backed by a delegate.
@@ -89,7 +95,11 @@ public abstract class AgentClassSkill : AgentSkill
/// <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"/> used to marshal the delegate's parameters and return value.
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
/// </param>
/// <returns>A new <see cref="AgentSkillScript"/> instance.</returns>
protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null)
=> new AgentInlineSkillScript(name, method, description);
protected static AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
=> new AgentInlineSkillScript(name, method, description, serializerOptions);
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -14,7 +15,7 @@ namespace Microsoft.Agents.AI;
/// </summary>
/// <remarks>
/// All calls to <see cref="AddResource(string, object, string?)"/>,
/// <see cref="AddResource(string, Delegate, string?)"/>, and <see cref="AddScript"/>
/// <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>, and <see cref="AddScript"/>
/// must be made before the skill's <see cref="Content"/> is first accessed.
/// Calls made after that point will not be reflected in the generated
/// <see cref="Content"/>. In typical usage, this means configuring all
@@ -25,6 +26,7 @@ namespace Microsoft.Agents.AI;
public sealed class AgentInlineSkill : AgentSkill
{
private readonly string _instructions;
private readonly JsonSerializerOptions? _serializerOptions;
private List<AgentInlineSkillResource>? _resources;
private List<AgentInlineSkillScript>? _scripts;
private string? _cachedContent;
@@ -35,10 +37,16 @@ public sealed class AgentInlineSkill : AgentSkill
/// </summary>
/// <param name="frontmatter">The skill frontmatter containing name, description, and other metadata.</param>
/// <param name="instructions">Skill instructions text.</param>
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions)
/// <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>
@@ -52,6 +60,11 @@ public sealed class AgentInlineSkill : AgentSkill
/// <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,
@@ -59,7 +72,8 @@ public sealed class AgentInlineSkill : AgentSkill
string? license = null,
string? compatibility = null,
string? allowedTools = null,
AdditionalPropertiesDictionary? metadata = null)
AdditionalPropertiesDictionary? metadata = null,
JsonSerializerOptions? serializerOptions = null)
: this(
new AgentSkillFrontmatter(name, description, compatibility)
{
@@ -67,7 +81,8 @@ public sealed class AgentInlineSkill : AgentSkill
AllowedTools = allowedTools,
Metadata = metadata,
},
instructions)
instructions,
serializerOptions)
{
}
@@ -103,10 +118,14 @@ public sealed class AgentInlineSkill : AgentSkill
/// <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)
public AgentInlineSkill AddResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
{
(this._resources ??= []).Add(new AgentInlineSkillResource(name, method, description));
(this._resources ??= []).Add(new AgentInlineSkillResource(name, method, description, serializerOptions ?? this._serializerOptions));
return this;
}
@@ -117,10 +136,14 @@ public sealed class AgentInlineSkill : AgentSkill
/// <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)
public AgentInlineSkill AddScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
{
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description));
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description, serializerOptions ?? this._serializerOptions));
return this;
}
}
@@ -2,6 +2,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -40,11 +41,17 @@ internal sealed class AgentInlineSkillResource : AgentSkillResource
/// <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>
public AgentInlineSkillResource(string name, Delegate method, string? description = null)
/// <param name="serializerOptions">
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the delegate's parameters and return value.
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
/// </param>
public AgentInlineSkillResource(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
: base(name, description)
{
Throw.IfNull(method);
this._function = AIFunctionFactory.Create(method, name: this.Name);
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
this._function = AIFunctionFactory.Create(method, options);
}
/// <inheritdoc/>
@@ -26,11 +26,17 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
/// <param name="name">The script name.</param>
/// <param name="method">A method to execute when the script is invoked. Parameters are automatically deserialized from JSON.</param>
/// <param name="description">An optional description of the script.</param>
public AgentInlineSkillScript(string name, Delegate method, string? description = null)
/// <param name="serializerOptions">
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the delegate's parameters and return value.
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
/// </param>
public AgentInlineSkillScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
: base(Throw.IfNullOrWhitespace(name), description)
{
Throw.IfNull(method);
this._function = AIFunctionFactory.Create(method, name: this.Name);
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
this._function = AIFunctionFactory.Create(method, options);
}
/// <summary>
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
@@ -374,9 +375,68 @@ public sealed class AgentClassSkillTests
{
}
public override Task<object?> RunAsync(AgentSkill skill, Extensions.AI.AIFunctionArguments arguments, CancellationToken cancellationToken = default)
public override Task<object?> RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
=> Task.FromResult<object?>("script-result");
}
#endregion
[Fact]
public async Task CreateScript_WithSerializerOptions_DeserializesCustomInputTypeAsync()
{
// Arrange
var skill = new CustomTypeSkill();
var jso = SkillTestJsonContext.Default.Options;
// Act — pass a custom type as JSON; the JSO enables deserialization
var script = skill.Scripts![0];
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso);
var args = new AIFunctionArguments { ["request"] = inputJson };
var result = await script.RunAsync(skill, args, CancellationToken.None);
// Assert — the custom input type was deserialized and the response was produced
Assert.NotNull(result);
var resultText = result!.ToString()!;
Assert.Contains("result for test", resultText);
Assert.Contains("5", resultText);
}
[Fact]
public async Task CreateResource_WithSerializerOptions_SerializesReturnsCustomTypeAsync()
{
// Arrange
var skill = new CustomTypeSkill();
// Act
var result = await skill.Resources![0].ReadAsync();
// Assert — the custom type was returned successfully
Assert.NotNull(result);
Assert.Contains("dark", result!.ToString()!);
}
private sealed class CustomTypeSkill : AgentClassSkill
{
public override AgentSkillFrontmatter Frontmatter { get; } = new("custom-type-skill", "Skill with custom-typed scripts and resources.");
protected override string Instructions => "Body.";
public override IReadOnlyList<AgentSkillResource>? Resources =>
[
CreateResource("config", () => new SkillConfig
{
Theme = "dark",
Verbose = true
}, serializerOptions: SkillTestJsonContext.Default.Options),
];
public override IReadOnlyList<AgentSkillScript>? Scripts =>
[
CreateScript("Lookup", (LookupRequest request) => new LookupResponse
{
Items = [$"result for {request.Query}"],
TotalCount = request.MaxResults,
}, serializerOptions: SkillTestJsonContext.Default.Options),
];
}
}
@@ -139,6 +139,21 @@ public sealed class AgentInlineSkillResourceTests
Assert.Equal("Dynamic resource.", resource.Description);
}
[Fact]
public async Task ReadAsync_WithSerializerOptions_SerializesReturnCustomTypeAsync()
{
// Arrange — delegate resource returns a custom type; the JSO includes a source-generated context for it
var jso = SkillTestJsonContext.Default.Options;
var resource = new AgentInlineSkillResource("config", () => new SkillConfig { Theme = "dark", Verbose = true }, serializerOptions: jso);
// Act
var result = await resource.ReadAsync();
// Assert — the custom type was returned successfully
Assert.NotNull(result);
Assert.Contains("dark", result!.ToString()!);
}
[Fact]
public async Task ReadAsync_SupportsCancellationTokenAsync()
{
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -115,6 +116,28 @@ public sealed class AgentInlineSkillScriptTests
new AgentInlineSkillScript("my-script", null!));
}
[Fact]
public async Task RunAsync_WithSerializerOptions_MarshalsCustomTypesAsync()
{
// Arrange — script accepts a custom type; the JSO includes a source-generated context for it
var jso = SkillTestJsonContext.Default.Options;
var script = new AgentInlineSkillScript("lookup", (LookupRequest request) => new LookupResponse
{
Items = ["result-1", "result-2"],
TotalCount = request.MaxResults,
}, serializerOptions: jso);
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso);
var args = new AIFunctionArguments { ["request"] = inputJson };
// Act
var result = await script.RunAsync(skill, args, CancellationToken.None);
// Assert — the custom input type was deserialized and the response was produced
Assert.NotNull(result);
Assert.Contains("5", result!.ToString()!);
}
[Fact]
public async Task RunAsync_StringParameter_WorksAsync()
{
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
@@ -417,4 +420,82 @@ public sealed class AgentInlineSkillTests
Assert.Contains("description=\"A described resource.\"", content);
Assert.DoesNotContain("no-desc\" description", content);
}
[Fact]
public async Task AddScript_SkillLevelSerializerOptions_AppliedToScriptAsync()
{
// Arrange — skill-level JSO with source-generated context for custom types
var jso = SkillTestJsonContext.Default.Options;
var skill = new AgentInlineSkill("jso-skill", "JSO test.", "Instructions.", serializerOptions: jso);
skill.AddScript("lookup", (LookupRequest request) => new LookupResponse
{
Items = [$"result for {request.Query}"],
TotalCount = request.MaxResults,
});
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso);
var args = new AIFunctionArguments { ["request"] = inputJson };
// Act
var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
// Assert — the custom input was deserialized via skill-level JSO and response was produced
Assert.NotNull(result);
Assert.Contains("result for test", result!.ToString()!);
}
[Fact]
public async Task AddScript_PerScriptSerializerOptions_OverridesSkillLevelAsync()
{
// Arrange — skill-level JSO uses snake_case naming; per-script JSO overrides with source-generated context
var skillJso = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
var scriptJso = SkillTestJsonContext.Default.Options;
var skill = new AgentInlineSkill("override-skill", "Override test.", "Instructions.", serializerOptions: skillJso);
skill.AddScript("lookup", (LookupRequest request) => new LookupResponse
{
Items = [$"found {request.Query}"],
TotalCount = request.MaxResults,
}, serializerOptions: scriptJso);
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "override", MaxResults = 7 }, scriptJso);
var args = new AIFunctionArguments { ["request"] = inputJson };
// Act
var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
// Assert — per-script JSO takes effect and custom types are properly marshaled
Assert.NotNull(result);
Assert.Contains("found override", result!.ToString()!);
}
[Fact]
public async Task AddResource_SkillLevelSerializerOptions_AppliedToDelegateResourceAsync()
{
// Arrange — skill-level JSO with source-generated context; delegate resource returns a custom type
var jso = SkillTestJsonContext.Default.Options;
var skill = new AgentInlineSkill("custom-type-resource-skill", "Custom type resource test.", "Instructions.", serializerOptions: jso);
skill.AddResource("config", () => new SkillConfig { Theme = "dark", Verbose = true });
// Act
var result = await skill.Resources![0].ReadAsync();
// Assert — the custom type was returned successfully via skill-level JSO
Assert.NotNull(result);
Assert.Contains("dark", result!.ToString()!);
}
[Fact]
public async Task AddResource_PerResourceSerializerOptions_OverridesSkillLevelAsync()
{
// Arrange — skill-level JSO uses snake_case naming; per-resource JSO overrides with source-generated context
var skillJso = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
var resourceJso = SkillTestJsonContext.Default.Options;
var skill = new AgentInlineSkill("override-resource-skill", "Override resource test.", "Instructions.", serializerOptions: skillJso);
skill.AddResource("config", () => new SkillConfig { Theme = "dark", Verbose = true }, serializerOptions: resourceJso);
// Act
var result = await skill.Resources![0].ReadAsync();
// Assert — per-resource JSO takes effect and custom type is properly marshaled
Assert.NotNull(result);
Assert.Contains("dark", result!.ToString()!);
}
}
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -70,3 +71,52 @@ internal sealed class TestAgentSkillsSource : AgentSkillsSource
return Task.FromResult(this._skills);
}
}
/// <summary>
/// Custom input type accepted by skill script delegates in JSO tests.
/// </summary>
internal sealed class LookupRequest
{
/// <summary>Gets or sets the search query.</summary>
public string Query { get; set; } = string.Empty;
/// <summary>Gets or sets the maximum number of results.</summary>
public int MaxResults { get; set; }
}
/// <summary>
/// Custom output type returned by skill script delegates in JSO tests.
/// </summary>
internal sealed class LookupResponse
{
/// <summary>Gets or sets the items found.</summary>
public IList<string> Items { get; set; } = [];
/// <summary>Gets or sets the total number of matches.</summary>
public int TotalCount { get; set; }
}
/// <summary>
/// Custom output type returned by skill resource delegates in JSO tests.
/// </summary>
internal sealed class SkillConfig
{
/// <summary>Gets or sets the theme name.</summary>
public string Theme { get; set; } = string.Empty;
/// <summary>Gets or sets whether verbose mode is enabled.</summary>
public bool Verbose { get; set; }
}
/// <summary>
/// Source-generated JSON serializer context for skill test types.
/// Provides serialization support for <see cref="LookupRequest"/>, <see cref="LookupResponse"/>,
/// and <see cref="SkillConfig"/> without requiring runtime reflection.
/// </summary>
[JsonSourceGenerationOptions]
[JsonSerializable(typeof(LookupRequest))]
[JsonSerializable(typeof(LookupResponse))]
[JsonSerializable(typeof(SkillConfig))]
internal sealed partial class SkillTestJsonContext : JsonSerializerContext
{
}