microsoft / microsoft/agent-framework

.NET: run_skill_script generates invalid schema for arguments

Open
#8,094 2 comments 0 reactions 1 assignee View on GitHub

@rogerbarreto is already working on this.

Since Sep 7, 2026.

.NET agents reproduced
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

Description

When an agent uses strict structured output, AgentSkillsProvider causes OpenAI/Azure OpenAI to reject the request before model execution.

AgentSkillsProvider always exposes run_skill_script for a file-based skill, even when the skill contains no scripts. In Microsoft.Agents.AI 1.20.0, the generated JSON Schema for its optional arguments parameter is:

{"default":null}

Because the property has no type, OpenAI/Azure OpenAI rejects the complete request when strict schema validation is enabled for the response:

HTTP 400 (invalid_request_error: invalid_function_parameters)
Parameter: tools[...].function.parameters

Invalid schema for function 'run_skill_script':
In context=('properties', 'arguments'), schema must have a 'type' key.

Without strict structured output, the same request is accepted by Azure OpenAI. Enabling strict output with:

chatOptions.AdditionalProperties["strict"] = true;

exposes the invalid tool schema. Although strict mode is configured for the response schema, the service validates every function-tool schema in the same request.

This also affects scriptless file skills because AgentSkillsProviderBuilder.UseFileSkill(s) requires a script runner, and the provider advertises run_skill_script regardless of whether any discovered skill contains scripts.

Minimal reproduction

The following is the only file required. Run it with the .NET 10 SDK:

// Program.cs
#:package Microsoft.Agents.AI@1.20.0

using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

var rootDirectory = Path.Combine(Path.GetTempPath(), $"agent-framework-repro-{Guid.NewGuid():N}");
var skillDirectory = Path.Combine(rootDirectory, "hello");
Directory.CreateDirectory(skillDirectory);
File.WriteAllText(
    Path.Combine(skillDirectory, "SKILL.md"),
    """
    ---
    name: hello
    description: Says hello.
    ---

    Say hello to the user.
    """);

try
{
    using var skills = new AgentSkillsProviderBuilder()
        .UseFileSkills([skillDirectory])
        .UseFileScriptRunner((_, _, _, _, _) =>
            throw new NotSupportedException("This skill has no scripts."))
        .UseOptions(options =>
        {
            options.DisableLoadSkillApproval = true;
            options.DisableReadSkillResourceApproval = true;
        })
        .Build();

    using var client = new CaptureChatClient();
    using var outputSchema = JsonDocument.Parse(
        """{"type":"object","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false}""");
    var chatOptions = new ChatOptions
    {
        ResponseFormat = ChatResponseFormat.ForJsonSchema(outputSchema.RootElement.Clone()),
        AdditionalProperties = new AdditionalPropertiesDictionary
        {
            ["strict"] = true,
        },
    };
    var agent = client.AsAIAgent(
        new ChatClientAgentOptions
        {
            AIContextProviders = [skills],
            ChatOptions = chatOptions,
        });

    await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")]);

    var function = (AIFunction)client.Options!.Tools!
        .Single(tool => tool.Name == AgentSkillsProvider.RunSkillScriptToolName);
    var argumentsSchema = function.JsonSchema
        .GetProperty("properties")
        .GetProperty("arguments");

    Console.WriteLine(argumentsSchema);
    Console.WriteLine($"Has type: {argumentsSchema.TryGetProperty("type", out _)}");
}
finally
{
    Directory.Delete(rootDirectory, recursive: true);
}

sealed class CaptureChatClient : IChatClient
{
    public ChatOptions? Options { get; private set; }

    public Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        Options = options;
        return Task.FromResult(
            new ChatResponse(new ChatMessage(ChatRole.Assistant, "Hello!")));
    }

    public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        Options = options;
        await Task.Yield();
        yield return new ChatResponseUpdate(ChatRole.Assistant, "Hello!");
    }

    public object? GetService(Type serviceType, object? serviceKey = null) =>
        serviceType.IsInstanceOfType(this) ? this : null;

    public void Dispose()
    {
    }
}
dotnet run Program.cs

Actual output:

{"default":null}
Has type: False
Expected behavior

Either:

  1. Do not expose run_skill_script when discovered file skills contain no scripts, or provide an option to disable script execution; and/or
  2. Generate a valid schema for arguments, for example an object schema that OpenAI-compatible providers accept.
Package and runtime versions
  • Microsoft.Agents.AI: 1.20.0
  • .NET SDK: 10.0.303
  • OS: Windows

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.