Skip to content

.NET: run_skill_script generates invalid schema for arguments #8094

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetagentsUsage: [Issues, PRs], Target: Single agentreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions