diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs new file mode 100644 index 0000000000..b0cbd3d793 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI; + +/// +/// Provides extension methods for to enable discoverability of . +/// +public partial class ChatClientAgent +{ + /// + /// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread. + /// + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task RunAsync( + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunAsync(thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent with a text message from the user. + /// + /// The user message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task RunAsync( + string message, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent with a single chat message. + /// + /// The chat message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task RunAsync( + ChatMessage message, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunAsync(message, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent with a collection of chat messages. + /// + /// The collection of messages to send to the agent for processing. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input messages and any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task RunAsync( + IEnumerable messages, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunAsync(messages, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent in streaming mode without providing new input messages, relying on existing context and instructions. + /// + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + public IAsyncEnumerable RunStreamingAsync( + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunStreamingAsync(thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent in streaming mode with a text message from the user. + /// + /// The user message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + public IAsyncEnumerable RunStreamingAsync( + string message, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent in streaming mode with a single chat message. + /// + /// The chat message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + public IAsyncEnumerable RunStreamingAsync( + ChatMessage message, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunStreamingAsync(message, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Runs the agent in streaming mode with a collection of chat messages. + /// + /// The collection of messages to send to the agent for processing. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input messages and any response updates generated during invocation. + /// + /// Configuration parameters for controlling the agent's invocation behavior. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous enumerable of instances representing the streaming response. + public IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + AgentThread? thread, + ChatClientAgentRunOptions? options, + CancellationToken cancellationToken = default) => + this.RunStreamingAsync(messages, thread, (AgentRunOptions?)options, cancellationToken); + + /// + /// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread, and requesting a response of the specified type . + /// + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with any response messages generated during invocation. + /// + /// The JSON serialization options to use. + /// Configuration parameters for controlling the agent's invocation behavior. + /// + /// to set a JSON schema on the ; otherwise, . The default is . + /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task> RunAsync( + AgentThread? thread, + JsonSerializerOptions? serializerOptions, + ChatClientAgentRunOptions? options, + bool? useJsonSchemaResponseFormat = null, + CancellationToken cancellationToken = default) => + this.RunAsync(thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken); + + /// + /// Runs the agent with a text message from the user, requesting a response of the specified type . + /// + /// The user message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// The JSON serialization options to use. + /// Configuration parameters for controlling the agent's invocation behavior. + /// + /// to set a JSON schema on the ; otherwise, . The default is . + /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task> RunAsync( + string message, + AgentThread? thread, + JsonSerializerOptions? serializerOptions, + ChatClientAgentRunOptions? options, + bool? useJsonSchemaResponseFormat = null, + CancellationToken cancellationToken = default) => + this.RunAsync(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken); + + /// + /// Runs the agent with a single chat message, requesting a response of the specified type . + /// + /// The chat message to send to the agent. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input message and any response messages generated during invocation. + /// + /// The JSON serialization options to use. + /// Configuration parameters for controlling the agent's invocation behavior. + /// + /// to set a JSON schema on the ; otherwise, . The default is . + /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task> RunAsync( + ChatMessage message, + AgentThread? thread, + JsonSerializerOptions? serializerOptions, + ChatClientAgentRunOptions? options, + bool? useJsonSchemaResponseFormat = null, + CancellationToken cancellationToken = default) => + this.RunAsync(message, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken); + + /// + /// Runs the agent with a collection of chat messages, requesting a response of the specified type . + /// + /// The collection of messages to send to the agent for processing. + /// + /// The conversation thread to use for this invocation. If , a new thread will be created. + /// The thread will be updated with the input messages and any response messages generated during invocation. + /// + /// The JSON serialization options to use. + /// Configuration parameters for controlling the agent's invocation behavior. + /// + /// to set a JSON schema on the ; otherwise, . The default is . + /// Using a JSON schema improves reliability if the underlying model supports native structured output with a schema, but might cause an error if the model does not support it. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains an with the agent's output. + public Task> RunAsync( + IEnumerable messages, + AgentThread? thread, + JsonSerializerOptions? serializerOptions, + ChatClientAgentRunOptions? options, + bool? useJsonSchemaResponseFormat = null, + CancellationToken cancellationToken = default) => + this.RunAsync(messages, thread, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken); +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs new file mode 100644 index 0000000000..85cb4cf0b4 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_RunWithCustomOptionsTests.cs @@ -0,0 +1,456 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Moq; + +namespace Microsoft.Agents.AI.UnitTests; + +/// +/// Tests for run methods with . +/// +public sealed partial class ChatClientAgent_RunWithCustomOptionsTests +{ + #region RunAsync Tests + + [Fact] + public async Task RunAsync_WithThreadAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse result = await agent.RunAsync(thread, options); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Messages); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsync_WithStringMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse result = await agent.RunAsync("Test message", thread, options); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Messages); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.Is>(msgs => msgs.Any(m => m.Text == "Test message")), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsync_WithChatMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatMessage message = new(ChatRole.User, "Test message"); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse result = await agent.RunAsync(message, thread, options); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Messages); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.Is>(msgs => msgs.Contains(message)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + IEnumerable messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")]; + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse result = await agent.RunAsync(messages, thread, options); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Messages); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsync_WithChatOptionsInRunOptions_UsesChatOptionsAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "Response")])); + + ChatClientAgent agent = new(mockChatClient.Object); + ChatClientAgentRunOptions options = new(new ChatOptions { Temperature = 0.5f }); + + // Act + AgentRunResponse result = await agent.RunAsync("Test", null, options); + + // Assert + Assert.NotNull(result); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.IsAny>(), + It.Is(opts => opts.Temperature == 0.5f), + It.IsAny()), + Times.Once); + } + + #endregion + + #region RunStreamingAsync Tests + + [Fact] + public async Task RunStreamingAsync_WithThreadAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).Returns(GetAsyncUpdatesAsync()); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + var updates = new List(); + await foreach (var update in agent.RunStreamingAsync(thread, options)) + { + updates.Add(update); + } + + // Assert + Assert.NotEmpty(updates); + mockChatClient.Verify( + x => x.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunStreamingAsync_WithStringMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).Returns(GetAsyncUpdatesAsync()); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + var updates = new List(); + await foreach (var update in agent.RunStreamingAsync("Test message", thread, options)) + { + updates.Add(update); + } + + // Assert + Assert.NotEmpty(updates); + mockChatClient.Verify( + x => x.GetStreamingResponseAsync( + It.Is>(msgs => msgs.Any(m => m.Text == "Test message")), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunStreamingAsync_WithChatMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).Returns(GetAsyncUpdatesAsync()); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatMessage message = new(ChatRole.User, "Test message"); + ChatClientAgentRunOptions options = new(); + + // Act + var updates = new List(); + await foreach (var update in agent.RunStreamingAsync(message, thread, options)) + { + updates.Add(update); + } + + // Assert + Assert.NotEmpty(updates); + mockChatClient.Verify( + x => x.GetStreamingResponseAsync( + It.Is>(msgs => msgs.Contains(message)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunStreamingAsync_WithMessagesCollectionAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).Returns(GetAsyncUpdatesAsync()); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + IEnumerable messages = [new ChatMessage(ChatRole.User, "Message 1"), new ChatMessage(ChatRole.User, "Message 2")]; + ChatClientAgentRunOptions options = new(); + + // Act + var updates = new List(); + await foreach (var update in agent.RunStreamingAsync(messages, thread, options)) + { + updates.Add(update); + } + + // Assert + Assert.NotEmpty(updates); + mockChatClient.Verify( + x => x.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + #endregion + + #region Helper Methods + + private static async IAsyncEnumerable GetAsyncUpdatesAsync() + { + yield return new ChatResponseUpdate { Contents = new[] { new TextContent("Hello") } }; + yield return new ChatResponseUpdate { Contents = new[] { new TextContent(" World") } }; + await Task.CompletedTask; + } + + #endregion + + #region RunAsync{T} Tests + + [Fact] + public async Task RunAsyncOfT_WithThreadAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse agentRunResponse = await agent.RunAsync(thread, JsonContext_WithCustomRunOptions.Default.Options, options); + + // Assert + Assert.NotNull(agentRunResponse); + Assert.Single(agentRunResponse.Messages); + Assert.Equal("Tigger", agentRunResponse.Result.FullName); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsyncOfT_WithStringMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse agentRunResponse = await agent.RunAsync("Test message", thread, JsonContext_WithCustomRunOptions.Default.Options, options); + + // Assert + Assert.NotNull(agentRunResponse); + Assert.Single(agentRunResponse.Messages); + Assert.Equal("Tigger", agentRunResponse.Result.FullName); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.Is>(msgs => msgs.Any(m => m.Text == "Test message")), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsyncOfT_WithChatMessageAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + ChatMessage message = new(ChatRole.User, "Test message"); + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse agentRunResponse = await agent.RunAsync(message, thread, JsonContext_WithCustomRunOptions.Default.Options, options); + + // Assert + Assert.NotNull(agentRunResponse); + Assert.Single(agentRunResponse.Messages); + Assert.Equal("Tigger", agentRunResponse.Result.FullName); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.Is>(msgs => msgs.Contains(message)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task RunAsyncOfT_WithMessagesCollectionAndOptions_CallsBaseMethodAsync() + { + // Arrange + Mock mockChatClient = new(); + mockChatClient.Setup( + s => s.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())).ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, """{"id":2, "fullName":"Tigger", "species":"Tiger"}""")])); + + ChatClientAgent agent = new(mockChatClient.Object); + AgentThread thread = agent.GetNewThread(); + IEnumerable messages = [new(ChatRole.User, "Message 1"), new(ChatRole.User, "Message 2")]; + ChatClientAgentRunOptions options = new(); + + // Act + AgentRunResponse agentRunResponse = await agent.RunAsync(messages, thread, JsonContext_WithCustomRunOptions.Default.Options, options); + + // Assert + Assert.NotNull(agentRunResponse); + Assert.Single(agentRunResponse.Messages); + Assert.Equal("Tigger", agentRunResponse.Result.FullName); + mockChatClient.Verify( + x => x.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + #endregion + + private sealed class Animal + { + public int Id { get; set; } + public string? FullName { get; set; } + public Species Species { get; set; } + } + + private enum Species + { + Bear, + Tiger, + Walrus, + } + + [JsonSourceGenerationOptions(UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] + [JsonSerializable(typeof(Animal))] + private sealed partial class JsonContext_WithCustomRunOptions : JsonSerializerContext; +}