diff --git a/dotnet/samples/02-agents/AgentSkills/SubprocessScriptRunner.cs b/dotnet/samples/02-agents/AgentSkills/SubprocessScriptRunner.cs
index e95bde61df..b2068c4c0b 100644
--- a/dotnet/samples/02-agents/AgentSkills/SubprocessScriptRunner.cs
+++ b/dotnet/samples/02-agents/AgentSkills/SubprocessScriptRunner.cs
@@ -5,16 +5,16 @@
// This is provided for demonstration purposes only.
using System.Diagnostics;
+using System.Text.Json;
using Microsoft.Agents.AI;
-using Microsoft.Extensions.AI;
///
/// Executes file-based skill scripts as local subprocesses.
///
///
-/// This runner uses the script's absolute path, converts the arguments
-/// to CLI flags, and returns captured output. It is intended for
-/// demonstration purposes only.
+/// This runner uses the script's absolute path and converts the arguments
+/// to CLI arguments. When the LLM sends a JSON array, each element is used
+/// as a positional argument. It is intended for demonstration purposes only.
///
internal static class SubprocessScriptRunner
{
@@ -24,7 +24,8 @@ internal static class SubprocessScriptRunner
public static async Task RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
- AIFunctionArguments arguments,
+ JsonElement? arguments,
+ IServiceProvider? serviceProvider,
CancellationToken cancellationToken)
{
if (!File.Exists(script.FullPath))
@@ -61,24 +62,27 @@ internal static class SubprocessScriptRunner
startInfo.FileName = script.FullPath;
}
- if (arguments is not null)
+ if (arguments is { ValueKind: JsonValueKind.Array } json)
{
- foreach (var (key, value) in arguments)
+ // Positional CLI arguments
+ foreach (var element in json.EnumerateArray())
{
- if (value is bool boolValue)
+ if (element.ValueKind != JsonValueKind.String)
{
- if (boolValue)
- {
- startInfo.ArgumentList.Add(NormalizeKey(key));
- }
- }
- else if (value is not null)
- {
- startInfo.ArgumentList.Add(NormalizeKey(key));
- startInfo.ArgumentList.Add(value.ToString()!);
+ throw new InvalidOperationException(
+ $"File-based skill scripts only accept string CLI arguments but received a JSON element of kind '{element.ValueKind}'. " +
+ "All array elements must be JSON strings.");
}
+
+ startInfo.ArgumentList.Add(element.GetString()!);
}
}
+ else if (arguments is not null && arguments.Value.ValueKind != JsonValueKind.Null && arguments.Value.ValueKind != JsonValueKind.Undefined)
+ {
+ throw new InvalidOperationException(
+ $"Expected a JSON array of CLI arguments but received {arguments.Value.ValueKind}. " +
+ "File-based skill scripts expect positional arguments as a JSON array of strings.");
+ }
Process? process = null;
try
@@ -128,10 +132,4 @@ internal static class SubprocessScriptRunner
process?.Dispose();
}
}
-
- ///
- /// Normalizes a parameter key to a consistent --flag format.
- /// Models may return keys with or without leading dashes (e.g., "value" vs "--value").
- ///
- private static string NormalizeKey(string key) => "--" + key.TrimStart('-');
}
diff --git a/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml b/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml
index a8268863bd..6a2c790f22 100644
--- a/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml
+++ b/dotnet/src/Microsoft.Agents.AI/CompatibilitySuppressions.xml
@@ -1,6 +1,20 @@
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)
+ lib/net10.0/Microsoft.Agents.AI.dll
+ lib/net10.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net10.0/Microsoft.Agents.AI.dll
+ lib/net10.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)
@@ -29,6 +43,13 @@
lib/net10.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net10.0/Microsoft.Agents.AI.dll
+ lib/net10.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])
@@ -43,6 +64,20 @@
lib/net10.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)
+ lib/net472/Microsoft.Agents.AI.dll
+ lib/net472/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net472/Microsoft.Agents.AI.dll
+ lib/net472/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)
@@ -71,6 +106,13 @@
lib/net472/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net472/Microsoft.Agents.AI.dll
+ lib/net472/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])
@@ -85,6 +127,20 @@
lib/net472/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)
+ lib/net8.0/Microsoft.Agents.AI.dll
+ lib/net8.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net8.0/Microsoft.Agents.AI.dll
+ lib/net8.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)
@@ -113,6 +169,13 @@
lib/net8.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net8.0/Microsoft.Agents.AI.dll
+ lib/net8.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])
@@ -127,6 +190,20 @@
lib/net8.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)
+ lib/net9.0/Microsoft.Agents.AI.dll
+ lib/net9.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net9.0/Microsoft.Agents.AI.dll
+ lib/net9.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)
@@ -155,6 +232,13 @@
lib/net9.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/net9.0/Microsoft.Agents.AI.dll
+ lib/net9.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])
@@ -169,6 +253,20 @@
lib/net9.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.BeginInvoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken,System.AsyncCallback,System.Object)
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentFileSkillScriptRunner.Invoke(Microsoft.Agents.AI.AgentFileSkill,Microsoft.Agents.AI.AgentFileSkillScript,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentInlineSkill.#ctor(Microsoft.Agents.AI.AgentSkillFrontmatter,System.String)
@@ -197,6 +295,13 @@
lib/netstandard2.0/Microsoft.Agents.AI.dll
true
+
+ CP0002
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,Microsoft.Extensions.AI.AIFunctionArguments,System.Threading.CancellationToken)
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ true
+
CP0002
M:Microsoft.Agents.AI.AgentSkillsProvider.#ctor(Microsoft.Agents.AI.AgentInlineSkill[])
@@ -211,4 +316,39 @@
lib/netstandard2.0/Microsoft.Agents.AI.dll
true
+
+ CP0005
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)
+ lib/net10.0/Microsoft.Agents.AI.dll
+ lib/net10.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0005
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)
+ lib/net472/Microsoft.Agents.AI.dll
+ lib/net472/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0005
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)
+ lib/net8.0/Microsoft.Agents.AI.dll
+ lib/net8.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0005
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)
+ lib/net9.0/Microsoft.Agents.AI.dll
+ lib/net9.0/Microsoft.Agents.AI.dll
+ true
+
+
+ CP0005
+ M:Microsoft.Agents.AI.AgentSkillScript.RunAsync(Microsoft.Agents.AI.AgentSkill,System.Nullable{System.Text.Json.JsonElement},System.IServiceProvider,System.Threading.CancellationToken)
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ lib/netstandard2.0/Microsoft.Agents.AI.dll
+ true
+
\ No newline at end of file
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkill.cs
index 4c5ca8dbc3..6f549301d0 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkill.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkill.cs
@@ -35,7 +35,8 @@ public abstract class AgentSkill
/// Gets the full skill content.
///
///
- /// For file-based skills this is the raw SKILL.md file content.
+ /// For file-based skills this is the raw SKILL.md file content, optionally
+ /// augmented with a synthesized scripts block when scripts are present.
/// For code-defined skills this is a synthesized XML document
/// containing name, description, and body (instructions, resources, scripts).
///
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs
index 1ac44bfac8..bbfbcb8616 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillScript.cs
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -46,8 +46,9 @@ protected AgentSkillScript(string name, string? description = null)
/// Runs the script with the given arguments.
///
/// The skill that owns this script.
- /// Arguments for script execution.
+ /// Raw JSON arguments for script execution, preserving the original format (object or array) sent by the caller.
+ /// Optional service provider for dependency injection.
/// Cancellation token.
/// The script execution result.
- public abstract Task RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default);
+ public abstract Task RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default);
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
index b5598e19d3..af1225c9df 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System.Security;
using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -243,7 +244,7 @@ private IList BuildTools(IList skills, bool hasScripts,
}
AIFunction scriptFunction = AIFunctionFactory.Create(
- (string skillName, string scriptName, IDictionary? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default) =>
+ (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.");
@@ -340,7 +341,7 @@ private string LoadSkill(IList skills, string skillName)
}
}
- private async Task RunSkillScriptAsync(IList skills, string skillName, string scriptName, IDictionary? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
+ private async Task RunSkillScriptAsync(IList skills, string skillName, string scriptName, JsonElement? arguments = null, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(skillName))
{
@@ -366,7 +367,7 @@ private string LoadSkill(IList skills, string skillName)
try
{
- return await script.RunAsync(skill, new AIFunctionArguments(arguments) { Services = serviceProvider }, cancellationToken).ConfigureAwait(false);
+ return await script.RunAsync(skill, arguments, serviceProvider, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs
index 4bb62e99a8..3e10557968 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkill.cs
@@ -15,6 +15,8 @@ public sealed class AgentFileSkill : AgentSkill
{
private readonly IReadOnlyList _resources;
private readonly IReadOnlyList _scripts;
+ private readonly string _originalContent;
+ private string? _content;
///
/// Initializes a new instance of the class.
@@ -32,7 +34,7 @@ internal AgentFileSkill(
IReadOnlyList? scripts = null)
{
this.Frontmatter = Throw.IfNull(frontmatter);
- this.Content = Throw.IfNull(content);
+ this._originalContent = Throw.IfNull(content);
this.Path = Throw.IfNullOrWhitespace(path);
this._resources = resources ?? [];
this._scripts = scripts ?? [];
@@ -42,7 +44,18 @@ internal AgentFileSkill(
public override AgentSkillFrontmatter Frontmatter { get; }
///
- public override string Content { get; }
+ ///
+ /// Returns the raw SKILL.md content. When the skill has scripts, a
+ /// <scripts><script name="..."><parameters_schema>...</parameters_schema></script></scripts>
+ /// block is appended with a per-script entry describing the expected argument format.
+ /// The result is cached after the first access.
+ ///
+ public override string Content
+ {
+ get => this._content ??= this._scripts is { Count: > 0 }
+ ? this._originalContent + AgentInlineSkillContentBuilder.BuildScriptsBlock(this._scripts)
+ : this._originalContent;
+ }
///
/// Gets the directory path where the skill was discovered.
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs
index 116847126f..74c0cd2f01 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScript.cs
@@ -2,9 +2,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
@@ -16,6 +16,11 @@ namespace Microsoft.Agents.AI;
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public sealed class AgentFileSkillScript : AgentSkillScript
{
+ ///
+ /// Cached JSON schema element describing the expected argument format: a string array of CLI arguments.
+ ///
+ private static readonly JsonElement s_defaultSchema = CreateDefaultSchema();
+
private readonly AgentFileSkillScriptRunner? _runner;
///
@@ -37,7 +42,14 @@ internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScript
public string FullPath { get; }
///
- public override async Task RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
+ ///
+ /// Returns a fixed schema describing a string array of CLI arguments:
+ /// {"type":"array","items":{"type":"string"}} .
+ ///
+ public override JsonElement? ParametersSchema => s_defaultSchema;
+
+ ///
+ public override async Task RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default)
{
if (skill is not AgentFileSkill fileSkill)
{
@@ -51,6 +63,12 @@ internal AgentFileSkillScript(string name, string fullPath, AgentFileSkillScript
$"Supply a script runner when constructing {nameof(AgentFileSkillsSource)} to enable script execution.");
}
- return await this._runner(fileSkill, this, arguments, cancellationToken).ConfigureAwait(false);
+ return await this._runner(fileSkill, this, arguments, serviceProvider, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static JsonElement CreateDefaultSchema()
+ {
+ using JsonDocument document = JsonDocument.Parse("""{"type":"array","items":{"type":"string"}}""");
+ return document.RootElement.Clone();
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScriptRunner.cs b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScriptRunner.cs
index c19d19e056..1746150ca2 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScriptRunner.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/File/AgentFileSkillScriptRunner.cs
@@ -1,9 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI;
@@ -13,15 +14,19 @@ namespace Microsoft.Agents.AI;
///
///
/// Implementations determine the execution strategy (e.g., local subprocess, hosted code execution environment).
+/// The parameter preserves the raw JSON sent by the caller, in the shape
+/// described by .
///
/// The skill that owns the script.
/// The file-based script to run.
-/// Optional arguments for the script, provided by the agent/LLM.
+/// Raw JSON arguments for the script, in the shape described by .
+/// Optional service provider for dependency injection.
/// Cancellation token.
/// The script execution result.
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public delegate Task AgentFileSkillScriptRunner(
AgentFileSkill skill,
AgentFileSkillScript script,
- AIFunctionArguments arguments,
+ JsonElement? arguments,
+ IServiceProvider? serviceProvider,
CancellationToken cancellationToken);
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs
index f90d67dd1d..dabf75fa1a 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillContentBuilder.cs
@@ -59,33 +59,57 @@ public static string Build(
if (scripts is { Count: > 0 })
{
- sb.Append("\n\n\n");
- foreach (var script in scripts)
+ sb.Append('\n');
+ sb.Append(BuildScriptsBlock(scripts));
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Builds a <scripts>...</scripts> XML block for the given scripts.
+ /// Each script is emitted as a <script name="..."> element with optional
+ /// description attribute and <parameters_schema> child element.
+ ///
+ /// The scripts to include in the block.
+ /// An XML string starting with \n<scripts> , or an empty string if the list is empty.
+ public static string BuildScriptsBlock(IReadOnlyList scripts)
+ {
+ _ = Throw.IfNull(scripts);
+
+ if (scripts.Count == 0)
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder();
+ sb.Append("\n\n");
+
+ foreach (var script in scripts)
+ {
+ var parametersSchema = script.ParametersSchema;
+
+ if (script.Description is null && parametersSchema is null)
+ {
+ sb.Append($" \n");
- }
+ sb.Append(" \n");
}
-
- sb.Append(" ");
}
+ sb.Append(" ");
+
return sb.ToString();
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs
index 1e3041aafc..c0abc73252 100644
--- a/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Skills/Programmatic/AgentInlineSkillScript.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
@@ -67,8 +68,42 @@ public AgentInlineSkillScript(string name, MethodInfo method, object? target, st
public override JsonElement? ParametersSchema => this._function.JsonSchema;
///
- public override async Task RunAsync(AgentSkill skill, AIFunctionArguments arguments, CancellationToken cancellationToken = default)
+ public override async Task RunAsync(AgentSkill skill, JsonElement? arguments, IServiceProvider? serviceProvider, CancellationToken cancellationToken = default)
{
- return await this._function.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false);
+ var funcArgs = ConvertToFunctionArguments(arguments);
+ funcArgs.Services = serviceProvider;
+
+ return await this._function.InvokeAsync(funcArgs, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Converts a raw to for delegate invocation.
+ ///
+ ///
+ /// Thrown when is provided but is not a JSON object.
+ /// Inline skill scripts expect arguments as a JSON object whose properties map to the delegate's parameters.
+ ///
+ private static AIFunctionArguments ConvertToFunctionArguments(JsonElement? arguments)
+ {
+ if (arguments is null ||
+ arguments.Value.ValueKind == JsonValueKind.Null ||
+ arguments.Value.ValueKind == JsonValueKind.Undefined)
+ {
+ return [];
+ }
+
+ if (arguments.Value.ValueKind != JsonValueKind.Object)
+ {
+ throw new InvalidOperationException(
+ $"Inline skill scripts expect arguments as a JSON object but received a JSON element of kind '{arguments.Value.ValueKind}'.");
+ }
+
+ var dict = new Dictionary();
+ foreach (var property in arguments.Value.EnumerateObject())
+ {
+ dict[property.Name] = property.Value;
+ }
+
+ return new AIFunctionArguments(dict);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs
index 1dc7d5b3f9..dc83fad119 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentClassSkillTests.cs
@@ -8,7 +8,6 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
@@ -128,8 +127,9 @@ public async Task CreateScriptAndResource_WithSerializerOptions_HandleCustomType
// Act — script with custom type deserialization
var script = skill.Scripts![0];
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 5 }, jso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
- var scriptResult = await script.RunAsync(skill, args, CancellationToken.None);
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
+ var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.NotNull(scriptResult);
@@ -173,12 +173,14 @@ public async Task Scripts_DiscoveredViaAttribute_StaticAndInstance_CanBeInvokedA
// Act & Assert — static method
var doWorkScript = skill.Scripts!.First(s => s.Name == "do-work");
- var doWorkResult = await doWorkScript.RunAsync(skill, new AIFunctionArguments { ["input"] = "hello" }, CancellationToken.None);
+ using var doWorkDoc = JsonDocument.Parse("""{"input":"hello"}""");
+ var doWorkResult = await doWorkScript.RunAsync(skill, doWorkDoc.RootElement, null, CancellationToken.None);
Assert.Equal("HELLO", doWorkResult?.ToString());
// Act & Assert — instance method
var appendScript = skill.Scripts!.First(s => s.Name == "append");
- var appendResult = await appendScript.RunAsync(skill, new AIFunctionArguments { ["input"] = "test" }, CancellationToken.None);
+ using var appendDoc = JsonDocument.Parse("""{"input":"test"}""");
+ var appendResult = await appendScript.RunAsync(skill, appendDoc.RootElement, null, CancellationToken.None);
Assert.Equal("test-suffix", appendResult?.ToString());
}
@@ -367,7 +369,7 @@ public async Task MixedStaticAndInstance_AllDiscoveredAndInvocableAsync()
// Act & Assert — all scripts produce values
foreach (var script in skill.Scripts!)
{
- var result = await script.RunAsync(skill, new AIFunctionArguments(), CancellationToken.None);
+ var result = await script.RunAsync(skill, null, null, CancellationToken.None);
Assert.NotNull(result);
}
}
@@ -382,8 +384,9 @@ public async Task SerializerOptions_UsedForReflectedMembersAsync()
// Act & Assert — script with custom JSO
var script = skill.Scripts![0];
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
- var scriptResult = await script.RunAsync(skill, args, CancellationToken.None);
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
+ var scriptResult = await script.RunAsync(skill, args, null, CancellationToken.None);
Assert.NotNull(scriptResult);
Assert.Contains("test", scriptResult!.ToString()!);
Assert.Contains("3", scriptResult!.ToString()!);
@@ -497,8 +500,9 @@ public async Task CreateScript_FallsBackToSerializerOptions_WhenNoExplicitJsoAsy
var script = skill.Scripts!.First(s => s.Name == "Lookup");
var jso = SkillTestJsonContext.Default.Options;
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "fallback", MaxResults = 7 }, jso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.NotNull(result);
@@ -531,8 +535,9 @@ public async Task CreateScript_UsesExplicitJso_OverSerializerOptionsAsync()
var script = skill.Scripts!.First(s => s.Name == "Lookup");
var jso = SkillTestJsonContext.Default.Options;
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "explicit", MaxResults = 2 }, jso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.NotNull(result);
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs
index eb4f706f30..e638380019 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillScriptTests.cs
@@ -1,9 +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;
@@ -16,13 +16,13 @@ public sealed class AgentFileSkillScriptTests
public async Task RunAsync_SkillIsNotAgentFileSkill_ThrowsInvalidOperationExceptionAsync()
{
// Arrange
- static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, AIFunctionArguments a, CancellationToken ct) => Task.FromResult("result");
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult("result");
var script = CreateScript("test-script", "/path/to/script.py", RunnerAsync);
var nonFileSkill = new TestAgentSkill("my-skill", "A skill", "Instructions.");
// Act & Assert
await Assert.ThrowsAsync(
- () => script.RunAsync(nonFileSkill, new AIFunctionArguments(), CancellationToken.None));
+ () => script.RunAsync(nonFileSkill, null, null, CancellationToken.None));
}
[Fact]
@@ -30,7 +30,7 @@ public async Task RunAsync_WithAgentFileSkill_DelegatesToRunnerAsync()
{
// Arrange
var runnerCalled = false;
- Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, AIFunctionArguments args, CancellationToken ct)
+ Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
{
runnerCalled = true;
return Task.FromResult("executed");
@@ -42,7 +42,7 @@ public async Task RunAsync_WithAgentFileSkill_DelegatesToRunnerAsync()
"/skills/my-skill");
// Act
- var result = await script.RunAsync(fileSkill, new AIFunctionArguments(), CancellationToken.None);
+ var result = await script.RunAsync(fileSkill, null, null, CancellationToken.None);
// Assert
Assert.True(runnerCalled);
@@ -55,7 +55,7 @@ public async Task RunAsync_RunnerReceivesCorrectArgumentsAsync()
// Arrange
AgentFileSkill? capturedSkill = null;
AgentFileSkillScript? capturedScript = null;
- Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, AIFunctionArguments args, CancellationToken ct)
+ Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
{
capturedSkill = skill;
capturedScript = scriptArg;
@@ -68,7 +68,7 @@ public async Task RunAsync_RunnerReceivesCorrectArgumentsAsync()
"/skills/owner-skill");
// Act
- await script.RunAsync(fileSkill, new AIFunctionArguments(), CancellationToken.None);
+ await script.RunAsync(fileSkill, null, null, CancellationToken.None);
// Assert
Assert.Same(fileSkill, capturedSkill);
@@ -79,7 +79,7 @@ public async Task RunAsync_RunnerReceivesCorrectArgumentsAsync()
public void Script_HasCorrectNameAndPath()
{
// Arrange & Act
- static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, AIFunctionArguments a, CancellationToken ct) => Task.FromResult(null);
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult(null);
var script = CreateScript("my-script", "/path/to/my-script.py", RunnerAsync);
// Assert
@@ -87,10 +87,173 @@ public void Script_HasCorrectNameAndPath()
Assert.Equal("/path/to/my-script.py", script.FullPath);
}
+ [Fact]
+ public void ParametersSchema_ReturnsExpectedArraySchema()
+ {
+ // Arrange
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult(null);
+ var script = CreateScript("my-script", "/path/to/script.py", RunnerAsync);
+
+ // Act
+ var schema = script.ParametersSchema;
+
+ // Assert
+ Assert.NotNull(schema);
+ var raw = schema!.Value.GetRawText();
+ Assert.Contains("\"type\":\"array\"", raw);
+ Assert.Contains("\"items\":{\"type\":\"string\"}", raw);
+ }
+
+ [Fact]
+ public void Content_WithScripts_AppendsPerScriptEntries()
+ {
+ // Arrange
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult(null);
+ var script1 = CreateScript("build", "/scripts/build.sh", RunnerAsync);
+ var script2 = CreateScript("deploy", "/scripts/deploy.sh", RunnerAsync);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Original content",
+ "/skills/my-skill",
+ scripts: [script1, script2]);
+
+ // Act
+ var content = fileSkill.Content;
+
+ // Assert — content starts with original and appends per-script entries
+ Assert.StartsWith("Original content", content);
+ Assert.Contains("", content);
+ Assert.Contains(" ", content);
+ }
+
+ [Fact]
+ public void Content_WithoutScripts_ReturnsOriginalContent()
+ {
+ // Arrange
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Original content only",
+ "/skills/my-skill");
+
+ // Act
+ var content = fileSkill.Content;
+
+ // Assert
+ Assert.Equal("Original content only", content);
+ }
+
+ [Fact]
+ public void Content_WithScripts_IsCached()
+ {
+ // Arrange
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult(null);
+ var script = CreateScript("test", "/scripts/test.sh", RunnerAsync);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Content",
+ "/skills/my-skill",
+ scripts: [script]);
+
+ // Act
+ var content1 = fileSkill.Content;
+ var content2 = fileSkill.Content;
+
+ // Assert
+ Assert.Same(content1, content2);
+ }
+
+ [Fact]
+ public async Task RunAsync_ForwardsJsonArrayArgumentsToRunnerAsync()
+ {
+ // Arrange
+ JsonElement? capturedArgs = null;
+ Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
+ {
+ capturedArgs = args;
+ return Task.FromResult("done");
+ }
+ var script = CreateScript("array-test", "/scripts/test.sh", runnerAsync);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Content",
+ "/skills/my-skill");
+ using var arrayArgsDoc = JsonDocument.Parse("""["arg1","arg2","arg3"]""");
+ var arrayArgs = arrayArgsDoc.RootElement;
+
+ // Act
+ await script.RunAsync(fileSkill, arrayArgs, null, CancellationToken.None);
+
+ // Assert — the raw JSON array is forwarded unchanged
+ Assert.NotNull(capturedArgs);
+ Assert.Equal(JsonValueKind.Array, capturedArgs!.Value.ValueKind);
+ Assert.Equal("""["arg1","arg2","arg3"]""", capturedArgs.Value.GetRawText());
+ }
+
+ [Fact]
+ public async Task RunAsync_ForwardsServiceProviderToRunnerAsync()
+ {
+ // Arrange
+ IServiceProvider? capturedProvider = null;
+ Task runnerAsync(AgentFileSkill skill, AgentFileSkillScript scriptArg, JsonElement? args, IServiceProvider? sp, CancellationToken ct)
+ {
+ capturedProvider = sp;
+ return Task.FromResult("done");
+ }
+ var script = CreateScript("sp-test", "/scripts/test.sh", runnerAsync);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Content",
+ "/skills/my-skill");
+ var mockProvider = new TestServiceProvider();
+
+ // Act
+ await script.RunAsync(fileSkill, null, mockProvider, CancellationToken.None);
+
+ // Assert
+ Assert.Same(mockProvider, capturedProvider);
+ }
+
+ [Fact]
+ public async Task RunAsync_NoRunner_ThrowsInvalidOperationExceptionAsync()
+ {
+ // Arrange — create script without a runner
+ var script = CreateScript("no-runner", "/scripts/test.sh", runner: null);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Content",
+ "/skills/my-skill");
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ () => script.RunAsync(fileSkill, null, null, CancellationToken.None));
+ }
+
+ [Fact]
+ public void Content_WithScripts_ContainsDefaultParametersSchema()
+ {
+ // Arrange
+ static Task RunnerAsync(AgentFileSkill s, AgentFileSkillScript sc, JsonElement? a, IServiceProvider? sp, CancellationToken ct) => Task.FromResult(null);
+ var script = CreateScript("test", "/scripts/test.sh", RunnerAsync);
+ var fileSkill = new AgentFileSkill(
+ new AgentSkillFrontmatter("my-skill", "A skill"),
+ "Original content",
+ "/skills/my-skill",
+ scripts: [script]);
+
+ // Act
+ var content = fileSkill.Content;
+
+ // Assert — the appended block contains the actual default schema from AgentFileSkillScript
+ Assert.Contains("""{"type":"array","items":{"type":"string"}}""", content);
+ }
+
///
/// Helper to create an via reflection since the constructor is internal.
///
- private static AgentFileSkillScript CreateScript(string name, string fullPath, AgentFileSkillScriptRunner executor)
+ private static AgentFileSkillScript CreateScript(string name, string fullPath, AgentFileSkillScriptRunner? runner)
{
var ctor = typeof(AgentFileSkillScript).GetConstructor(
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
@@ -98,6 +261,14 @@ private static AgentFileSkillScript CreateScript(string name, string fullPath, A
[typeof(string), typeof(string), typeof(AgentFileSkillScriptRunner)],
null) ?? throw new InvalidOperationException("Could not find internal constructor.");
- return (AgentFileSkillScript)ctor.Invoke([name, fullPath, executor]);
+ return (AgentFileSkillScript)ctor.Invoke([name, fullPath, runner]);
+ }
+
+ ///
+ /// Minimal for testing service forwarding.
+ ///
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs
index f1ca662202..7b4dfa1812 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentFileSkillsSourceScriptTests.cs
@@ -3,9 +3,9 @@
using System;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
@@ -15,7 +15,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
public sealed class AgentFileSkillsSourceScriptTests : IDisposable
{
private static readonly string[] s_rubyExtension = new[] { ".rb" };
- private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult(null);
+ private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult(null);
private readonly string _testRoot;
@@ -139,7 +139,7 @@ public async Task GetSkillsAsync_WithRunner_ScriptsCanRunAsync()
var executorCalled = false;
var source = new AgentFileSkillsSource(
this._testRoot,
- (skill, script, args, ct) =>
+ (skill, script, args, sp, ct) =>
{
executorCalled = true;
Assert.Equal("exec-skill", skill.Frontmatter.Name);
@@ -150,7 +150,7 @@ public async Task GetSkillsAsync_WithRunner_ScriptsCanRunAsync()
// Act
var skills = await source.GetSkillsAsync(CancellationToken.None);
- var scriptResult = await skills[0].Scripts![0].RunAsync(skills[0], new AIFunctionArguments(), CancellationToken.None);
+ var scriptResult = await skills[0].Scripts![0].RunAsync(skills[0], null, null, CancellationToken.None);
// Assert
Assert.True(executorCalled);
@@ -178,7 +178,7 @@ public async Task GetSkillsAsync_ScriptsWithNoRunner_ThrowsOnRunAsync()
var script = skills[0].Scripts![0];
// Assert — running the script throws because no runner was provided
- await Assert.ThrowsAsync(() => script.RunAsync(skills[0], new AIFunctionArguments(), CancellationToken.None));
+ await Assert.ThrowsAsync(() => script.RunAsync(skills[0], null, null, CancellationToken.None));
}
[Fact]
@@ -204,10 +204,10 @@ public async Task GetSkillsAsync_ExecutorReceivesArgumentsAsync()
{
// Arrange
CreateSkillWithScript(this._testRoot, "args-skill", "Args test", "Body.", "scripts/test.py", "print('ok')");
- AIFunctionArguments? capturedArgs = null;
+ JsonElement? capturedArgs = null;
var source = new AgentFileSkillsSource(
this._testRoot,
- (skill, script, args, ct) =>
+ (skill, script, args, sp, ct) =>
{
capturedArgs = args;
return Task.FromResult("done");
@@ -215,17 +215,15 @@ public async Task GetSkillsAsync_ExecutorReceivesArgumentsAsync()
// Act
var skills = await source.GetSkillsAsync(CancellationToken.None);
- var arguments = new AIFunctionArguments
- {
- ["value"] = 26.2,
- ["factor"] = 1.60934
- };
- await skills[0].Scripts![0].RunAsync(skills[0], arguments, CancellationToken.None);
+ using var argumentsDoc = JsonDocument.Parse("""{"value":26.2,"factor":1.60934}""");
+ var arguments = argumentsDoc.RootElement;
+ await skills[0].Scripts![0].RunAsync(skills[0], arguments, null, CancellationToken.None);
// Assert
Assert.NotNull(capturedArgs);
- Assert.Equal(26.2, capturedArgs["value"]);
- Assert.Equal(1.60934, capturedArgs["factor"]);
+ Assert.Equal(JsonValueKind.Object, capturedArgs!.Value.ValueKind);
+ Assert.Equal(26.2, capturedArgs.Value.GetProperty("value").GetDouble());
+ Assert.Equal(1.60934, capturedArgs.Value.GetProperty("factor").GetDouble());
}
[Fact]
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillScriptTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillScriptTests.cs
index efab3b2c7d..85521ba4a9 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillScriptTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillScriptTests.cs
@@ -5,7 +5,6 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
@@ -22,7 +21,7 @@ public async Task RunAsync_InvokesDelegate_ReturnsResultAsync()
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
// Act
- var result = await script.RunAsync(skill, new AIFunctionArguments(), CancellationToken.None);
+ var result = await script.RunAsync(skill, null, null, CancellationToken.None);
// Assert
Assert.Equal("hello", result?.ToString());
@@ -34,10 +33,11 @@ public async Task RunAsync_WithParameters_PassesArgumentsAsync()
// Arrange
var script = new AgentInlineSkillScript("add", (int a, int b) => a + b);
var skill = new AgentInlineSkill("calc-skill", "Calc.", "Instructions.");
- var args = new AIFunctionArguments { ["a"] = 3, ["b"] = 7 };
+ using var argsDoc = JsonDocument.Parse("""{"a":3,"b":7}""");
+ var args = argsDoc.RootElement;
// Act
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.Equal(10, int.Parse(result?.ToString()!));
@@ -129,10 +129,11 @@ public async Task RunAsync_WithSerializerOptions_MarshalsCustomTypesAsync()
}, 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 };
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
// Act
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert — the custom input type was deserialized and the response was produced
Assert.NotNull(result);
@@ -145,10 +146,11 @@ public async Task RunAsync_StringParameter_WorksAsync()
// Arrange
var script = new AgentInlineSkillScript("echo", (string message) => message);
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
- var args = new AIFunctionArguments { ["message"] = "hello world" };
+ using var argsDoc = JsonDocument.Parse("""{"message":"hello world"}""");
+ var args = argsDoc.RootElement;
// Act
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.Equal("hello world", result?.ToString());
@@ -175,10 +177,11 @@ public async Task RunAsync_MethodInfo_StaticMethod_InvokesAndReturnsAsync()
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(StaticScriptHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
var script = new AgentInlineSkillScript("static-method-script", method, target: null);
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
- var args = new AIFunctionArguments { ["input"] = "hello" };
+ using var argsDoc = JsonDocument.Parse("""{"input":"hello"}""");
+ var args = argsDoc.RootElement;
// Act
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ var result = await script.RunAsync(skill, args, null, CancellationToken.None);
// Assert
Assert.Equal("HELLO", result?.ToString());
@@ -191,10 +194,11 @@ public async Task RunAsync_MethodInfo_InstanceMethod_InvokesAndReturnsAsync()
var method = typeof(AgentInlineSkillScriptTests).GetMethod(nameof(InstanceScriptHelper), BindingFlags.NonPublic | BindingFlags.Instance)!;
var script = new AgentInlineSkillScript("instance-method-script", method, target: this);
var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
- var args = new AIFunctionArguments { ["input"] = "test" };
+ using var argsDoc2 = JsonDocument.Parse("""{"input":"test"}""");
+ var args2 = argsDoc2.RootElement;
// Act
- var result = await script.RunAsync(skill, args, CancellationToken.None);
+ var result = await script.RunAsync(skill, args2, null, CancellationToken.None);
// Assert
Assert.Equal("test-suffix", result?.ToString());
@@ -223,7 +227,63 @@ public void ParametersSchema_MethodInfo_ContainsParameterNames()
Assert.Contains("input", schema!.Value.GetRawText());
}
+ [Fact]
+ public async Task RunAsync_WithNonObjectArguments_ThrowsInvalidOperationExceptionAsync()
+ {
+ // Arrange — inline scripts require a JSON object for arguments
+ var script = new AgentInlineSkillScript("noop", () => "ok");
+ var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
+ using var arrayArgsDoc = JsonDocument.Parse("""["a","b"]""");
+ var arrayArgs = arrayArgsDoc.RootElement;
+
+ // Act & Assert — non-object JSON should fail fast rather than silently dropping arguments
+ await Assert.ThrowsAsync(
+ () => script.RunAsync(skill, arrayArgs, null, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task RunAsync_WithNullArguments_TreatsAsNoArgumentsAsync()
+ {
+ // Arrange — a parameterless delegate should succeed when given null arguments
+ var script = new AgentInlineSkillScript("noop", () => "ok");
+ var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
+
+ // Act
+ var result = await script.RunAsync(skill, null, null, CancellationToken.None);
+
+ // Assert
+ Assert.Equal("ok", result?.ToString());
+ }
+
+ [Fact]
+ public async Task RunAsync_ServiceProviderIsForwardedAsync()
+ {
+ // Arrange — delegate that resolves a service from the IServiceProvider
+ IServiceProvider? capturedProvider = null;
+ var script = new AgentInlineSkillScript("svc-test", (IServiceProvider sp) =>
+ {
+ capturedProvider = sp;
+ return "done";
+ });
+ var skill = new AgentInlineSkill("test-skill", "Test.", "Instructions.");
+ var mockProvider = new TestServiceProvider();
+
+ // Act
+ await script.RunAsync(skill, null, mockProvider, CancellationToken.None);
+
+ // Assert
+ Assert.Same(mockProvider, capturedProvider);
+ }
+
private static string StaticScriptHelper(string input) => input.ToUpperInvariant();
private string InstanceScriptHelper(string input) => input + "-suffix";
+
+ ///
+ /// Minimal for testing service forwarding.
+ ///
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillTests.cs
index 5aad5ef2f3..817223bac7 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentInlineSkillTests.cs
@@ -433,10 +433,11 @@ public async Task AddScript_SkillLevelSerializerOptions_AppliedToScriptAsync()
TotalCount = request.MaxResults,
});
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "test", MaxResults = 3 }, jso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
// Act
- var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
+ var result = await skill.Scripts![0].RunAsync(skill, args, null, CancellationToken.None);
// Assert — the custom input was deserialized via skill-level JSO and response was produced
Assert.NotNull(result);
@@ -456,10 +457,11 @@ public async Task AddScript_PerScriptSerializerOptions_OverridesSkillLevelAsync(
TotalCount = request.MaxResults,
}, serializerOptions: scriptJso);
var inputJson = JsonSerializer.SerializeToElement(new LookupRequest { Query = "override", MaxResults = 7 }, scriptJso);
- var args = new AIFunctionArguments { ["request"] = inputJson };
+ using var argsDoc = JsonDocument.Parse($$"""{ "request": {{inputJson.GetRawText()}} }""");
+ var args = argsDoc.RootElement;
// Act
- var result = await skill.Scripts![0].RunAsync(skill, args, CancellationToken.None);
+ var result = await skill.Scripts![0].RunAsync(skill, args, null, CancellationToken.None);
// Assert — per-script JSO takes effect and custom types are properly marshaled
Assert.NotNull(result);
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
index 23c2745247..123be17387 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -15,7 +16,7 @@ namespace Microsoft.Agents.AI.UnitTests.AgentSkills;
///
public sealed class AgentSkillsProviderTests : IDisposable
{
- private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult(null);
+ private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult(null);
private readonly string _testRoot;
private readonly TestAIAgent _agent = new();
@@ -462,7 +463,7 @@ public async Task Builder_UseFileScriptRunnerAfterUseFileSkills_RunnerIsUsedAsyn
// Act — call UseFileScriptRunner AFTER UseFileSkill (the bug scenario)
var provider = new AgentSkillsProviderBuilder()
.UseFileSkill(this._testRoot)
- .UseFileScriptRunner((skill, script, args, ct) =>
+ .UseFileScriptRunner((skill, script, args, sp, ct) =>
{
executorCalled = true;
return Task.FromResult("executed");
@@ -487,6 +488,62 @@ public async Task Builder_UseFileScriptRunnerAfterUseFileSkills_RunnerIsUsedAsyn
Assert.True(executorCalled);
}
+ [Fact]
+ public async Task RunSkillScript_ForwardsJsonArgumentsAndServiceProviderToRunnerAsync()
+ {
+ // Arrange — create a skill with a script file
+ string skillDir = Path.Combine(this._testRoot, "fwd-skill");
+ Directory.CreateDirectory(Path.Combine(skillDir, "scripts"));
+ File.WriteAllText(
+ Path.Combine(skillDir, "SKILL.md"),
+ "---\nname: fwd-skill\ndescription: Forwarding test\n---\nBody.");
+ File.WriteAllText(
+ Path.Combine(skillDir, "scripts", "run.py"),
+ "print('ok')");
+
+ JsonElement? capturedArgs = null;
+ IServiceProvider? capturedServiceProvider = null;
+
+ var provider = new AgentSkillsProviderBuilder()
+ .UseFileSkill(this._testRoot)
+ .UseFileScriptRunner((skill, script, args, sp, ct) =>
+ {
+ capturedArgs = args;
+ capturedServiceProvider = sp;
+ return Task.FromResult("executed");
+ })
+ .Build();
+
+ var mockServiceProvider = new TestServiceProvider();
+ var invokingContext = new AIContextProvider.InvokingContext(this._agent, session: null, new AIContext());
+ var result = await provider.InvokingAsync(invokingContext, CancellationToken.None);
+ var runScriptTool = result.Tools!.First(t => t.Name == "run_skill_script") as AIFunction;
+
+ // Act — invoke with JsonElement arguments and a service provider
+ using var argsJsonDoc = JsonDocument.Parse("""["arg1","arg2"]""");
+ var argsJson = argsJsonDoc.RootElement;
+ await runScriptTool!.InvokeAsync(new AIFunctionArguments(new Dictionary
+ {
+ ["skillName"] = "fwd-skill",
+ ["scriptName"] = "scripts/run.py",
+ ["arguments"] = argsJson,
+ })
+ {
+ Services = mockServiceProvider,
+ });
+
+ // Assert — JsonElement arguments and service provider are forwarded to the runner
+ Assert.NotNull(capturedArgs);
+ Assert.Equal(JsonValueKind.Array, capturedArgs!.Value.ValueKind);
+ Assert.Equal("""["arg1","arg2"]""", capturedArgs.Value.GetRawText());
+ Assert.Same(mockServiceProvider, capturedServiceProvider);
+ }
+
+ private sealed class TestServiceProvider : IServiceProvider
+ {
+ public object? GetService(Type serviceType) => null;
+ }
+
private static void CreateSkillIn(string root, string name, string description, string body)
{
string skillDir = Path.Combine(root, name);
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
index c43568acd9..d451f63a05 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/FileAgentSkillLoaderTests.cs
@@ -15,7 +15,7 @@ public sealed class FileAgentSkillLoaderTests : IDisposable
private static readonly string[] s_customExtensions = [".custom"];
private static readonly string[] s_validExtensions = [".md", ".json", ".custom"];
private static readonly string[] s_mixedValidInvalidExtensions = [".md", "json"];
- private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, ct) => Task.FromResult(null);
+ private static readonly AgentFileSkillScriptRunner s_noOpExecutor = (skill, script, args, sp, ct) => Task.FromResult(null);
private readonly string _testRoot;