Skip to content

Commit 9fb16e0

Browse files
SergeyMenshykhsemenshiCopilot
authored
.NET: Allow custom argument marshaling for skill scripts (#6498)
Add an optional Func<JsonElement?, AIFunctionArguments> argument marshaler to inline and class-based skills so callers can customize how raw JSON tool-call arguments are converted into AIFunctionArguments before delegate invocation. This enables handling backends (e.g. vLLM) that send tool-call arguments as a JSON string instead of a JSON object. The marshaler can be supplied at the script, inline-skill, or class-skill level; when omitted, the existing strict JSON-object behavior is preserved unchanged. Co-authored-by: SergeyMenshykh <SergeMenshikh@outlook.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e07cfba commit 9fb16e0

4 files changed

Lines changed: 385 additions & 15 deletions

File tree

dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentClassSkill.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,18 @@ public abstract class AgentClassSkill<
102102
private readonly Lazy<IReadOnlyList<AgentSkillResource>?> _resources;
103103
private readonly Lazy<IReadOnlyList<AgentSkillScript>?> _scripts;
104104
private readonly Lazy<string> _content;
105+
private readonly Func<JsonElement?, AIFunctionArguments>? _argumentMarshaler;
105106

106107
/// <summary>
107108
/// Initializes a new instance of the <see cref="AgentClassSkill{TSelf}"/> class.
108109
/// </summary>
109-
protected AgentClassSkill()
110+
/// <param name="argumentMarshaler">
111+
/// Optional argument marshaler applied to all scripts in this skill.
112+
/// When <see langword="null"/>, the default marshaler is used which expects arguments as a JSON object.
113+
/// </param>
114+
protected AgentClassSkill(Func<JsonElement?, AIFunctionArguments>? argumentMarshaler = null)
110115
{
116+
this._argumentMarshaler = argumentMarshaler;
111117
this._resources = new Lazy<IReadOnlyList<AgentSkillResource>?>(this.DiscoverResources);
112118
this._scripts = new Lazy<IReadOnlyList<AgentSkillScript>?>(this.DiscoverScripts);
113119
this._content = new Lazy<string>(() => AgentInlineSkillContentBuilder.Build(
@@ -240,7 +246,7 @@ protected AgentSkillResource CreateResource(string name, Delegate method, string
240246
/// </param>
241247
/// <returns>A new <see cref="AgentSkillScript"/> instance.</returns>
242248
protected AgentSkillScript CreateScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
243-
=> new AgentInlineSkillScript(name, method, description, serializerOptions ?? this.SerializerOptions);
249+
=> new AgentInlineSkillScript(name, method, description, serializerOptions ?? this.SerializerOptions, this._argumentMarshaler);
244250

245251
private List<AgentSkillResource>? DiscoverResources()
246252
{
@@ -356,7 +362,8 @@ private static void ValidateResourceMethodParameters(MethodInfo method, Type ski
356362
method: method,
357363
target: method.IsStatic ? null : this,
358364
description: method.GetCustomAttribute<DescriptionAttribute>()?.Description,
359-
serializerOptions: this.SerializerOptions));
365+
serializerOptions: this.SerializerOptions,
366+
argumentMarshaler: this._argumentMarshaler));
360367
}
361368

362369
return scripts;

dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkill.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public sealed class AgentInlineSkill : AgentSkill
3030
{
3131
private readonly string _instructions;
3232
private readonly JsonSerializerOptions? _serializerOptions;
33+
private readonly Func<JsonElement?, AIFunctionArguments>? _argumentMarshaler;
3334
private List<AgentInlineSkillResource>? _resources;
3435
private List<AgentInlineSkillScript>? _scripts;
3536
private string? _cachedContent;
@@ -45,11 +46,16 @@ public sealed class AgentInlineSkill : AgentSkill
4546
/// added to this skill. Individual <see cref="AddScript"/> and <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>
4647
/// calls can override this default. When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
4748
/// </param>
48-
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions, JsonSerializerOptions? serializerOptions = null)
49+
/// <param name="argumentMarshaler">
50+
/// Optional argument marshaler applied by default to all scripts added to this skill.
51+
/// When <see langword="null"/>, the default marshaler is used which expects arguments as a JSON object.
52+
/// </param>
53+
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions, JsonSerializerOptions? serializerOptions = null, Func<JsonElement?, AIFunctionArguments>? argumentMarshaler = null)
4954
{
5055
this.Frontmatter = Throw.IfNull(frontmatter);
5156
this._instructions = Throw.IfNullOrWhitespace(instructions);
5257
this._serializerOptions = serializerOptions;
58+
this._argumentMarshaler = argumentMarshaler;
5359
}
5460

5561
/// <summary>
@@ -68,6 +74,10 @@ public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions,
6874
/// added to this skill. Individual <see cref="AddScript"/> and <see cref="AddResource(string, Delegate, string?, JsonSerializerOptions?)"/>
6975
/// calls can override this default. When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
7076
/// </param>
77+
/// <param name="argumentMarshaler">
78+
/// Optional argument marshaler applied by default to all scripts added to this skill.
79+
/// When <see langword="null"/>, the default marshaler is used which expects arguments as a JSON object.
80+
/// </param>
7181
public AgentInlineSkill(
7282
string name,
7383
string description,
@@ -76,7 +86,8 @@ public AgentInlineSkill(
7686
string? compatibility = null,
7787
string? allowedTools = null,
7888
AdditionalPropertiesDictionary? metadata = null,
79-
JsonSerializerOptions? serializerOptions = null)
89+
JsonSerializerOptions? serializerOptions = null,
90+
Func<JsonElement?, AIFunctionArguments>? argumentMarshaler = null)
8091
: this(
8192
new AgentSkillFrontmatter(name, description, compatibility)
8293
{
@@ -85,7 +96,8 @@ public AgentInlineSkill(
8596
Metadata = metadata,
8697
},
8798
instructions,
88-
serializerOptions)
99+
serializerOptions,
100+
argumentMarshaler)
89101
{
90102
}
91103

@@ -169,7 +181,7 @@ public AgentInlineSkill AddResource(string name, Delegate method, string? descri
169181
/// <returns>This instance, for chaining.</returns>
170182
public AgentInlineSkill AddScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
171183
{
172-
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description, serializerOptions ?? this._serializerOptions));
184+
(this._scripts ??= []).Add(new AgentInlineSkillScript(name, method, description, serializerOptions ?? this._serializerOptions, this._argumentMarshaler));
173185
return this;
174186
}
175187
}

dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace Microsoft.Agents.AI;
2020
internal sealed class AgentInlineSkillScript : AgentSkillScript
2121
{
2222
private readonly AIFunction _function;
23+
private readonly Func<JsonElement?, AIFunctionArguments> _argumentMarshaler;
2324

2425
/// <summary>
2526
/// Initializes a new instance of the <see cref="AgentInlineSkillScript"/> class from a delegate.
@@ -32,13 +33,18 @@ internal sealed class AgentInlineSkillScript : AgentSkillScript
3233
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the delegate's parameters and return value.
3334
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
3435
/// </param>
35-
public AgentInlineSkillScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null)
36+
/// <param name="argumentMarshaler">
37+
/// Optional function for converting raw JSON arguments into <see cref="AIFunctionArguments"/>.
38+
/// When <see langword="null"/>, the default marshaler is used which expects arguments as a JSON object.
39+
/// </param>
40+
public AgentInlineSkillScript(string name, Delegate method, string? description = null, JsonSerializerOptions? serializerOptions = null, Func<JsonElement?, AIFunctionArguments>? argumentMarshaler = null)
3641
: base(Throw.IfNullOrWhitespace(name), description)
3742
{
3843
Throw.IfNull(method);
3944

4045
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
4146
this._function = AIFunctionFactory.Create(method, options);
47+
this._argumentMarshaler = argumentMarshaler ?? ConvertToFunctionArguments;
4248
}
4349

4450
/// <summary>
@@ -53,13 +59,18 @@ public AgentInlineSkillScript(string name, Delegate method, string? description
5359
/// Optional <see cref="JsonSerializerOptions"/> used to marshal the method's parameters and return value.
5460
/// When <see langword="null"/>, <see cref="AIJsonUtilities.DefaultOptions"/> is used.
5561
/// </param>
56-
public AgentInlineSkillScript(string name, MethodInfo method, object? target, string? description = null, JsonSerializerOptions? serializerOptions = null)
62+
/// <param name="argumentMarshaler">
63+
/// Optional function for converting raw JSON arguments into <see cref="AIFunctionArguments"/>.
64+
/// When <see langword="null"/>, the default marshaler is used which expects arguments as a JSON object.
65+
/// </param>
66+
public AgentInlineSkillScript(string name, MethodInfo method, object? target, string? description = null, JsonSerializerOptions? serializerOptions = null, Func<JsonElement?, AIFunctionArguments>? argumentMarshaler = null)
5767
: base(Throw.IfNullOrWhitespace(name), description)
5868
{
5969
Throw.IfNull(method);
6070

6171
var options = new AIFunctionFactoryOptions { Name = this.Name, SerializerOptions = serializerOptions };
6272
this._function = AIFunctionFactory.Create(method, target, options);
73+
this._argumentMarshaler = argumentMarshaler ?? ConvertToFunctionArguments;
6374
}
6475

6576
/// <summary>
@@ -70,19 +81,15 @@ public AgentInlineSkillScript(string name, MethodInfo method, object? target, st
7081
/// <inheritdoc/>
7182
public override async Task<object?> RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default)
7283
{
73-
var funcArgs = ConvertToFunctionArguments(arguments);
84+
var funcArgs = this._argumentMarshaler(arguments);
7485
funcArgs.Services = serviceProvider;
7586

7687
return await this._function.InvokeAsync(funcArgs, cancellationToken).ConfigureAwait(false);
7788
}
7889

7990
/// <summary>
80-
/// Converts a raw <see cref="JsonElement"/> to <see cref="AIFunctionArguments"/> for delegate invocation.
91+
/// Default argument marshaling: expects arguments as a JSON object whose properties map to the delegate's parameters.
8192
/// </summary>
82-
/// <exception cref="InvalidOperationException">
83-
/// Thrown when <paramref name="arguments"/> is provided but is not a JSON object.
84-
/// Inline skill scripts expect arguments as a JSON object whose properties map to the delegate's parameters.
85-
/// </exception>
8693
private static AIFunctionArguments ConvertToFunctionArguments(JsonElement? arguments)
8794
{
8895
if (arguments is null ||

0 commit comments

Comments
 (0)