// 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;
namespace Microsoft.Agents.AI.DurableTask;
///
/// A durable AIAgent implementation that uses entity methods to interact with agent entities.
///
public sealed class DurableAIAgent : AIAgent
{
private readonly TaskOrchestrationContext _context;
private readonly string _agentName;
///
/// Initializes a new instance of the class.
///
/// The orchestration context.
/// The name of the agent.
internal DurableAIAgent(TaskOrchestrationContext context, string agentName)
{
this._context = context;
this._agentName = agentName;
}
///
/// Creates a new agent thread for this agent using a random session ID.
///
/// A new agent thread.
public override AgentThread GetNewThread()
{
AgentSessionId sessionId = this._context.NewAgentSessionId(this._agentName);
return new DurableAgentThread(sessionId);
}
///
/// Deserializes an agent thread from JSON.
///
/// The serialized thread data.
/// Optional JSON serializer options.
/// The deserialized agent thread.
public override AgentThread DeserializeThread(
JsonElement serializedThread,
JsonSerializerOptions? jsonSerializerOptions = null)
{
return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
}
///
/// Runs the agent with messages and returns the response.
///
/// The messages to send to the agent.
/// The agent thread to use.
/// Optional run options.
/// The cancellation token.
/// The response from the agent.
/// Thrown when the agent has not been registered.
/// Thrown when the provided thread is not valid for a durable agent.
/// Thrown when cancellation is requested (cancellation is not supported for durable agents).
public override async Task RunAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
if (cancellationToken != default && cancellationToken.CanBeCanceled)
{
throw new NotSupportedException("Cancellation is not supported for durable agents.");
}
thread ??= this.GetNewThread();
if (thread is not DurableAgentThread durableThread)
{
throw new ArgumentException(
"The provided thread is not valid for a durable agent. " +
"Create a new thread using GetNewThread or provide a thread previously created by this agent.",
paramName: nameof(thread));
}
IList? enableToolNames = null;
bool enableToolCalls = true;
ChatResponseFormat? responseFormat = null;
if (options is DurableAgentRunOptions durableOptions)
{
enableToolCalls = durableOptions.EnableToolCalls;
enableToolNames = durableOptions.EnableToolNames;
responseFormat = durableOptions.ResponseFormat;
}
else if (options is ChatClientAgentRunOptions chatClientOptions && chatClientOptions.ChatOptions?.Tools != null)
{
// Honor the response format from the chat client options if specified
responseFormat = chatClientOptions.ChatOptions?.ResponseFormat;
}
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames)
{
OrchestrationId = this._context.InstanceId
};
try
{
return await this._context.Entities.CallEntityAsync(
durableThread.SessionId,
nameof(AgentEntity.Run),
request);
}
catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
{
throw new AgentNotRegisteredException(this._agentName, e);
}
}
///
/// Runs the agent with messages and returns a simulated streaming response.
///
///
/// Streaming is not supported for durable agents, so this method just returns the full response
/// as a single update.
///
/// The messages to send to the agent.
/// The agent thread to use.
/// Optional run options.
/// The cancellation token.
/// A streaming response enumerable.
public override async IAsyncEnumerable RunStreamingAsync(
IEnumerable messages,
AgentThread? thread = null,
AgentRunOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Streaming is not supported for durable agents, so we just return the full response
// as a single update.
AgentRunResponse response = await this.RunAsync(messages, thread, options, cancellationToken);
foreach (AgentRunResponseUpdate update in response.ToAgentRunResponseUpdates())
{
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 thread 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,
AgentThread? thread = null,
JsonSerializerOptions? serializerOptions = null,
AgentRunOptions? options = null,
CancellationToken cancellationToken = default)
{
return await this.RunAsync(
messages: [new ChatMessage(ChatRole.User, message) { CreatedAt = DateTimeOffset.UtcNow }],
thread,
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 thread 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,
AgentThread? thread = 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();
AgentRunResponse response = await this.RunAsync(messages, thread, 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 DurableAIAgentRunResponse(response, result);
}
private sealed class DurableAIAgentRunResponse(AgentRunResponse response, T result)
: AgentRunResponse(response.AsChatResponse())
{
public override T Result { get; } = result;
}
}