// 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; } }