diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
index 31f4993723..e210706767 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs
@@ -20,7 +20,7 @@ namespace Microsoft.Agents.AI;
/// may involve multiple agents working together.
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
-public abstract class AIAgent
+public abstract partial class AIAgent
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay =>
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgentStructuredOutput.cs
similarity index 58%
rename from dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs
rename to dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgentStructuredOutput.cs
index d933939884..796b796317 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentStructuredOutput.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgentStructuredOutput.cs
@@ -11,155 +11,126 @@
namespace Microsoft.Agents.AI;
///
-/// Provides an that delegates to an implementation.
+/// Provides structured output methods for that enable requesting responses in a specific type format.
///
-public sealed partial class ChatClientAgent
+public abstract partial class AIAgent
{
///
/// Run the agent with no message assuming that all required instructions are already provided to the agent or on the session, and requesting a response of the specified type .
///
+ /// The type of structured output to request.
///
/// The conversation session to use for this invocation. If , a new session will be created.
/// The session will be updated with any response messages generated during invocation.
///
- /// The JSON serialization options to use.
+ /// Optional JSON serializer options to use for deserializing the response.
/// Optional 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.
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
///
/// This overload is useful when the agent has sufficient context from previous messages in the session
/// or from its initial configuration to generate a meaningful response without additional input.
///
- public Task> RunAsync(
+ public Task> RunAsync(
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
- this.RunAsync([], session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
+ this.RunAsync([], session, serializerOptions, options, cancellationToken);
///
/// Runs the agent with a text message from the user, requesting a response of the specified type .
///
+ /// The type of structured output to request.
/// The user message to send to the agent.
///
/// The conversation session to use for this invocation. If , a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
///
- /// The JSON serialization options to use.
+ /// Optional JSON serializer options to use for deserializing the response.
/// Optional 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.
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
/// is , empty, or contains only whitespace.
///
/// The provided text will be wrapped in a with the role
/// before being sent to the agent. This is a convenience method for simple text-based interactions.
///
- public Task> RunAsync(
+ public Task> RunAsync(
string message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNullOrWhitespace(message);
- return this.RunAsync(new ChatMessage(ChatRole.User, message), session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
+ return this.RunAsync(new ChatMessage(ChatRole.User, message), session, serializerOptions, options, cancellationToken);
}
///
/// Runs the agent with a single chat message, requesting a response of the specified type .
///
+ /// The type of structured output to request.
/// The chat message to send to the agent.
///
/// The conversation session to use for this invocation. If , a new session will be created.
/// The session will be updated with the input message and any response messages generated during invocation.
///
- /// The JSON serialization options to use.
+ /// Optional JSON serializer options to use for deserializing the response.
/// Optional 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.
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
/// is .
- public Task> RunAsync(
+ public Task> RunAsync(
ChatMessage message,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
_ = Throw.IfNull(message);
- return this.RunAsync([message], session, serializerOptions, options, useJsonSchemaResponseFormat, cancellationToken);
+ return this.RunAsync([message], session, serializerOptions, options, cancellationToken);
}
///
/// Runs the agent with a collection of chat messages, requesting a response of the specified type .
///
+ /// The type of structured output to request.
/// The collection of messages to send to the agent for processing.
///
/// The conversation session to use for this invocation. If , a new session will be created.
/// The session will be updated with the input messages and any response messages generated during invocation.
///
- /// The JSON serialization options to use.
+ /// Optional JSON serializer options to use for deserializing the response.
/// Optional 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.
- /// The type of structured output to request.
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
///
///
- /// This is the primary invocation method that implementations must override. It handles collections of messages,
- /// allowing for complex conversational scenarios including multi-turn interactions, function calls, and
- /// context-rich conversations.
+ /// This method handles collections of messages, allowing for complex conversational scenarios including
+ /// multi-turn interactions, function calls, and context-rich conversations.
///
///
/// The messages are processed in the order provided and become part of the conversation history.
/// The agent's response will also be added to if one is provided.
///
///
- public Task> RunAsync(
+ public async Task> RunAsync(
IEnumerable messages,
AgentSession? session = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default)
{
- async Task> GetResponseAsync(IChatClient chatClient, List threadMessages, ChatOptions? chatOptions, CancellationToken ct)
- {
- return await chatClient.GetResponseAsync(
- threadMessages,
- serializerOptions ?? AgentJsonUtilities.DefaultOptions,
- chatOptions,
- useJsonSchemaResponseFormat,
- ct).ConfigureAwait(false);
- }
+ serializerOptions ??= AgentAbstractionsJsonUtilities.DefaultOptions;
+
+ options = options?.Clone() ?? new AgentRunOptions();
+ options.ResponseFormat = ChatResponseFormat.ForJsonSchema(serializerOptions);
- static ChatClientAgentResponse CreateResponse(ChatResponse chatResponse)
- {
- return new ChatClientAgentResponse(chatResponse)
- {
- ContinuationToken = WrapContinuationToken(chatResponse.ContinuationToken)
- };
- }
+ AgentResponse response = await this.RunAsync(messages, session, options, cancellationToken).ConfigureAwait(false);
- return this.RunCoreAsync(GetResponseAsync, CreateResponse, messages, session, options, cancellationToken);
+ return new AgentResponse(response, serializerOptions);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs
index d79dbee135..bd07d7cb8f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs
@@ -68,6 +68,29 @@ public AgentResponse(ChatResponse response)
this.ContinuationToken = response.ContinuationToken;
}
+ ///
+ /// Initializes a new instance of the class from an existing .
+ ///
+ /// The from which to copy properties.
+ /// is .
+ ///
+ /// This constructor creates a copy of an existing agent response, preserving all
+ /// metadata and storing the original response in for access to
+ /// the underlying implementation details.
+ ///
+ protected AgentResponse(AgentResponse response)
+ {
+ _ = Throw.IfNull(response);
+
+ this.AdditionalProperties = response.AdditionalProperties;
+ this.CreatedAt = response.CreatedAt;
+ this.Messages = response.Messages;
+ this.RawRepresentation = response;
+ this.ResponseId = response.ResponseId;
+ this.Usage = response.Usage;
+ this.ContinuationToken = response.ContinuationToken;
+ }
+
///
/// Initializes a new instance of the class with the specified collection of messages.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse{T}.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse{T}.cs
index 2a18aadb37..c75e78211d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse{T}.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse{T}.cs
@@ -1,6 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
-using Microsoft.Extensions.AI;
+using System;
+#if NET
+using System.Buffers;
+#endif
+
+#if NET
+using System.Text;
+#endif
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
namespace Microsoft.Agents.AI;
@@ -8,23 +18,64 @@ namespace Microsoft.Agents.AI;
/// Represents the response of the specified type to an run request.
///
/// The type of value expected from the agent.
-public abstract class AgentResponse : AgentResponse
+public class AgentResponse : AgentResponse
{
- /// Initializes a new instance of the class.
- protected AgentResponse()
- {
- }
+ private readonly JsonSerializerOptions _serializerOptions;
///
- /// Initializes a new instance of the class from an existing .
+ /// Initializes a new instance of the class.
///
- /// The from which to populate this .
- protected AgentResponse(ChatResponse response) : base(response)
+ /// The from which to populate this .
+ /// The to use when deserializing the result.
+ public AgentResponse(AgentResponse response, JsonSerializerOptions serializerOptions) : base(response)
{
+ this._serializerOptions = serializerOptions;
}
///
/// Gets the result value of the agent response as an instance of .
///
- public abstract T Result { get; }
+ [JsonIgnore]
+ public virtual T Result
+ {
+ get
+ {
+ var json = this.Text;
+ if (string.IsNullOrEmpty(json))
+ {
+ throw new InvalidOperationException("The response did not contain JSON to be deserialized.");
+ }
+
+ T? deserialized = DeserializeFirstTopLevelObject(json!, (JsonTypeInfo)this._serializerOptions.GetTypeInfo(typeof(T)));
+ if (deserialized is null)
+ {
+ throw new InvalidOperationException("The deserialized response is null.");
+ }
+
+ return deserialized;
+ }
+ }
+
+ private static T? DeserializeFirstTopLevelObject(string json, JsonTypeInfo typeInfo)
+ {
+#if NET
+ // We need to deserialize only the first top-level object as a workaround for a common LLM backend
+ // issue. GPT 3.5 Turbo commonly returns multiple top-level objects after doing a function call.
+ // See https://community.openai.com/t/2-json-objects-returned-when-using-function-calling-and-json-mode/574348
+ var utf8ByteLength = Encoding.UTF8.GetByteCount(json);
+ var buffer = ArrayPool.Shared.Rent(utf8ByteLength);
+ try
+ {
+ var utf8SpanLength = Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
+ var reader = new Utf8JsonReader(new ReadOnlySpan(buffer, 0, utf8SpanLength), new() { AllowMultipleValues = true });
+ return JsonSerializer.Deserialize(ref reader, typeInfo);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+#else
+ return JsonSerializer.Deserialize(json, typeInfo);
+#endif
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs
index 611d15036c..08811df288 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs
@@ -28,12 +28,13 @@ public AgentRunOptions()
///
/// The options instance from which to copy values.
/// is .
- public AgentRunOptions(AgentRunOptions options)
+ protected AgentRunOptions(AgentRunOptions options)
{
_ = Throw.IfNull(options);
this.ContinuationToken = options.ContinuationToken;
this.AllowBackgroundResponses = options.AllowBackgroundResponses;
this.AdditionalProperties = options.AdditionalProperties?.Clone();
+ this.ResponseFormat = options.ResponseFormat;
}
///
@@ -90,4 +91,35 @@ public AgentRunOptions(AgentRunOptions options)
/// preserving implementation-specific details or extending the options with custom data.
///
public AdditionalPropertiesDictionary? AdditionalProperties { get; set; }
+
+ ///
+ /// Gets or sets the response format.
+ ///
+ ///
+ /// If , no response format is specified and the agent will use its default.
+ /// This property can be set to to specify that the response should be unstructured text,
+ /// to to specify that the response should be structured JSON data, or
+ /// an instance of constructed with a specific JSON schema to request that the
+ /// response be structured JSON data according to that schema. It is up to the agent implementation if or how
+ /// to honor the request. If the agent implementation doesn't recognize the specific kind of ,
+ /// it can be ignored.
+ ///
+ public ChatResponseFormat? ResponseFormat { get; set; }
+
+ ///
+ /// Produces a clone of the current instance.
+ ///
+ ///
+ /// A clone of the current instance.
+ ///
+ ///
+ ///
+ /// The clone will have the same values for all properties as the original instance. Any collections, like ,
+ /// are shallow-cloned, meaning a new collection instance is created, but any references contained by the collections are shared with the original.
+ ///
+ ///
+ /// Derived types should override to return an instance of the derived type.
+ ///
+ ///
+ public virtual AgentRunOptions Clone() => new(this);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
index 2c1460b213..bcc6c1edbf 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
@@ -8,6 +8,7 @@
- Switch to new "Run" method name ([#2843](https://github.com/microsoft/agent-framework/pull/2843))
- Removed AgentThreadMetadata and used AgentSessionId directly instead ([#3067](https://github.com/microsoft/agent-framework/pull/3067));
- Renamed AgentThread to AgentSession ([#3430](https://github.com/microsoft/agent-framework/pull/3430))
+- Updated to use base `AgentRunOptions.ResponseFormat` for structured output configuration ([#3658](https://github.com/microsoft/agent-framework/pull/3658))
## v1.0.0-preview.251204.1
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
index a97652bd93..840c229726 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
-using System.Text.Json.Serialization.Metadata;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.AI;
@@ -92,7 +90,6 @@ protected override async Task RunCoreAsync(
{
enableToolCalls = durableOptions.EnableToolCalls;
enableToolNames = durableOptions.EnableToolNames;
- responseFormat = durableOptions.ResponseFormat;
}
else if (options is ChatClientAgentRunOptions chatClientOptions && chatClientOptions.ChatOptions?.Tools != null)
{
@@ -100,6 +97,12 @@ protected override async Task RunCoreAsync(
responseFormat = chatClientOptions.ChatOptions?.ResponseFormat;
}
+ // Override the response format if specified in the agent run options
+ if (options?.ResponseFormat is { } format)
+ {
+ responseFormat = format;
+ }
+
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames)
{
OrchestrationId = this._context.InstanceId
@@ -144,110 +147,4 @@ protected override async IAsyncEnumerable RunCoreStreamingA
yield return update;
}
}
-
- ///
- /// Runs the agent with a message and returns the deserialized output as an instance of .
- ///
- /// The message to send to the agent.
- /// The agent session to use.
- /// Optional JSON serializer options.
- /// Optional run options.
- /// The cancellation token.
- /// The type of the output.
- ///
- /// Thrown when the provided already contains a response schema.
- /// Thrown when the provided is not a .
- ///
- ///
- /// Thrown when the agent response is empty or cannot be deserialized.
- ///
- /// The output from the agent.
- public async Task> RunAsync(
- string message,
- AgentSession? session = null,
- JsonSerializerOptions? serializerOptions = null,
- AgentRunOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- return await this.RunAsync(
- messages: [new ChatMessage(ChatRole.User, message) { CreatedAt = DateTimeOffset.UtcNow }],
- session,
- serializerOptions,
- options,
- cancellationToken);
- }
-
- ///
- /// Runs the agent with messages and returns the deserialized output as an instance of .
- ///
- /// The messages to send to the agent.
- /// The agent session to use.
- /// Optional JSON serializer options.
- /// Optional run options.
- /// The cancellation token.
- /// The type of the output.
- ///
- /// Thrown when the provided already contains a response schema.
- /// Thrown when the provided is not a .
- ///
- ///
- /// Thrown when the agent response is empty or cannot be deserialized.
- ///
- /// The output from the agent.
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback to reflection-based deserialization is intentional for library flexibility with user-defined types.")]
- [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "Fallback to reflection-based deserialization is intentional for library flexibility with user-defined types.")]
- public async Task> RunAsync(
- IEnumerable messages,
- AgentSession? session = null,
- JsonSerializerOptions? serializerOptions = null,
- AgentRunOptions? options = null,
- CancellationToken cancellationToken = default)
- {
- options ??= new DurableAgentRunOptions();
- if (options is not DurableAgentRunOptions durableOptions)
- {
- throw new ArgumentException(
- "Response schema is only supported with DurableAgentRunOptions when using durable agents. " +
- "Cannot specify a response schema when calling RunAsync.",
- paramName: nameof(options));
- }
-
- if (durableOptions.ResponseFormat is not null)
- {
- throw new ArgumentException(
- "A response schema is already defined in the provided DurableAgentRunOptions. " +
- "Cannot specify a response schema when calling RunAsync.",
- paramName: nameof(options));
- }
-
- // Create the JSON schema for the response type
- durableOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema();
-
- AgentResponse response = await this.RunAsync(messages, session, durableOptions, cancellationToken);
-
- // Deserialize the response text to the requested type
- if (string.IsNullOrEmpty(response.Text))
- {
- throw new InvalidOperationException("Agent response is empty and cannot be deserialized.");
- }
-
- serializerOptions ??= DurableAgentJsonUtilities.DefaultOptions;
-
- // Prefer source-generated metadata when available to support AOT/trimming scenarios.
- // Fallback to reflection-based deserialization for types without source-generated metadata.
- // This is necessary since T is a user-provided type that may not have [JsonSerializable] coverage.
- JsonTypeInfo? typeInfo = serializerOptions.GetTypeInfo(typeof(T));
- T? result = (typeInfo is JsonTypeInfo typedInfo
- ? (T?)JsonSerializer.Deserialize(response.Text, typedInfo)
- : JsonSerializer.Deserialize(response.Text, serializerOptions))
- ?? throw new InvalidOperationException($"Failed to deserialize agent response to type {typeof(T).Name}.");
-
- return new DurableAIAgentResponse(response, result);
- }
-
- private sealed class DurableAIAgentResponse(AgentResponse response, T result)
- : AgentResponse(response.AsChatResponse())
- {
- public override T Result { get; } = result;
- }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
index b6d3fa4900..f5cbec05de 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
@@ -47,7 +47,6 @@ protected override async Task RunCoreAsync(
{
enableToolCalls = durableOptions.EnableToolCalls;
enableToolNames = durableOptions.EnableToolNames;
- responseFormat = durableOptions.ResponseFormat;
isFireAndForget = durableOptions.IsFireAndForget;
}
else if (options is ChatClientAgentRunOptions chatClientOptions)
@@ -56,6 +55,12 @@ protected override async Task RunCoreAsync(
responseFormat = chatClientOptions.ChatOptions?.ResponseFormat;
}
+ // Override the response format if specified in the agent run options
+ if (options?.ResponseFormat is { } format)
+ {
+ responseFormat = format;
+ }
+
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
AgentSessionId sessionId = durableSession.SessionId;
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs
index 0f1984ad62..f698eab3d8 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs
@@ -1,7 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using Microsoft.Extensions.AI;
-
namespace Microsoft.Agents.AI.DurableTask;
///
@@ -9,6 +7,25 @@ namespace Microsoft.Agents.AI.DurableTask;
///
public sealed class DurableAgentRunOptions : AgentRunOptions
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DurableAgentRunOptions()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class by copying values from the specified options.
+ ///
+ /// The options instance from which to copy values.
+ private DurableAgentRunOptions(DurableAgentRunOptions options)
+ : base(options)
+ {
+ this.EnableToolCalls = options.EnableToolCalls;
+ this.EnableToolNames = options.EnableToolNames is not null ? new List(options.EnableToolNames) : null;
+ this.IsFireAndForget = options.IsFireAndForget;
+ }
+
///
/// Gets or sets whether to enable tool calls for this request.
///
@@ -19,11 +36,6 @@ public sealed class DurableAgentRunOptions : AgentRunOptions
///
public IList? EnableToolNames { get; set; }
- ///
- /// Gets or sets the response format for the agent's response.
- ///
- public ChatResponseFormat? ResponseFormat { get; set; }
-
///
/// Gets or sets whether to fire and forget the agent run request.
///
@@ -33,4 +45,7 @@ public sealed class DurableAgentRunOptions : AgentRunOptions
/// long-running tasks where the caller does not need to wait for the agent to complete the run.
///
public bool IsFireAndForget { get; set; }
+
+ ///
+ public override AgentRunOptions Clone() => new DurableAgentRunOptions(this);
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
index 504928d2d6..77ab64a073 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
@@ -634,6 +634,12 @@ await session.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvide
chatOptions.AllowBackgroundResponses = agentRunOptions.AllowBackgroundResponses;
}
+ if (agentRunOptions?.ResponseFormat is not null)
+ {
+ chatOptions ??= new ChatOptions();
+ chatOptions.ResponseFormat = agentRunOptions.ResponseFormat;
+ }
+
ChatClientAgentContinuationToken? agentContinuationToken = null;
if ((agentRunOptions?.ContinuationToken ?? chatOptions?.ContinuationToken) is { } continuationToken)
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs
index 25ae5a903e..e5d6296700 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentCustomOptions.cs
@@ -162,19 +162,14 @@ public IAsyncEnumerable RunStreamingAsync(
///
/// 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(
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
+ public Task> RunAsync(
AgentSession? session,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
- this.RunAsync(session, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
+ this.RunAsync(session, serializerOptions, (AgentRunOptions?)options, cancellationToken);
///
/// Runs the agent with a text message from the user, requesting a response of the specified type .
@@ -186,20 +181,15 @@ public Task> RunAsync(
///
/// 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(
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
+ public Task> RunAsync(
string message,
AgentSession? session,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
- this.RunAsync(message, session, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
+ this.RunAsync(message, session, serializerOptions, (AgentRunOptions?)options, cancellationToken);
///
/// Runs the agent with a single chat message, requesting a response of the specified type .
@@ -211,20 +201,15 @@ public Task> RunAsync(
///
/// 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(
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
+ public Task> RunAsync(
ChatMessage message,
AgentSession? session,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
- this.RunAsync(message, session, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
+ this.RunAsync(message, session, serializerOptions, (AgentRunOptions?)options, cancellationToken);
///
/// Runs the agent with a collection of chat messages, requesting a response of the specified type .
@@ -236,18 +221,13 @@ public Task> RunAsync(
///
/// 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(
+ /// A task that represents the asynchronous operation. The task result contains an with the agent's output.
+ public Task> RunAsync(
IEnumerable messages,
AgentSession? session,
JsonSerializerOptions? serializerOptions,
ChatClientAgentRunOptions? options,
- bool? useJsonSchemaResponseFormat = null,
CancellationToken cancellationToken = default) =>
- this.RunAsync(messages, session, serializerOptions, (AgentRunOptions?)options, useJsonSchemaResponseFormat, cancellationToken);
+ this.RunAsync(messages, session, serializerOptions, (AgentRunOptions?)options, cancellationToken);
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunOptions.cs
index 0f2c9da485..cf35aa80a1 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunOptions.cs
@@ -26,6 +26,17 @@ public ChatClientAgentRunOptions(ChatOptions? chatOptions = null)
this.ChatOptions = chatOptions;
}
+ ///
+ /// Initializes a new instance of the class by copying values from the specified options.
+ ///
+ /// The options instance from which to copy values.
+ private ChatClientAgentRunOptions(ChatClientAgentRunOptions options)
+ : base(options)
+ {
+ this.ChatOptions = options.ChatOptions?.Clone();
+ this.ChatClientFactory = options.ChatClientFactory;
+ }
+
///
/// Gets or sets the chat options to apply to the agent invocation.
///
@@ -50,4 +61,7 @@ public ChatClientAgentRunOptions(ChatOptions? chatOptions = null)
/// chat client will be used without modification.
///
public Func? ChatClientFactory { get; set; }
+
+ ///
+ public override AgentRunOptions Clone() => new ChatClientAgentRunOptions(this);
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunResponse{T}.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunResponse{T}.cs
deleted file mode 100644
index a4fadff0c7..0000000000
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunResponse{T}.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using Microsoft.Extensions.AI;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Agents.AI;
-
-///
-/// Represents the response of the specified type to an run request.
-///
-/// The type of value expected from the chat response.
-///
-/// Language models are not guaranteed to honor the requested schema. If the model's output is not
-/// parsable as the expected type, you can access the underlying JSON response on the property.
-///
-public sealed class ChatClientAgentResponse : AgentResponse
-{
- private readonly ChatResponse _response;
-
- ///
- /// Initializes a new instance of the class from an existing .
- ///
- /// The from which to populate this .
- /// is .
- ///
- /// This constructor creates an agent response that wraps an existing , preserving all
- /// metadata and storing the original response in for access to
- /// the underlying implementation details.
- ///
- public ChatClientAgentResponse(ChatResponse response) : base(response)
- {
- _ = Throw.IfNull(response);
-
- this._response = response;
- }
-
- ///
- /// Gets the result value of the agent response as an instance of .
- ///
- ///
- /// If the response did not contain JSON, or if deserialization fails, this property will throw.
- ///
- public override T Result => this._response.Result;
-}
diff --git a/dotnet/tests/AgentConformance.IntegrationTests/StructuredOutputRunTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/StructuredOutputRunTests.cs
new file mode 100644
index 0000000000..5901986aa1
--- /dev/null
+++ b/dotnet/tests/AgentConformance.IntegrationTests/StructuredOutputRunTests.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Text.Json;
+using System.Threading.Tasks;
+using AgentConformance.IntegrationTests.Support;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace AgentConformance.IntegrationTests;
+
+///
+/// Conformance tests for structured output handling for run methods on agents.
+///
+/// The type of test fixture used by the concrete test implementation.
+/// Function to create the test fixture with.
+public abstract class StructuredOutputRunTests(Func createAgentFixture) : AgentTests(createAgentFixture)
+ where TAgentFixture : IAgentFixture
+{
+ [RetryFact(Constants.RetryCount, Constants.RetryDelay)]
+ public virtual async Task RunWithResponseFormatReturnsExpectedResultAsync()
+ {
+ // Arrange
+ var agent = this.Fixture.Agent;
+ var session = await agent.CreateSessionAsync();
+ await using var cleanup = new SessionCleanup(session, this.Fixture);
+
+ var options = new AgentRunOptions
+ {
+ ResponseFormat = ChatResponseFormat.ForJsonSchema(AgentAbstractionsJsonUtilities.DefaultOptions)
+ };
+
+ // Act
+ var response = await agent.RunAsync(new ChatMessage(ChatRole.User, "Provide information about the capital of France."), session, options);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Single(response.Messages);
+ Assert.Contains("Paris", response.Text);
+ Assert.True(TryDeserialize(response.Text, AgentAbstractionsJsonUtilities.DefaultOptions, out CityInfo cityInfo));
+ Assert.Equal("Paris", cityInfo.Name);
+ }
+
+ [RetryFact(Constants.RetryCount, Constants.RetryDelay)]
+ public virtual async Task RunWithGenericTypeReturnsExpectedResultAsync()
+ {
+ // Arrange
+ var agent = this.Fixture.Agent;
+ var session = await agent.CreateSessionAsync();
+ await using var cleanup = new SessionCleanup(session, this.Fixture);
+
+ // Act
+ AgentResponse response = await agent.RunAsync(
+ new ChatMessage(ChatRole.User, "Provide information about the capital of France."),
+ session);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Single(response.Messages);
+ Assert.Contains("Paris", response.Text);
+
+ Assert.NotNull(response.Result);
+ Assert.Equal("Paris", response.Result.Name);
+ }
+
+ protected static bool TryDeserialize(string json, JsonSerializerOptions jsonSerializerOptions, out T structuredOutput)
+ {
+ try
+ {
+ T? deserialized = JsonSerializer.Deserialize(json, jsonSerializerOptions);
+ if (deserialized is null)
+ {
+ structuredOutput = default!;
+ return false;
+ }
+
+ structuredOutput = deserialized;
+ return true;
+ }
+ catch
+ {
+ structuredOutput = default!;
+ return false;
+ }
+ }
+}
+
+public sealed class CityInfo
+{
+ public string? Name { get; set; }
+}
diff --git a/dotnet/tests/AgentConformance.IntegrationTests/Support/Constants.cs b/dotnet/tests/AgentConformance.IntegrationTests/Support/Constants.cs
index 178b1951ba..232b5fdb10 100644
--- a/dotnet/tests/AgentConformance.IntegrationTests/Support/Constants.cs
+++ b/dotnet/tests/AgentConformance.IntegrationTests/Support/Constants.cs
@@ -2,7 +2,7 @@
namespace AgentConformance.IntegrationTests.Support;
-internal static class Constants
+public static class Constants
{
public const int RetryCount = 3;
public const int RetryDelay = 5000;
diff --git a/dotnet/tests/AgentConformance.IntegrationTests/Support/SessionCleanup.cs b/dotnet/tests/AgentConformance.IntegrationTests/Support/SessionCleanup.cs
index c59b999fd2..91e858e53f 100644
--- a/dotnet/tests/AgentConformance.IntegrationTests/Support/SessionCleanup.cs
+++ b/dotnet/tests/AgentConformance.IntegrationTests/Support/SessionCleanup.cs
@@ -11,7 +11,7 @@ namespace AgentConformance.IntegrationTests.Support;
///
/// The session to delete.
/// The fixture that provides agent specific capabilities.
-internal sealed class SessionCleanup(AgentSession session, IAgentFixture fixture) : IAsyncDisposable
+public sealed class SessionCleanup(AgentSession session, IAgentFixture fixture) : IAsyncDisposable
{
public async ValueTask DisposeAsync() =>
await fixture.DeleteSessionAsync(session);
diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs
new file mode 100644
index 0000000000..3b69b5693a
--- /dev/null
+++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientAgentStructuredOutputRunTests.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using AgentConformance.IntegrationTests;
+using AgentConformance.IntegrationTests.Support;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+namespace AzureAI.IntegrationTests;
+
+public class AIProjectClientAgentStructuredOutputRunTests() : StructuredOutputRunTests>(() => new AIProjectClientStructuredOutputFixture())
+{
+ private const string NotSupported = "AIProjectClient does not support specifying structured output type at invocation time.";
+
+ ///
+ /// Verifies that response format provided at agent initialization is used when invoking RunAsync.
+ ///
+ ///
+ [RetryFact(Constants.RetryCount, Constants.RetryDelay)]
+ public async Task RunWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
+ {
+ // Arrange
+ var agent = this.Fixture.Agent;
+ var session = await agent.CreateSessionAsync();
+ await using var cleanup = new SessionCleanup(session, this.Fixture);
+
+ // Act
+ var response = await agent.RunAsync(new ChatMessage(ChatRole.User, "Provide information about the capital of France."), session);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Single(response.Messages);
+ Assert.Contains("Paris", response.Text);
+ Assert.True(TryDeserialize(response.Text, AgentAbstractionsJsonUtilities.DefaultOptions, out CityInfo cityInfo));
+ Assert.Equal("Paris", cityInfo.Name);
+ }
+
+ ///
+ /// Verifies that generic RunAsync works with AIProjectClient when structured output is configured at agent initialization.
+ ///
+ ///
+ /// AIProjectClient does not support specifying the structured output type at invocation time yet.
+ /// The type T provided to RunAsync<T> is ignored by AzureAIProjectChatClient and is only used
+ /// for deserializing the agent response by AgentResponse<T>.Result.
+ ///
+ [RetryFact(Constants.RetryCount, Constants.RetryDelay)]
+ public async Task RunGenericWithResponseFormatAtAgentInitializationReturnsExpectedResultAsync()
+ {
+ // Arrange
+ var agent = this.Fixture.Agent;
+ var session = await agent.CreateSessionAsync();
+ await using var cleanup = new SessionCleanup(session, this.Fixture);
+
+ // Act
+ AgentResponse response = await agent.RunAsync(
+ new ChatMessage(ChatRole.User, "Provide information about the capital of France."),
+ session);
+
+ // Assert
+ Assert.NotNull(response);
+ Assert.Single(response.Messages);
+ Assert.Contains("Paris", response.Text);
+
+ Assert.NotNull(response.Result);
+ Assert.Equal("Paris", response.Result.Name);
+ }
+
+ [Fact(Skip = NotSupported)]
+ public override Task RunWithGenericTypeReturnsExpectedResultAsync() =>
+ base.RunWithGenericTypeReturnsExpectedResultAsync();
+
+ [Fact(Skip = NotSupported)]
+ public override Task RunWithResponseFormatReturnsExpectedResultAsync() =>
+ base.RunWithResponseFormatReturnsExpectedResultAsync();
+}
+
+///
+/// Represents a fixture for testing AIProjectClient with structured output of type provided at agent initialization.
+///
+public class AIProjectClientStructuredOutputFixture : AIProjectClientFixture
+{
+ public override Task InitializeAsync()
+ {
+ var agentOptions = new ChatClientAgentOptions
+ {
+ ChatOptions = new ChatOptions()
+ {
+ ResponseFormat = ChatResponseFormat.ForJsonSchema(AgentAbstractionsJsonUtilities.DefaultOptions)
+ },
+ };
+
+ return this.InitializeAsync(agentOptions);
+ }
+}
diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs
index 4b78d30f1c..618ab1199b 100644
--- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs
+++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs
@@ -119,6 +119,13 @@ public async Task CreateChatClientAgentAsync(
return await this._client.CreateAIAgentAsync(GenerateUniqueAgentName(name), model: s_config.DeploymentName, instructions: instructions, tools: aiTools);
}
+ public async Task CreateChatClientAgentAsync(ChatClientAgentOptions options)
+ {
+ options.Name ??= GenerateUniqueAgentName("HelpfulAssistant");
+
+ return await this._client.CreateAIAgentAsync(model: s_config.DeploymentName, options);
+ }
+
public static string GenerateUniqueAgentName(string baseName) =>
$"{baseName}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
@@ -159,9 +166,15 @@ public Task DisposeAsync()
return Task.CompletedTask;
}
- public async Task InitializeAsync()
+ public virtual async Task InitializeAsync()
{
this._client = new(new Uri(s_config.Endpoint), new AzureCliCredential());
this._agent = await this.CreateChatClientAgentAsync();
}
+
+ public async Task InitializeAsync(ChatClientAgentOptions options)
+ {
+ this._client = new(new Uri(s_config.Endpoint), new AzureCliCredential());
+ this._agent = await this.CreateChatClientAgentAsync(options);
+ }
}
diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs
new file mode 100644
index 0000000000..8b628770c7
--- /dev/null
+++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentStructuredOutputRunTests.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace AzureAIAgentsPersistent.IntegrationTests;
+
+public class AzureAIAgentsPersistentStructuredOutputRunTests() : StructuredOutputRunTests(() => new())
+{
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs
index 7460ea4623..028828c520 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentRunOptionsTests.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
using System.Text.Json;
using Microsoft.Extensions.AI;
@@ -27,7 +26,7 @@ public void CloningConstructorCopiesProperties()
};
// Act
- var clone = new AgentRunOptions(options);
+ var clone = options.Clone();
// Assert
Assert.NotNull(clone);
@@ -39,11 +38,6 @@ public void CloningConstructorCopiesProperties()
Assert.Equal(42, clone.AdditionalProperties["key2"]);
}
- [Fact]
- public void CloningConstructorThrowsIfNull() =>
- // Act & Assert
- Assert.Throws(() => new AgentRunOptions(null!));
-
[Fact]
public void JsonSerializationRoundtrips()
{
@@ -77,4 +71,57 @@ public void JsonSerializationRoundtrips()
Assert.IsType(value2);
Assert.Equal(42, ((JsonElement)value2!).GetInt32());
}
+
+ [Fact]
+ public void CloneReturnsNewInstanceWithSameValues()
+ {
+ // Arrange
+ var options = new AgentRunOptions
+ {
+ ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
+ AllowBackgroundResponses = true,
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1",
+ ["key2"] = 42
+ },
+ ResponseFormat = ChatResponseFormat.Json
+ };
+
+ // Act
+ AgentRunOptions clone = options.Clone();
+
+ // Assert
+ Assert.NotNull(clone);
+ Assert.IsType(clone);
+ Assert.NotSame(options, clone);
+ Assert.Same(options.ContinuationToken, clone.ContinuationToken);
+ Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
+ Assert.NotNull(clone.AdditionalProperties);
+ Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
+ Assert.Equal("value1", clone.AdditionalProperties["key1"]);
+ Assert.Equal(42, clone.AdditionalProperties["key2"]);
+ Assert.Same(options.ResponseFormat, clone.ResponseFormat);
+ }
+
+ [Fact]
+ public void CloneCreatesIndependentAdditionalPropertiesDictionary()
+ {
+ // Arrange
+ var options = new AgentRunOptions
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1"
+ }
+ };
+
+ // Act
+ AgentRunOptions clone = options.Clone();
+ clone.AdditionalProperties!["key2"] = "value2";
+
+ // Assert
+ Assert.True(clone.AdditionalProperties.ContainsKey("key2"));
+ Assert.False(options.AdditionalProperties.ContainsKey("key2"));
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentRunOptionsTests.cs
new file mode 100644
index 0000000000..77012f4957
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentRunOptionsTests.cs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask.UnitTests;
+
+///
+/// Unit tests for the class.
+///
+public sealed class DurableAgentRunOptionsTests
+{
+ [Fact]
+ public void CloneReturnsNewInstanceWithSameValues()
+ {
+ // Arrange
+ DurableAgentRunOptions options = new()
+ {
+ EnableToolCalls = false,
+ EnableToolNames = new List { "tool1", "tool2" },
+ IsFireAndForget = true,
+ AllowBackgroundResponses = true,
+ ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1",
+ ["key2"] = 42
+ },
+ ResponseFormat = ChatResponseFormat.Json
+ };
+
+ // Act
+ AgentRunOptions cloneAsBase = options.Clone();
+
+ // Assert
+ Assert.NotNull(cloneAsBase);
+ Assert.IsType(cloneAsBase);
+ DurableAgentRunOptions clone = (DurableAgentRunOptions)cloneAsBase;
+ Assert.NotSame(options, clone);
+ Assert.Equal(options.EnableToolCalls, clone.EnableToolCalls);
+ Assert.NotNull(clone.EnableToolNames);
+ Assert.NotSame(options.EnableToolNames, clone.EnableToolNames);
+ Assert.Equal(2, clone.EnableToolNames.Count);
+ Assert.Contains("tool1", clone.EnableToolNames);
+ Assert.Contains("tool2", clone.EnableToolNames);
+ Assert.Equal(options.IsFireAndForget, clone.IsFireAndForget);
+ Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
+ Assert.Same(options.ContinuationToken, clone.ContinuationToken);
+ Assert.NotNull(clone.AdditionalProperties);
+ Assert.NotSame(options.AdditionalProperties, clone.AdditionalProperties);
+ Assert.Equal("value1", clone.AdditionalProperties["key1"]);
+ Assert.Equal(42, clone.AdditionalProperties["key2"]);
+ Assert.Same(options.ResponseFormat, clone.ResponseFormat);
+ }
+
+ [Fact]
+ public void CloneCreatesIndependentEnableToolNamesList()
+ {
+ // Arrange
+ DurableAgentRunOptions options = new()
+ {
+ EnableToolNames = new List { "tool1" }
+ };
+
+ // Act
+ DurableAgentRunOptions clone = (DurableAgentRunOptions)options.Clone();
+ clone.EnableToolNames!.Add("tool2");
+
+ // Assert
+ Assert.Equal(2, clone.EnableToolNames.Count);
+ Assert.Single(options.EnableToolNames);
+ Assert.DoesNotContain("tool2", options.EnableToolNames);
+ }
+
+ [Fact]
+ public void CloneCreatesIndependentAdditionalPropertiesDictionary()
+ {
+ // Arrange
+ DurableAgentRunOptions options = new()
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1"
+ }
+ };
+
+ // Act
+ DurableAgentRunOptions clone = (DurableAgentRunOptions)options.Clone();
+ clone.AdditionalProperties!["key2"] = "value2";
+
+ // Assert
+ Assert.True(clone.AdditionalProperties.ContainsKey("key2"));
+ Assert.False(options.AdditionalProperties.ContainsKey("key2"));
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentRunOptionsTests.cs
index 1aa49dc328..7a00a9f796 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentRunOptionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentRunOptionsTests.cs
@@ -332,4 +332,91 @@ await Assert.ThrowsAsync(async () =>
}
#endregion
+
+ #region Clone Tests
+
+ ///
+ /// Verify that Clone returns a new instance with the same property values.
+ ///
+ [Fact]
+ public void CloneReturnsNewInstanceWithSameValues()
+ {
+ // Arrange
+ var chatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f };
+ Func factory = c => c;
+ var runOptions = new ChatClientAgentRunOptions(chatOptions)
+ {
+ ChatClientFactory = factory,
+ AllowBackgroundResponses = true,
+ ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1"
+ }
+ };
+
+ // Act
+ AgentRunOptions cloneAsBase = runOptions.Clone();
+
+ // Assert
+ Assert.NotNull(cloneAsBase);
+ Assert.IsType(cloneAsBase);
+ ChatClientAgentRunOptions clone = (ChatClientAgentRunOptions)cloneAsBase;
+ Assert.NotSame(runOptions, clone);
+ Assert.NotNull(clone.ChatOptions);
+ Assert.NotSame(runOptions.ChatOptions, clone.ChatOptions);
+ Assert.Equal(100, clone.ChatOptions!.MaxOutputTokens);
+ Assert.Equal(0.7f, clone.ChatOptions.Temperature);
+ Assert.Same(factory, clone.ChatClientFactory);
+ Assert.Equal(runOptions.AllowBackgroundResponses, clone.AllowBackgroundResponses);
+ Assert.Same(runOptions.ContinuationToken, clone.ContinuationToken);
+ Assert.NotNull(clone.AdditionalProperties);
+ Assert.NotSame(runOptions.AdditionalProperties, clone.AdditionalProperties);
+ Assert.Equal("value1", clone.AdditionalProperties["key1"]);
+ }
+
+ ///
+ /// Verify that modifying the cloned ChatOptions does not affect the original.
+ ///
+ [Fact]
+ public void CloneCreatesIndependentChatOptions()
+ {
+ // Arrange
+ var chatOptions = new ChatOptions { MaxOutputTokens = 100 };
+ var runOptions = new ChatClientAgentRunOptions(chatOptions);
+
+ // Act
+ ChatClientAgentRunOptions clone = (ChatClientAgentRunOptions)runOptions.Clone();
+ clone.ChatOptions!.MaxOutputTokens = 200;
+
+ // Assert
+ Assert.Equal(100, runOptions.ChatOptions!.MaxOutputTokens);
+ Assert.Equal(200, clone.ChatOptions.MaxOutputTokens);
+ }
+
+ ///
+ /// Verify that modifying the cloned AdditionalProperties does not affect the original.
+ ///
+ [Fact]
+ public void CloneCreatesIndependentAdditionalPropertiesDictionary()
+ {
+ // Arrange
+ var runOptions = new ChatClientAgentRunOptions
+ {
+ AdditionalProperties = new AdditionalPropertiesDictionary
+ {
+ ["key1"] = "value1"
+ }
+ };
+
+ // Act
+ ChatClientAgentRunOptions clone = (ChatClientAgentRunOptions)runOptions.Clone();
+ clone.AdditionalProperties!["key2"] = "value2";
+
+ // Assert
+ Assert.True(clone.AdditionalProperties.ContainsKey("key2"));
+ Assert.False(runOptions.AdditionalProperties.ContainsKey("key2"));
+ }
+
+ #endregion
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
index e1ff5f8cbd..d2784803ab 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
@@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -479,45 +477,6 @@ public async Task RunAsyncInvokesAIContextProviderAndSucceedsWithEmptyAIContextA
#endregion
- #region RunAsync Structured Output Tests
-
- ///
- /// Verify the invocation of with specified type parameter is
- /// propagated to the underlying call and the expected structured output is returned.
- ///
- [Fact]
- public async Task RunAsyncWithTypeParameterInvokesChatClientMethodForStructuredOutputAsync()
- {
- // Arrange
- Animal expectedSO = new() { Id = 1, FullName = "Tigger", Species = Species.Tiger };
-
- Mock mockService = new();
- mockService.Setup(s => s
- .GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedSO, JsonContext2.Default.Animal)))
- {
- ResponseId = "test",
- });
-
- ChatClientAgent agent = new(mockService.Object, options: new());
-
- // Act
- AgentResponse agentResponse = await agent.RunAsync(messages: [new(ChatRole.User, "Hello")], serializerOptions: JsonContext2.Default.Options);
-
- // Assert
- Assert.Single(agentResponse.Messages);
-
- Assert.NotNull(agentResponse.Result);
- Assert.Equal(expectedSO.Id, agentResponse.Result.Id);
- Assert.Equal(expectedSO.FullName, agentResponse.Result.FullName);
- Assert.Equal(expectedSO.Species, agentResponse.Result.Species);
- }
-
- #endregion
-
#region Property Override Tests
///
@@ -1485,22 +1444,4 @@ private static async IAsyncEnumerable ToAsyncEnumerableAsync(IEnumerable mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => capturedResponseFormat = opts?.ResponseFormat)
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test"))
+ {
+ ResponseId = "test",
+ });
+
+ ChatResponseFormatJson responseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+
+ ChatClientAgent agent = new(mockService.Object, options: new ChatClientAgentOptions
+ {
+ ChatOptions = new ChatOptions()
+ {
+ ResponseFormat = responseFormat
+ }
+ });
+
+ // Act
+ await agent.RunAsync(messages: [new(ChatRole.User, "Hello")]);
+
+ // Assert
+ Assert.NotNull(capturedResponseFormat);
+ Assert.Same(responseFormat, capturedResponseFormat);
+ }
+
+ [Fact]
+ public async Task RunAsync_ResponseFormatProvidedAtAgentInvocation_IsPropagatedToChatClientAsync()
+ {
+ // Arrange
+ ChatResponseFormat? capturedResponseFormat = null;
+
+ Mock mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => capturedResponseFormat = opts?.ResponseFormat)
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test"))
+ {
+ ResponseId = "test",
+ });
+
+ ChatResponseFormatJson responseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+
+ ChatClientAgent agent = new(mockService.Object);
+
+ ChatClientAgentRunOptions runOptions = new()
+ {
+ ResponseFormat = responseFormat
+ };
+
+ // Act
+ await agent.RunAsync(messages: [new(ChatRole.User, "Hello")], options: runOptions);
+
+ // Assert
+ Assert.NotNull(capturedResponseFormat);
+ Assert.Same(responseFormat, capturedResponseFormat);
+ }
+
+ [Fact]
+ public async Task RunAsync_ResponseFormatProvidedAtAgentInvocation_OverridesOneProvidedAtAgentInitializationAsync()
+ {
+ // Arrange
+ ChatResponseFormat? capturedResponseFormat = null;
+
+ Mock mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => capturedResponseFormat = opts?.ResponseFormat)
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test"))
+ {
+ ResponseId = "test",
+ });
+
+ ChatResponseFormatJson initializationResponseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+ ChatResponseFormatJson invocationResponseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+
+ ChatClientAgent agent = new(mockService.Object, options: new ChatClientAgentOptions
+ {
+ ChatOptions = new ChatOptions()
+ {
+ ResponseFormat = initializationResponseFormat
+ },
+ });
+
+ ChatClientAgentRunOptions runOptions = new()
+ {
+ ResponseFormat = invocationResponseFormat
+ };
+
+ // Act
+ await agent.RunAsync(messages: [new(ChatRole.User, "Hello")], options: runOptions);
+
+ // Assert
+ Assert.NotNull(capturedResponseFormat);
+ Assert.Same(invocationResponseFormat, capturedResponseFormat);
+ Assert.NotSame(initializationResponseFormat, capturedResponseFormat);
+ }
+
+ [Fact]
+ public async Task RunAsync_ResponseFormatProvidedAtAgentRunOptions_OverridesOneProvidedViaChatOptionsAsync()
+ {
+ // Arrange
+ ChatResponseFormat? capturedResponseFormat = null;
+
+ Mock mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => capturedResponseFormat = opts?.ResponseFormat)
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "test"))
+ {
+ ResponseId = "test",
+ });
+
+ ChatResponseFormatJson chatOptionsResponseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+ ChatResponseFormatJson runOptionsResponseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+
+ ChatClientAgent agent = new(mockService.Object);
+
+ ChatClientAgentRunOptions runOptions = new()
+ {
+ ChatOptions = new ChatOptions
+ {
+ ResponseFormat = chatOptionsResponseFormat
+ },
+ ResponseFormat = runOptionsResponseFormat
+ };
+
+ // Act
+ await agent.RunAsync(messages: [new(ChatRole.User, "Hello")], options: runOptions);
+
+ // Assert
+ Assert.NotNull(capturedResponseFormat);
+ Assert.Same(runOptionsResponseFormat, capturedResponseFormat);
+ Assert.NotSame(chatOptionsResponseFormat, capturedResponseFormat);
+ }
+
+ [Fact]
+ public async Task RunAsync_StructuredOutputResponse_IsAvailableAsTextOnAgentResponseAsync()
+ {
+ // Arrange
+ Animal expectedAnimal = new() { FullName = "Wally the Walrus", Id = 1, Species = Species.Walrus };
+
+ Mock mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedAnimal, JsonContext4.Default.Animal)))
+ {
+ ResponseId = "test",
+ });
+
+ ChatResponseFormatJson responseFormat = ChatResponseFormat.ForJsonSchema(JsonContext4.Default.Options);
+
+ ChatClientAgent agent = new(mockService.Object, options: new ChatClientAgentOptions
+ {
+ ChatOptions = new ChatOptions()
+ {
+ ResponseFormat = responseFormat
+ },
+ });
+
+ // Act
+ AgentResponse agentResponse = await agent.RunAsync(messages: [new(ChatRole.User, "Hello")]);
+
+ // Assert
+ Assert.NotNull(agentResponse?.Text);
+
+ Animal? deserialised = JsonSerializer.Deserialize(agentResponse.Text, JsonContext4.Default.Animal);
+ Assert.NotNull(deserialised);
+ Assert.Equal(expectedAnimal.Id, deserialised.Id);
+ Assert.Equal(expectedAnimal.FullName, deserialised.FullName);
+ Assert.Equal(expectedAnimal.Species, deserialised.Species);
+ }
+
+ [JsonSerializable(typeof(Animal))]
+ private sealed partial class JsonContext4 : JsonSerializerContext;
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_StructuredOutput_WithRunAsyncTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_StructuredOutput_WithRunAsyncTests.cs
new file mode 100644
index 0000000000..57e1bbf371
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_StructuredOutput_WithRunAsyncTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+public partial class ChatClientAgent_StructuredOutput_WithRunAsyncTests
+{
+ [Fact]
+ public async Task RunAsync_WithGenericType_SetsJsonSchemaResponseFormatAndDeserializesResultAsync()
+ {
+ // Arrange
+ ChatResponseFormat? capturedResponseFormat = null;
+ ChatResponseFormatJson expectedResponseFormat = ChatResponseFormat.ForJsonSchema(JsonContext3.Default.Options);
+ Animal expectedSO = new() { Id = 1, FullName = "Tigger", Species = Species.Tiger };
+
+ Mock mockService = new();
+ mockService.Setup(s => s
+ .GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) => capturedResponseFormat = opts?.ResponseFormat)
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, JsonSerializer.Serialize(expectedSO, JsonContext3.Default.Animal)))
+ {
+ ResponseId = "test",
+ });
+
+ ChatClientAgent agent = new(mockService.Object);
+
+ // Act
+ AgentResponse agentResponse = await agent.RunAsync(
+ messages: [new(ChatRole.User, "Hello")],
+ serializerOptions: JsonContext3.Default.Options);
+
+ // Assert
+ Assert.NotNull(capturedResponseFormat);
+ Assert.Equal(expectedResponseFormat.Schema?.GetRawText(), ((ChatResponseFormatJson)capturedResponseFormat).Schema?.GetRawText());
+
+ Animal animal = agentResponse.Result;
+ Assert.NotNull(animal);
+ Assert.Equal(expectedSO.Id, animal.Id);
+ Assert.Equal(expectedSO.FullName, animal.FullName);
+ Assert.Equal(expectedSO.Species, animal.Species);
+ }
+
+ [JsonSourceGenerationOptions(UseStringEnumConverter = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+ [JsonSerializable(typeof(Animal))]
+ private sealed partial class JsonContext3 : JsonSerializerContext;
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Animal.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Animal.cs
new file mode 100644
index 0000000000..331f336b8e
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Animal.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+internal sealed class Animal
+{
+ public int Id { get; set; }
+ public string? FullName { get; set; }
+ public Species Species { get; set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Species.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Species.cs
new file mode 100644
index 0000000000..14c493be72
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Models/Species.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.UnitTests;
+
+internal enum Species
+{
+ Bear,
+ Tiger,
+ Walrus,
+}
diff --git a/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantStructuredOutputRunTests.cs b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantStructuredOutputRunTests.cs
new file mode 100644
index 0000000000..caa42ecc8d
--- /dev/null
+++ b/dotnet/tests/OpenAIAssistant.IntegrationTests/OpenAIAssistantStructuredOutputRunTests.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace OpenAIAssistant.IntegrationTests;
+
+public class OpenAIAssistantStructuredOutputRunTests() : StructuredOutputRunTests(() => new())
+{
+}
diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionStructuredOutputRunTests.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionStructuredOutputRunTests.cs
new file mode 100644
index 0000000000..b7c66f1f26
--- /dev/null
+++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionStructuredOutputRunTests.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace OpenAIChatCompletion.IntegrationTests;
+
+public class OpenAIChatCompletionStructuredOutputRunTests() : StructuredOutputRunTests(() => new(useReasoningChatModel: false))
+{
+}
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseStructuredOutputRunTests.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseStructuredOutputRunTests.cs
new file mode 100644
index 0000000000..497c16eb5a
--- /dev/null
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseStructuredOutputRunTests.cs
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using AgentConformance.IntegrationTests;
+
+namespace ResponseResult.IntegrationTests;
+
+public class OpenAIResponseStructuredOutputRunTests() : StructuredOutputRunTests(() => new(store: false))
+{
+}