diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 69d3e03d31..65a0e39f13 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -93,6 +93,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 86eee2fbbe..aee9bdb1f6 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -274,8 +274,10 @@
+
+
@@ -299,7 +301,9 @@
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs
new file mode 100644
index 0000000000..4f5619ac76
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Extension methods for the class.
+///
+public static class AIAgentExtensions
+{
+ ///
+ /// Converts an AIAgent to a durable agent proxy.
+ ///
+ /// The agent to convert.
+ /// The service provider.
+ /// The durable agent proxy.
+ ///
+ /// Thrown when the agent is a DurableAIAgent instance or if the agent has no name.
+ ///
+ ///
+ /// Thrown if does not contain an .
+ ///
+ public static AIAgent AsDurableAgentProxy(this AIAgent agent, IServiceProvider services)
+ {
+ // Don't allow this method to be used on DurableAIAgent instances.
+ if (agent is DurableAIAgent)
+ {
+ throw new ArgumentException(
+ $"{nameof(DurableAIAgent)} instances cannot be converted to a durable agent proxy.",
+ nameof(agent));
+ }
+
+ string agentName = agent.Name ?? throw new ArgumentException("Agent must have a name.", nameof(agent));
+ IDurableAgentClient agentClient = services.GetRequiredService();
+ return new DurableAIAgentProxy(agentName, agentClient);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs
new file mode 100644
index 0000000000..8c5c265f2d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentEntity.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Entities;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+internal class AgentEntity(IServiceProvider services, CancellationToken cancellationToken = default) : TaskEntity
+{
+ private readonly IServiceProvider _services = services;
+ private readonly DurableTaskClient _client = services.GetRequiredService();
+ private readonly ILoggerFactory _loggerFactory = services.GetRequiredService();
+ private readonly IAgentResponseHandler? _messageHandler = services.GetService();
+ private readonly CancellationToken _cancellationToken = cancellationToken != default
+ ? cancellationToken
+ : services.GetService()?.ApplicationStopping ?? CancellationToken.None;
+
+ public async Task RunAgentAsync(RunRequest request)
+ {
+ AgentSessionId sessionId = this.Context.Id;
+ IReadOnlyDictionary> agents =
+ this._services.GetRequiredService>>();
+ if (!agents.TryGetValue(sessionId.Name, out Func? agentFactory))
+ {
+ throw new InvalidOperationException($"Agent '{sessionId.Name}' not found");
+ }
+
+ AIAgent agent = agentFactory(this._services);
+ EntityAgentWrapper agentWrapper = new(agent, this.Context, request, this._services);
+
+ // Logger category is Microsoft.DurableTask.Agents.{agentName}.{sessionId}
+ ILogger logger = this._loggerFactory.CreateLogger($"Microsoft.DurableTask.Agents.{agent.Name}.{sessionId.Key}");
+
+ if (request.Messages.Count == 0)
+ {
+ logger.LogInformation("Ignoring empty request");
+ }
+
+ // TODO: Get state from optional state store
+ DurableAgentState state = this.State;
+
+ foreach (ChatMessage msg in request.Messages)
+ {
+ logger.LogAgentRequest(sessionId, msg.Role, msg.Text);
+ state.AddChatMessage(msg);
+ }
+
+ // Set the current agent context for the duration of the agent run. This will be exposed
+ // to any tools that are invoked by the agent.
+ DurableAgentContext agentContext = new(
+ entityContext: this.Context,
+ client: this._client,
+ lifetime: this._services.GetRequiredService(),
+ services: this._services);
+ DurableAgentContext.SetCurrent(agentContext);
+
+ try
+ {
+ // Start the agent response stream
+ IAsyncEnumerable responseStream = agentWrapper.RunStreamingAsync(
+ state.EnumerateChatMessages(),
+ agentWrapper.GetNewThread(),
+ options: null,
+ this._cancellationToken);
+
+ AgentRunResponse response;
+ if (this._messageHandler is null)
+ {
+ // If no message handler is provided, we can just get the full response at once.
+ // This is expected to be the common case for non-interactive agents.
+ response = await responseStream.ToAgentRunResponseAsync(this._cancellationToken);
+ }
+ else
+ {
+ List responseUpdates = [];
+
+ // To support interactive chat agents, we need to stream the responses to an IAgentMessageHandler.
+ // The user-provided message handler can be implemented to send the responses to the user.
+ // We assume that only non-empty text updates are useful for the user.
+ async IAsyncEnumerable StreamResultsAsync()
+ {
+ await foreach (AgentRunResponseUpdate update in responseStream)
+ {
+ // We need the full response further down, so we piece it together as we go.
+ responseUpdates.Add(update);
+
+ // Yield the update to the message handler.
+ yield return update;
+ }
+ }
+
+ await this._messageHandler.OnStreamingResponseUpdateAsync(StreamResultsAsync(), this._cancellationToken);
+ response = responseUpdates.ToAgentRunResponse();
+ }
+
+ // Persist the agent response to the entity state for client polling
+ state.AddAgentResponse(response, request.CorrelationId);
+
+ string responseText = response.Text;
+
+ if (!string.IsNullOrEmpty(responseText))
+ {
+ logger.LogAgentResponse(
+ sessionId,
+ response.Messages.FirstOrDefault()?.Role ?? ChatRole.Assistant,
+ responseText,
+ response.Usage?.InputTokenCount,
+ response.Usage?.OutputTokenCount,
+ response.Usage?.TotalTokenCount);
+ }
+
+ this.UpdateEntityState(state);
+
+ return response;
+ }
+ finally
+ {
+ // Clear the current agent context
+ DurableAgentContext.ClearCurrent();
+ }
+ }
+
+ private void UpdateEntityState(DurableAgentState state)
+ {
+ // This method is called to update the state of the entity.
+ // It can be used to persist the state to a durable store if needed.
+ this.State = state;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentRunHandle.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentRunHandle.cs
new file mode 100644
index 0000000000..6df24d3832
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentRunHandle.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Client.Entities;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents a handle for a running agent request that can be used to retrieve the response.
+///
+internal sealed class AgentRunHandle
+{
+ private readonly DurableTaskClient _client;
+
+ internal AgentRunHandle(DurableTaskClient client, AgentSessionId sessionId, string correlationId)
+ {
+ this._client = client;
+ this.SessionId = sessionId;
+ this.CorrelationId = correlationId;
+ }
+
+ ///
+ /// Gets the correlation ID for this request.
+ ///
+ public string CorrelationId { get; }
+
+ ///
+ /// Gets the session ID for this request.
+ ///
+ public AgentSessionId SessionId { get; }
+
+ ///
+ /// Reads the agent response for this request by polling the entity state until the response is found.
+ /// Uses an exponential backoff polling strategy with a maximum interval of 1 second.
+ ///
+ /// The cancellation token.
+ /// The agent response corresponding to this request.
+ /// Thrown when the response is not found after polling.
+ public async Task ReadAgentResponseAsync(CancellationToken cancellationToken = default)
+ {
+ TimeSpan pollInterval = TimeSpan.FromMilliseconds(50); // Start with 50ms
+ TimeSpan maxPollInterval = TimeSpan.FromSeconds(3); // Maximum 3 seconds
+
+ while (true)
+ {
+ // Poll the entity state for responses
+ EntityMetadata? entityResponse = await this._client.Entities.GetEntityAsync(
+ this.SessionId,
+ cancellation: cancellationToken);
+ DurableAgentState? state = entityResponse?.State;
+
+ // Look for an agent response with matching CorrelationId
+ if (state is not null && state.TryGetAgentResponse(this.CorrelationId, out AgentRunResponse? response))
+ {
+ return response;
+ }
+
+ // Wait before polling again with exponential backoff
+ await Task.Delay(pollInterval, cancellationToken);
+
+ // Double the poll interval, but cap it at the maximum
+ pollInterval = TimeSpan.FromMilliseconds(Math.Min(pollInterval.TotalMilliseconds * 2, maxPollInterval.TotalMilliseconds));
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentSessionId.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentSessionId.cs
new file mode 100644
index 0000000000..f183ec84dc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentSessionId.cs
@@ -0,0 +1,165 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.DurableTask.Entities;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents an agent session ID, which is used to identify a long-running agent session.
+///
+[JsonConverter(typeof(AgentSessionIdJsonConverter))]
+public readonly struct AgentSessionId : IEquatable
+{
+ private const string EntityNamePrefix = "dafx-";
+ private readonly EntityInstanceId _entityId;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The name of the agent that owns the session (case-insensitive).
+ /// The unique key of the agent session (case-sensitive).
+ public AgentSessionId(string name, string key)
+ {
+ this.Name = name;
+ this._entityId = new EntityInstanceId(ToEntityName(name), key);
+ }
+
+ ///
+ /// Converts an agent name to its underlying entity name representation.
+ ///
+ /// The agent name.
+ /// The entity name used by Durable Task for this agent.
+ public static string ToEntityName(string name) => $"{EntityNamePrefix}{name}";
+
+ ///
+ /// Gets the name of the agent that owns the session. Names are case-insensitive.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the unique key of the agent session. Keys are case-sensitive and are used to identify the session.
+ ///
+ public string Key => this._entityId.Key;
+
+ internal EntityInstanceId ToEntityId() => this._entityId;
+
+ ///
+ /// Creates a new with the specified name and a randomly generated key.
+ ///
+ /// The name of the agent that owns the session.
+ /// A new with the specified name and a random key.
+ public static AgentSessionId WithRandomKey(string name) =>
+ new(name, Guid.NewGuid().ToString("N"));
+
+ ///
+ /// Determines whether two instances are equal.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// true if the two instances are equal; otherwise, false.
+ public static bool operator ==(AgentSessionId left, AgentSessionId right) =>
+ left._entityId == right._entityId;
+
+ ///
+ /// Determines whether two instances are not equal.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// true if the two instances are not equal; otherwise, false.
+ public static bool operator !=(AgentSessionId left, AgentSessionId right) =>
+ left._entityId != right._entityId;
+
+ ///
+ /// Determines whether the specified is equal to the current .
+ ///
+ /// The to compare with the current .
+ /// true if the specified is equal to the current ; otherwise, false.
+ public bool Equals(AgentSessionId other) => this == other;
+
+ ///
+ /// Determines whether the specified object is equal to the current .
+ ///
+ /// The object to compare with the current .
+ /// true if the specified object is equal to the current ; otherwise, false.
+ public override bool Equals(object? obj) => obj is AgentSessionId other && this == other;
+
+ ///
+ /// Returns the hash code for this .
+ ///
+ /// A hash code for the current .
+ public override int GetHashCode() => this._entityId.GetHashCode();
+
+ ///
+ /// Returns a string representation of this in the form of @name@key.
+ ///
+ /// A string representation of the current .
+ public override string ToString() => this._entityId.ToString();
+
+ ///
+ /// Converts the string representation of an agent session ID to its equivalent.
+ /// The input string must be in the form of @name@key.
+ ///
+ /// A string containing an agent session ID to convert.
+ /// A equivalent to the agent session ID contained in .
+ /// Thrown when is not a valid agent session ID format.
+ public static AgentSessionId Parse(string sessionIdString)
+ {
+ EntityInstanceId entityId = EntityInstanceId.FromString(sessionIdString);
+ if (!entityId.Name.StartsWith(EntityNamePrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"'{sessionIdString}' is not a valid agent session ID.", nameof(sessionIdString));
+ }
+
+ return new AgentSessionId(entityId.Name[EntityNamePrefix.Length..], entityId.Key);
+ }
+
+ ///
+ /// Implicitly converts an to an .
+ /// This conversion is useful for entity API interoperability.
+ ///
+ /// The to convert.
+ /// The equivalent .
+ public static implicit operator EntityInstanceId(AgentSessionId agentSessionId) => agentSessionId.ToEntityId();
+
+ ///
+ /// Implicitly converts an to an .
+ ///
+ /// The to convert.
+ /// The equivalent .
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Implicit conversion must validate format.")]
+ public static implicit operator AgentSessionId(EntityInstanceId entityId)
+ {
+ if (!entityId.Name.StartsWith(EntityNamePrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"'{entityId}' is not a valid agent session ID.", nameof(entityId));
+ }
+ return new AgentSessionId(entityId.Name[EntityNamePrefix.Length..], entityId.Key);
+ }
+
+ ///
+ /// Custom JSON converter for to ensure proper serialization and deserialization.
+ ///
+ public sealed class AgentSessionIdJsonConverter : JsonConverter
+ {
+ ///
+ public override AgentSessionId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ {
+ throw new JsonException("Expected string value");
+ }
+
+ string value = reader.GetString() ?? string.Empty;
+
+ return Parse(value);
+ }
+
+ ///
+ public override void Write(Utf8JsonWriter writer, AgentSessionId value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToString());
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
new file mode 100644
index 0000000000..e908deac89
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Release History
+
+## v1.0.0-preview.* (Unreleased)
+
+- Initial public release.
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DefaultDurableAgentClient.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DefaultDurableAgentClient.cs
new file mode 100644
index 0000000000..e19b7ae742
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DefaultDurableAgentClient.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+internal class DefaultDurableAgentClient(DurableTaskClient client, ILoggerFactory? loggerFactory = null) : IDurableAgentClient
+{
+ private readonly DurableTaskClient _client = client;
+ private readonly ILogger? _logger = loggerFactory?.CreateLogger();
+
+ public async Task RunAgentAsync(
+ AgentSessionId sessionId,
+ RunRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ // The correlation ID is used to fetch the correct response later.
+ request.CorrelationId = Guid.NewGuid().ToString("N");
+
+ // TODO: Use source generators to log the request
+ this._logger?.LogInformation("Signalling agent with session ID '{SessionId}'", sessionId);
+
+ await this._client.Entities.SignalEntityAsync(
+ sessionId,
+ nameof(AgentEntity.RunAgentAsync),
+ request,
+ cancellation: cancellationToken);
+
+ return new AgentRunHandle(this._client, sessionId, request.CorrelationId);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
new file mode 100644
index 0000000000..7134fcd16f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
@@ -0,0 +1,228 @@
+// 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.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.
+ 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);
+ return await this._context.Entities.CallEntityAsync(durableThread.SessionId, nameof(AgentEntity.RunAgentAsync), request);
+ }
+
+ ///
+ /// 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([new ChatMessage(ChatRole.User, message)], 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;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
new file mode 100644
index 0000000000..58f9598a7e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgentProxy.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+internal class DurableAIAgentProxy(string name, IDurableAgentClient agentClient) : AIAgent
+{
+ private readonly IDurableAgentClient _agentClient = agentClient;
+
+ public override string? Name { get; } = name;
+
+ public override AgentThread DeserializeThread(
+ JsonElement serializedThread,
+ JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ return DurableAgentThread.Deserialize(serializedThread, jsonSerializerOptions);
+ }
+
+ public override AgentThread GetNewThread()
+ {
+ return new DurableAgentThread(AgentSessionId.WithRandomKey(this.Name!));
+ }
+
+ public override async Task RunAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ 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;
+ bool isFireAndForget = false;
+
+ if (options is DurableAgentRunOptions durableOptions)
+ {
+ enableToolCalls = durableOptions.EnableToolCalls;
+ enableToolNames = durableOptions.EnableToolNames;
+ responseFormat = durableOptions.ResponseFormat;
+ isFireAndForget = durableOptions.IsFireAndForget;
+ }
+ else if (options is ChatClientAgentRunOptions chatClientOptions)
+ {
+ // Honor the response format from the chat client options if specified
+ responseFormat = chatClientOptions.ChatOptions?.ResponseFormat;
+ }
+
+ RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
+ AgentSessionId sessionId = durableThread.SessionId;
+
+ AgentRunHandle agentRunHandle = await this._agentClient.RunAgentAsync(sessionId, request, cancellationToken);
+
+ if (isFireAndForget)
+ {
+ // If the request is fire and forget, return an empty response.
+ return new AgentRunResponse();
+ }
+
+ return await agentRunHandle.ReadAgentResponseAsync(cancellationToken);
+ }
+
+ public override IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotSupportedException("Streaming is not supported for durable agents.");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentContext.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentContext.cs
new file mode 100644
index 0000000000..94a6c00424
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentContext.cs
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Entities;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// A context for durable agents that provides access to orchestration capabilities.
+/// This class provides thread-static access to the current agent context.
+///
+public class DurableAgentContext
+{
+ private static readonly AsyncLocal s_currentContext = new();
+ private readonly IServiceProvider _services;
+ private readonly CancellationToken _cancellationToken;
+
+ internal DurableAgentContext(
+ TaskEntityContext entityContext,
+ DurableTaskClient client,
+ IHostApplicationLifetime lifetime,
+ IServiceProvider services)
+ {
+ this.EntityContext = entityContext;
+ this.CurrentThread = new DurableAgentThread(entityContext.Id);
+ this.Client = client;
+ this._services = services;
+ this._cancellationToken = lifetime.ApplicationStopping;
+ }
+
+ ///
+ /// Gets the current durable agent context instance.
+ ///
+ /// Thrown when no agent context is available.
+ public static DurableAgentContext Current => s_currentContext.Value ??
+ throw new InvalidOperationException("No agent context found!");
+
+ ///
+ /// Gets the entity context for this agent.
+ ///
+ public TaskEntityContext EntityContext { get; }
+
+ ///
+ /// Gets the durable task client for this agent.
+ ///
+ public DurableTaskClient Client { get; }
+
+ ///
+ /// Gets the current agent thread.
+ ///
+ public DurableAgentThread CurrentThread { get; }
+
+ ///
+ /// Sets the current durable agent context instance.
+ /// This is called internally by the agent entity during execution.
+ ///
+ /// The context instance to set.
+ internal static void SetCurrent(DurableAgentContext context)
+ {
+ if (s_currentContext.Value is not null)
+ {
+ throw new InvalidOperationException("A DurableAgentContext has already been set for this AsyncLocal context.");
+ }
+
+ s_currentContext.Value = context;
+ }
+
+ ///
+ /// Clears the current durable agent context instance.
+ /// This is called internally by the agent entity after execution.
+ ///
+ internal static void ClearCurrent()
+ {
+ s_currentContext.Value = null;
+ }
+
+ ///
+ /// Schedules a new orchestration instance.
+ ///
+ ///
+ /// When run in the context of a durable agent tool, the actual scheduling of the orchestration
+ /// occurs after the completion of the tool call. This allows the durable scheduling of the orchestration
+ /// and the agent state update to be committed atomically in a single transaction.
+ ///
+ /// The name of the orchestration to schedule.
+ /// The input to the orchestration.
+ /// The options for the orchestration.
+ /// The instance ID of the scheduled orchestration.
+ public string ScheduleNewOrchestration(
+ TaskName name,
+ object? input = null,
+ StartOrchestrationOptions? options = null)
+ {
+ return this.EntityContext.ScheduleNewOrchestration(name, input, options);
+ }
+
+ ///
+ /// Gets the status of an orchestration instance.
+ ///
+ /// The instance ID of the orchestration to get the status of.
+ /// Whether to include detailed information about the orchestration.
+ /// The status of the orchestration.
+ public Task GetOrchestrationStatusAsync(string instanceId, bool includeDetails = false)
+ {
+ return this.Client.GetInstanceAsync(instanceId, includeDetails, this._cancellationToken);
+ }
+
+ ///
+ /// Raises an event on an orchestration instance.
+ ///
+ /// The instance ID of the orchestration to raise the event on.
+ /// The name of the event to raise.
+ /// The data to send with the event.
+#pragma warning disable CA1030 // Use events where appropriate
+ public Task RaiseOrchestrationEventAsync(string instanceId, string eventName, object? eventData = null)
+#pragma warning restore CA1030 // Use events where appropriate
+ {
+ return this.Client.RaiseEventAsync(instanceId, eventName, eventData, this._cancellationToken);
+ }
+
+ ///
+ /// Asks the for an object of the specified type, .
+ ///
+ /// The type of the object being requested.
+ /// An optional key to identify the service instance.
+ /// The service instance, or if the service is not found.
+ ///
+ /// Thrown when is not and the service provider does not support keyed services.
+ ///
+ public TService? GetService(object? serviceKey = null)
+ {
+ return this.GetService(typeof(TService), serviceKey) is TService service ? service : default;
+ }
+
+ ///
+ /// Asks the for an object of the specified type, .
+ ///
+ /// The type of the object being requested.
+ /// An optional key to identify the service instance.
+ /// The service instance, or if the service is not found.
+ ///
+ /// Thrown when is not and the service provider does not support keyed services.
+ ///
+ public object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ if (serviceKey is not null)
+ {
+ if (this._services is not IKeyedServiceProvider keyedServiceProvider)
+ {
+ throw new InvalidOperationException("The service provider does not support keyed services.");
+ }
+
+ return keyedServiceProvider.GetKeyedService(serviceType, serviceKey);
+ }
+
+ return this._services.GetService(serviceType);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentJsonUtilities.cs
new file mode 100644
index 0000000000..dbe5cd76af
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentJsonUtilities.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+/// Provides JSON serialization utilities and source-generated contracts for Durable Agent types.
+///
+///
+/// This mirrors the pattern used by other libraries (e.g. WorkflowsJsonUtilities) to enable Native AOT and trimming
+/// friendly serialization without relying on runtime reflection. It establishes a singleton
+/// instance that is preconfigured with:
+///
+///
+/// - baseline defaults.
+/// - for default null-value suppression.
+/// - to tolerate numbers encoded as strings.
+/// - Chained type info resolvers from shared agent abstractions to cover cross-package types (e.g. , ).
+///
+///
+/// Keep the list of [JsonSerializable] types in sync with the Durable Agent data model anytime new state or request/response
+/// containers are introduced that must round-trip via JSON.
+///
+///
+internal static partial class DurableAgentJsonUtilities
+{
+ ///
+ /// Gets the singleton used for Durable Agent serialization.
+ ///
+ public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
+
+ ///
+ /// Serializes a sequence of chat messages using the durable agent default options.
+ ///
+ /// The messages to serialize.
+ /// A representing the serialized messages.
+ public static JsonElement Serialize(this IEnumerable messages) =>
+ JsonSerializer.SerializeToElement(messages, DefaultOptions.GetTypeInfo(typeof(IEnumerable)));
+
+ ///
+ /// Deserializes chat messages from a using durable agent options.
+ ///
+ /// The JSON element containing the messages.
+ /// The deserialized list of chat messages.
+ public static List DeserializeMessages(this JsonElement element) =>
+ (List?)element.Deserialize(DefaultOptions.GetTypeInfo(typeof(List))) ?? [];
+
+ ///
+ /// Creates the configured instance for durable agents.
+ ///
+ /// The configured options.
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
+ [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")]
+ private static JsonSerializerOptions CreateDefaultOptions()
+ {
+ // Base configuration from the source-generated context below.
+ JsonSerializerOptions options = new(JsonContext.Default.Options);
+
+ // Chain in shared abstractions resolver (Microsoft.Extensions.AI + Agent abstractions) so dependent types are covered.
+ options.TypeInfoResolverChain.Add(AgentAbstractionsJsonUtilities.DefaultOptions.TypeInfoResolver!);
+
+ options.MakeReadOnly();
+ return options;
+ }
+
+ // Keep in sync with CreateDefaultOptions above.
+ [JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString)]
+
+ // Durable Agent State Types
+ [JsonSerializable(typeof(DurableAgentState))]
+ [JsonSerializable(typeof(AgentStateEntry))]
+ [JsonSerializable(typeof(DurableAgentThread))]
+
+ // Request Types
+ [JsonSerializable(typeof(RunRequest))]
+
+ // Primitive / Supporting Types
+ [JsonSerializable(typeof(ChatMessage))]
+ [JsonSerializable(typeof(JsonElement))]
+
+ [ExcludeFromCodeCoverage]
+ internal sealed partial class JsonContext : JsonSerializerContext;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs
new file mode 100644
index 0000000000..0f1984ad62
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentRunOptions.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Options for running a durable agent.
+///
+public sealed class DurableAgentRunOptions : AgentRunOptions
+{
+ ///
+ /// Gets or sets whether to enable tool calls for this request.
+ ///
+ public bool EnableToolCalls { get; set; } = true;
+
+ ///
+ /// Gets or sets the collection of tool names to enable. If not specified, all tools are enabled.
+ ///
+ 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.
+ ///
+ ///
+ /// If is true, the agent run request will be sent and the method will return immediately.
+ /// The caller will not wait for the agent to complete the run and will not receive a response. This setting is useful for
+ /// long-running tasks where the caller does not need to wait for the agent to complete the run.
+ ///
+ public bool IsFireAndForget { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentState.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentState.cs
new file mode 100644
index 0000000000..80a43d403f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentState.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents the state of a durable agent, including its conversation history.
+///
+public class DurableAgentState
+{
+ ///
+ /// Gets the ordered list of state entries representing the complete conversation history.
+ /// This includes both user messages and agent responses in chronological order.
+ ///
+ public List ConversationHistory { get; set; } = [];
+
+ ///
+ /// Gets all chat messages from the conversation history.
+ ///
+ /// A collection of chat messages in chronological order.
+ public IEnumerable EnumerateChatMessages()
+ {
+ foreach (AgentStateEntry entry in this.ConversationHistory)
+ {
+ if (entry.Type == AgentStateEntry.EntryType.ChatMessage)
+ {
+ yield return entry.ChatMessage!;
+ }
+ else if (entry.Type == AgentStateEntry.EntryType.AgentResponse)
+ {
+ foreach (ChatMessage message in entry.AgentResponse!.Messages)
+ {
+ yield return message;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets an agent response from the conversation history.
+ ///
+ /// The correlation ID of the agent response to get.
+ /// The agent response if found, null otherwise.
+ /// True if the agent response was found, false otherwise.
+ public bool TryGetAgentResponse(string correlationId, [NotNullWhen(true)] out AgentRunResponse? response)
+ {
+ foreach (AgentStateEntry entry in this.ConversationHistory.Where(
+ entry => entry.Type == AgentStateEntry.EntryType.AgentResponse &&
+ entry.CorrelationId == correlationId))
+ {
+ response = entry.AgentResponse!;
+ return true;
+ }
+
+ response = null;
+ return false;
+ }
+
+ ///
+ /// Adds a chat message to the state.
+ ///
+ /// The chat message to add.
+ public void AddChatMessage(ChatMessage message)
+ {
+ this.ConversationHistory.Add(AgentStateEntry.CreateChatMessage(message));
+ }
+
+ ///
+ /// Adds an agent response to the state.
+ ///
+ /// The agent response to add.
+ /// The correlation ID for the agent response.
+ public void AddAgentResponse(AgentRunResponse response, string? correlationId)
+ {
+ this.ConversationHistory.Add(AgentStateEntry.CreateAgentResponse(response, correlationId));
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentStateEntry.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentStateEntry.cs
new file mode 100644
index 0000000000..f0b6d8c3da
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentStateEntry.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents a single entry in the durable agent state, which can either be a chat message or agent response.
+/// This maintains chronological order of all interactions while preserving strong typing.
+///
+public sealed class AgentStateEntry
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The chat message, if this entry represents a chat message.
+ /// The agent response, if this entry represents an agent response.
+ /// The correlation ID associated with the agent response, if any.
+ [JsonConstructor]
+ public AgentStateEntry(ChatMessage? chatMessage, AgentRunResponse? agentResponse, string? correlationId)
+ {
+ this.ChatMessage = chatMessage;
+ this.AgentResponse = agentResponse;
+ this.Type = agentResponse is null ? EntryType.ChatMessage : EntryType.AgentResponse;
+ this.CorrelationId = correlationId;
+ }
+
+ ///
+ /// Gets the type of this state entry.
+ ///
+ public EntryType Type { get; }
+
+ ///
+ /// Gets the correlation ID for this entry.
+ ///
+ public string? CorrelationId { get; }
+
+ ///
+ /// Gets the chat message, if this entry is a chat message.
+ ///
+ public ChatMessage? ChatMessage { get; }
+
+ ///
+ /// Gets the agent response, if this entry is an agent response.
+ ///
+ public AgentRunResponse? AgentResponse { get; }
+
+ ///
+ /// Creates a chat message entry.
+ ///
+ /// The chat message.
+ /// A new chat message entry.
+ public static AgentStateEntry CreateChatMessage(ChatMessage message) => new(message, null, null);
+
+ ///
+ /// Creates an agent response entry.
+ ///
+ /// The agent response.
+ /// The correlation ID for the agent response.
+ /// A new agent response entry.
+ public static AgentStateEntry CreateAgentResponse(AgentRunResponse response, string? correlationId) => new(null, response, correlationId);
+
+ ///
+ /// Defines the types of entries that can be stored in the durable agent state.
+ ///
+ public enum EntryType
+ {
+ ///
+ /// A user chat message.
+ ///
+ ChatMessage,
+
+ ///
+ /// An agent response.
+ ///
+ AgentResponse
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentThread.cs
new file mode 100644
index 0000000000..8dd2b73609
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentThread.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// An agent thread implementation for durable agents.
+///
+[DebuggerDisplay("{SessionId}")]
+public sealed class DurableAgentThread : AgentThread
+{
+ [JsonConstructor]
+ internal DurableAgentThread(AgentSessionId sessionId)
+ {
+ this.SessionId = sessionId;
+ }
+
+ ///
+ /// Gets the agent session ID.
+ ///
+ [JsonInclude]
+ [JsonPropertyName("sessionId")]
+ internal AgentSessionId SessionId { get; }
+
+ ///
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ return JsonSerializer.SerializeToElement(
+ this,
+ DurableAgentJsonUtilities.DefaultOptions.GetTypeInfo(typeof(DurableAgentThread)));
+ }
+
+ ///
+ /// Deserializes a DurableAgentThread from JSON.
+ ///
+ /// The serialized thread data.
+ /// Optional JSON serializer options.
+ /// The deserialized DurableAgentThread.
+ public static DurableAgentThread Deserialize(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ if (!serializedThread.TryGetProperty("sessionId", out JsonElement sessionIdElement) ||
+ sessionIdElement.ValueKind != JsonValueKind.String)
+ {
+ throw new JsonException("Invalid or missing sessionId property.");
+ }
+
+ string sessionIdString = sessionIdElement.GetString() ?? throw new JsonException("sessionId property is null.");
+ AgentSessionId sessionId = AgentSessionId.Parse(sessionIdString);
+ return new DurableAgentThread(sessionId);
+ }
+
+ ///
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ // This is a common convention for MAF agents.
+ if (serviceType == typeof(AgentThreadMetadata))
+ {
+ return new AgentThreadMetadata(conversationId: this.SessionId.ToString());
+ }
+
+ if (serviceType == typeof(AgentSessionId))
+ {
+ return this.SessionId;
+ }
+
+ return base.GetService(serviceType, serviceKey);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return this.SessionId.ToString();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
new file mode 100644
index 0000000000..f2ac3f4c9a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Builder for configuring durable agents.
+///
+public sealed class DurableAgentsOptions
+{
+ // Agent names are case-insensitive
+ private readonly Dictionary> _agentFactories = new(StringComparer.OrdinalIgnoreCase);
+
+ internal DurableAgentsOptions()
+ {
+ }
+
+ ///
+ /// Adds an AI agent factory to the options.
+ ///
+ /// The name of the agent.
+ /// The factory function to create the agent.
+ /// The options instance.
+ /// Thrown when or is null.
+ public DurableAgentsOptions AddAIAgentFactory(string name, Func factory)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ ArgumentNullException.ThrowIfNull(factory);
+ this._agentFactories.Add(name, factory);
+ return this;
+ }
+
+ ///
+ /// Adds a list of AI agents to the options.
+ ///
+ /// The list of agents to add.
+ /// The options instance.
+ /// Thrown when is null.
+ public DurableAgentsOptions AddAIAgents(params IEnumerable agents)
+ {
+ ArgumentNullException.ThrowIfNull(agents);
+ foreach (AIAgent agent in agents)
+ {
+ this.AddAIAgent(agent);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Adds an AI agent to the options.
+ ///
+ /// The agent to add.
+ /// The options instance.
+ /// Thrown when is null.
+ ///
+ /// Thrown when is null or whitespace or when an agent with the same name has already been registered.
+ ///
+ public DurableAgentsOptions AddAIAgent(AIAgent agent)
+ {
+ ArgumentNullException.ThrowIfNull(agent);
+
+ if (string.IsNullOrWhiteSpace(agent.Name))
+ {
+ throw new ArgumentException($"{nameof(agent.Name)} must not be null or whitespace.", nameof(agent));
+ }
+
+ if (this._agentFactories.ContainsKey(agent.Name))
+ {
+ throw new ArgumentException($"An agent with name '{agent.Name}' has already been registered.", nameof(agent));
+ }
+
+ this._agentFactories.Add(agent.Name, sp => agent);
+ return this;
+ }
+
+ ///
+ /// Gets the agents that have been added to this builder.
+ ///
+ /// A read-only collection of agents.
+ internal IReadOnlyDictionary> GetAgentFactories()
+ {
+ return this._agentFactories.AsReadOnly();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs
new file mode 100644
index 0000000000..34c9208967
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs
@@ -0,0 +1,125 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Runtime.CompilerServices;
+using Microsoft.Agents.AI;
+using Microsoft.DurableTask.Entities;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+internal sealed class EntityAgentWrapper(
+ AIAgent innerAgent,
+ TaskEntityContext entityContext,
+ RunRequest runRequest,
+ IServiceProvider? entityScopedServices = null) : DelegatingAIAgent(innerAgent)
+{
+ private readonly TaskEntityContext _entityContext = entityContext;
+ private readonly RunRequest _runRequest = runRequest;
+ private readonly IServiceProvider? _entityScopedServices = entityScopedServices;
+
+ // The ID of the agent is always the entity ID.
+ public override string Id => this._entityContext.Id.ToString();
+
+ public override async Task RunAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ AgentRunResponse response = await base.RunAsync(
+ messages,
+ thread,
+ this.GetAgentEntityRunOptions(options),
+ cancellationToken);
+
+ response.AgentId = this.Id;
+ return response;
+ }
+
+ public override async IAsyncEnumerable RunStreamingAsync(
+ IEnumerable messages,
+ AgentThread? thread = null,
+ AgentRunOptions? options = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await foreach (AgentRunResponseUpdate update in base.RunStreamingAsync(
+ messages,
+ thread,
+ this.GetAgentEntityRunOptions(options),
+ cancellationToken))
+ {
+ update.AgentId = this.Id;
+ yield return update;
+ }
+ }
+
+ // Override the GetService method to provide entity-scoped services.
+ public override object? GetService(Type serviceType, object? serviceKey = null)
+ {
+ object? result = null;
+ if (this._entityScopedServices is not null)
+ {
+ result = (serviceKey is not null && this._entityScopedServices is IKeyedServiceProvider keyedServiceProvider)
+ ? keyedServiceProvider.GetKeyedService(serviceType, serviceKey)
+ : this._entityScopedServices.GetService(serviceType);
+ }
+
+ return result ?? base.GetService(serviceType, serviceKey);
+ }
+
+ private AgentRunOptions GetAgentEntityRunOptions(AgentRunOptions? options = null)
+ {
+ // Copied/modified from FunctionInvocationDelegatingAgent.cs in microsoft/agent-framework.
+ if (options is null || options.GetType() == typeof(AgentRunOptions))
+ {
+ options = new ChatClientAgentRunOptions();
+ }
+
+ if (options is not ChatClientAgentRunOptions chatAgentRunOptions)
+ {
+ throw new NotSupportedException($"Function Invocation Middleware is only supported without options or with {nameof(ChatClientAgentRunOptions)}.");
+ }
+
+ Func? originalFactory = chatAgentRunOptions.ChatClientFactory;
+
+ chatAgentRunOptions.ChatClientFactory = chatClient =>
+ {
+ ChatClientBuilder builder = chatClient.AsBuilder();
+ if (originalFactory is not null)
+ {
+ builder.Use(originalFactory);
+ }
+
+ // Update the run options based on the run request.
+ // NOTE: Function middleware can go here if needed in the future.
+ return builder.ConfigureOptions(
+ newOptions =>
+ {
+ // Update the response format if requested by the caller.
+ if (this._runRequest.ResponseFormat is not null)
+ {
+ newOptions.ResponseFormat = this._runRequest.ResponseFormat;
+ }
+
+ // Update the tools if requested by the caller.
+ if (this._runRequest.EnableToolCalls)
+ {
+ IList? tools = chatAgentRunOptions.ChatOptions?.Tools;
+ if (tools is not null && this._runRequest.EnableToolNames?.Count > 0)
+ {
+ // Filter tools to only include those with matching names
+ newOptions.Tools = [.. tools.Where(tool => this._runRequest.EnableToolNames.Contains(tool.Name))];
+ }
+ }
+ else
+ {
+ newOptions.Tools = null;
+ }
+ })
+ .Build();
+ };
+
+ return options;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/IAgentResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/IAgentResponseHandler.cs
new file mode 100644
index 0000000000..45a4e9f258
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/IAgentResponseHandler.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Handler for processing responses from the agent. This is typically used to send messages to the user.
+///
+public interface IAgentResponseHandler
+{
+ ///
+ /// Handles a streaming response update from the agent. This is typically used to send messages to the user.
+ ///
+ ///
+ /// The stream of messages from the agent.
+ ///
+ ///
+ /// Signals that the operation should be cancelled.
+ ///
+ ValueTask OnStreamingResponseUpdateAsync(
+ IAsyncEnumerable messageStream,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Handles a discrete response from the agent. This is typically used to send messages to the user.
+ ///
+ ///
+ /// The message from the agent.
+ ///
+ ///
+ /// Signals that the operation should be cancelled.
+ ///
+ ValueTask OnAgentResponseAsync(
+ AgentRunResponse message,
+ CancellationToken cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/IDurableAgentClient.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/IDurableAgentClient.cs
new file mode 100644
index 0000000000..d49999cbbe
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/IDurableAgentClient.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents a client for interacting with a durable agent.
+///
+internal interface IDurableAgentClient
+{
+ ///
+ /// Runs an agent with the specified request.
+ ///
+ /// The ID of the target agent session.
+ /// The request containing the message, role, and configuration.
+ /// The cancellation token for scheduling the request.
+ /// A task that returns a handle used to read the agent response.
+ Task RunAgentAsync(
+ AgentSessionId sessionId,
+ RunRequest request,
+ CancellationToken cancellationToken = default);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs
new file mode 100644
index 0000000000..139b073198
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+
+internal static partial class Logs
+{
+ [LoggerMessage(
+ EventId = 1,
+ Level = LogLevel.Information,
+ Message = "[{SessionId}] Request: [{Role}] {Content}")]
+ public static partial void LogAgentRequest(
+ this ILogger logger,
+ AgentSessionId sessionId,
+ ChatRole role,
+ string content);
+
+ [LoggerMessage(
+ EventId = 2,
+ Level = LogLevel.Information,
+ Message = "[{SessionId}] Response: [{Role}] {Content} (Input tokens: {InputTokenCount}, Output tokens: {OutputTokenCount}, Total tokens: {TotalTokenCount})")]
+ public static partial void LogAgentResponse(
+ this ILogger logger,
+ AgentSessionId sessionId,
+ ChatRole role,
+ string content,
+ long? inputTokenCount,
+ long? outputTokenCount,
+ long? totalTokenCount);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
new file mode 100644
index 0000000000..c1cadd29e1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
@@ -0,0 +1,41 @@
+
+
+
+ $(ProjectsCoreTargetFrameworks)
+ $(ProjectsDebugCoreTargetFrameworks)
+ enable
+
+
+ $(NoWarn);CA2007;MEAI001
+
+ false
+
+
+
+
+
+
+ Durable Task extensions for Microsoft Agent Framework
+ Provides distributed durable execution capabilities for agents built with Microsoft Agent Framework.
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/README.md b/dotnet/src/Microsoft.Agents.AI.DurableTask/README.md
new file mode 100644
index 0000000000..ede4f19051
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/README.md
@@ -0,0 +1,42 @@
+# Microsoft.Agents.AI.DurableTask
+
+The Microsoft Agent Framework provides a programming model for building agents and agent workflows in .NET. This package, the *Durable extensions for the Agent Framework*, extends the Agent Framework programming model with the following capabilities:
+
+- Stateful, durable execution of agents in distributed environments
+- Automatic conversation history management
+- Long-running agent workflows as "durable orchestrator" functions
+- Tools and dashboards for managing and monitoring agents and agent workflows
+
+These capabilities are implemented using foundational technologies from the Durable Task technology stack:
+
+- [Durable Entities](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-entities) for stateful, durable execution of agents
+- [Durable Orchestrations](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-orchestrations) for long-running agent workflows
+- The [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/choose-orchestration-framework) for managing durable task execution and observability at scale
+
+This package can be used by itself or in conjunction with the [Microsoft.Agents.AI.Hosting.AzureFunctions](https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.AzureFunctions) package, which provides additional features via Azure Functions integration.
+
+## Install the package
+
+From the command-line:
+
+```bash
+dotnet add package Microsoft.Agents.AI.DurableTask
+```
+
+Or directly in your project file:
+
+```xml
+
+
+
+```
+
+You can alternatively just reference the [Microsoft.Agents.AI.Hosting.AzureFunctions](https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.AzureFunctions) package if you're hosting your agents and orchestrations in the Azure Functions .NET Isolated worker.
+
+## Usage Examples
+
+For a comprehensive tour of all the functionality, concepts, and APIs, check out the [Azure Functions samples](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/AzureFunctions).
+
+## Feedback & Contributing
+
+We welcome feedback and contributions in [our GitHub repo](https://github.com/microsoft/agent-framework).
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/RunRequest.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/RunRequest.cs
new file mode 100644
index 0000000000..f2dcc6832e
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/RunRequest.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Represents a request to run an agent with a specific message and configuration.
+///
+public record RunRequest
+{
+ ///
+ /// Gets the list of chat messages to send to the agent (for multi-message requests).
+ ///
+ public IList Messages { get; init; } = [];
+
+ ///
+ /// Gets the optional response format for the agent's response.
+ ///
+ public ChatResponseFormat? ResponseFormat { get; init; }
+
+ ///
+ /// Gets whether to enable tool calls for this request.
+ ///
+ public bool EnableToolCalls { get; init; } = true;
+
+ ///
+ /// Gets the collection of tool names to enable. If not specified, all tools are enabled.
+ ///
+ public IList? EnableToolNames { get; init; }
+
+ ///
+ /// Gets or sets the correlation ID for correlating this request with its response.
+ ///
+ [JsonInclude]
+ internal string? CorrelationId { get; set; }
+
+ ///
+ /// Initializes a new instance of the class for a single message.
+ ///
+ /// The message to send to the agent.
+ /// The role of the message sender (User or System).
+ /// Optional response format for the agent's response.
+ /// Whether to enable tool calls for this request.
+ /// Optional collection of tool names to enable. If not specified, all tools are enabled.
+ public RunRequest(
+ string message,
+ ChatRole? role = null,
+ ChatResponseFormat? responseFormat = null,
+ bool enableToolCalls = true,
+ IList? enableToolNames = null)
+ : this([new ChatMessage(role ?? ChatRole.User, message)], responseFormat, enableToolCalls, enableToolNames)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class for multiple messages.
+ ///
+ /// The list of chat messages to send to the agent.
+ /// Optional response format for the agent's response.
+ /// Whether to enable tool calls for this request.
+ /// Optional collection of tool names to enable. If not specified, all tools are enabled.
+ [JsonConstructor]
+ public RunRequest(
+ IList messages,
+ ChatResponseFormat? responseFormat = null,
+ bool enableToolCalls = true,
+ IList? enableToolNames = null)
+ {
+ this.Messages = messages;
+ this.ResponseFormat = responseFormat;
+ this.EnableToolCalls = enableToolCalls;
+ this.EnableToolNames = enableToolNames;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..9a910609f0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Worker;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Agent-specific extension methods for the class.
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Gets a durable agent proxy by name.
+ ///
+ /// The service provider.
+ /// The name of the agent.
+ /// The durable agent proxy.
+ /// Thrown if the agent proxy is not found.
+ public static AIAgent GetDurableAgentProxy(this IServiceProvider services, string name)
+ {
+ return services.GetKeyedService(name)
+ ?? throw new KeyNotFoundException($"A durable agent with name '{name}' has not been registered.");
+ }
+
+ ///
+ /// Configures the Durable Agents services via the service collection.
+ ///
+ /// The service collection.
+ /// A delegate to configure the durable agents.
+ /// A delegate to configure the Durable Task worker.
+ /// A delegate to configure the Durable Task client.
+ /// The service collection.
+ public static IServiceCollection ConfigureDurableAgents(
+ this IServiceCollection services,
+ Action configure,
+ Action? workerBuilder = null,
+ Action? clientBuilder = null)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ DurableAgentsOptions options = services.ConfigureDurableAgents(configure);
+
+ // A worker is required to run the agent entities
+ services.AddDurableTaskWorker(builder =>
+ {
+ workerBuilder?.Invoke(builder);
+
+ builder.AddTasks(registry =>
+ {
+ foreach (string name in options.GetAgentFactories().Keys)
+ {
+ registry.AddEntity(AgentSessionId.ToEntityName(name));
+ }
+ });
+ });
+
+ // The client is needed to send notifications to the agent entities from non-orchestrator code
+ if (clientBuilder != null)
+ {
+ services.AddDurableTaskClient(clientBuilder);
+ }
+
+ services.AddSingleton();
+
+ return services;
+ }
+
+ // This is internal because it's also used by Microsoft.Azure.Functions.DurableAgents, which is a friend assembly project.
+ internal static DurableAgentsOptions ConfigureDurableAgents(
+ this IServiceCollection services,
+ Action configure)
+ {
+ DurableAgentsOptions options = new();
+ configure(options);
+
+ var agents = options.GetAgentFactories();
+
+ // The agent dictionary contains the real agent factories, which is used by the agent entities.
+ services.AddSingleton(agents);
+
+ // The keyed services are used to resolve durable agent *proxy* instances for external clients.
+ foreach (var factory in agents)
+ {
+ services.AddKeyedSingleton(factory.Key, (sp, _) => factory.Value(sp).AsDurableAgentProxy(sp));
+ }
+
+ // A custom data converter is needed because the default chat client uses camel case for JSON properties,
+ // which is not the default behavior for the Durable Task SDK.
+ services.AddSingleton();
+
+ return options;
+ }
+
+ private class DefaultDataConverter : DataConverter
+ {
+ // Use durable agent options (web defaults + camel case by default) with case-insensitive matching.
+ // We clone to apply naming/casing tweaks while retaining source-generated metadata where available.
+ private static readonly JsonSerializerOptions s_options = new(DurableAgentJsonUtilities.DefaultOptions)
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ };
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback path uses reflection when metadata unavailable.")]
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "Fallback path uses reflection when metadata unavailable.")]
+ public override object? Deserialize(string? data, Type targetType)
+ {
+ if (data is null)
+ {
+ return null;
+ }
+
+ JsonTypeInfo? typeInfo = s_options.GetTypeInfo(targetType);
+ if (typeInfo is JsonTypeInfo typedInfo)
+ {
+ return JsonSerializer.Deserialize(data, typedInfo);
+ }
+
+ // Fallback (may trigger trimming/AOT warnings for unsupported dynamic types).
+ return JsonSerializer.Deserialize(data, targetType, s_options);
+ }
+
+ [return: NotNullIfNotNull(nameof(value))]
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Fallback path uses reflection when metadata unavailable.")]
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "Fallback path uses reflection when metadata unavailable.")]
+ public override string? Serialize(object? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ JsonTypeInfo? typeInfo = s_options.GetTypeInfo(value.GetType());
+ if (typeInfo is JsonTypeInfo typedInfo)
+ {
+ return JsonSerializer.Serialize(value, typedInfo);
+ }
+
+ return JsonSerializer.Serialize(value, s_options);
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/TaskOrchestrationContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/TaskOrchestrationContextExtensions.cs
new file mode 100644
index 0000000000..63f491cf48
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/TaskOrchestrationContextExtensions.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+using Microsoft.DurableTask;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Agent-related extension methods for the class.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+public static class TaskOrchestrationContextExtensions
+{
+ ///
+ /// Gets a for interacting with hosted agents within an orchestration.
+ ///
+ /// The orchestration context.
+ /// The name of the agent.
+ /// Thrown when is null or empty.
+ /// A that can be used to interact with the agent.
+ public static DurableAIAgent GetAgent(
+ this TaskOrchestrationContext context,
+ string agentName)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(agentName);
+ return new DurableAIAgent(context, agentName);
+ }
+
+ ///
+ /// Generates an for an agent.
+ ///
+ ///
+ /// This method is deterministic and safe for use in an orchestration context.
+ ///
+ /// The orchestration context.
+ /// The name of the agent.
+ /// Thrown when is null or empty.
+ /// The generated agent session ID.
+ internal static AgentSessionId NewAgentSessionId(
+ this TaskOrchestrationContext context,
+ string agentName)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(agentName);
+
+ return new AgentSessionId(agentName, context.NewGuid().ToString("N"));
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
new file mode 100644
index 0000000000..55021172aa
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Context.Features;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Azure.Functions.Worker.Invocation;
+using Microsoft.DurableTask.Client;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// This implementation of function executor handles invocations using the built-in static methods for agent HTTP and entity functions.
+///
+/// By default, the Azure Functions worker generates function executor and that executor is used for function invocations.
+/// But for the dummy HTTP function we create for agents (by augmenting the metadata), that executor will not have the code to handle that function since the entrypoint is a built-in static method.
+///
+internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
+{
+ public async ValueTask ExecuteAsync(FunctionContext context)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ // Acquire the input binding feature (fail fast if missing rather than null-forgiving operator).
+ IFunctionInputBindingFeature? functionInputBindingFeature = context.Features.Get();
+ if (functionInputBindingFeature == null)
+ {
+ throw new InvalidOperationException("Function input binding feature is not available on the current context.");
+ }
+
+ FunctionInputBindingResult? inputBindingResults = await functionInputBindingFeature.BindFunctionInputAsync(context);
+ if (inputBindingResults is not { Values: { } values })
+ {
+ throw new InvalidOperationException($"Function input binding failed for the invocation {context.InvocationId}");
+ }
+
+ HttpRequestData? httpRequestData = null;
+ TaskEntityDispatcher? dispatcher = null;
+ DurableTaskClient? durableTaskClient = null;
+
+ foreach (var binding in values)
+ {
+ switch (binding)
+ {
+ case HttpRequestData request:
+ httpRequestData = request;
+ break;
+ case TaskEntityDispatcher entityDispatcher:
+ dispatcher = entityDispatcher;
+ break;
+ case DurableTaskClient client:
+ durableTaskClient = client;
+ break;
+ }
+ }
+
+ bool isAgentHttpInvocation = string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal);
+
+ if (isAgentHttpInvocation)
+ {
+ if (httpRequestData == null)
+ {
+ throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
+ }
+ if (durableTaskClient == null)
+ {
+ throw new InvalidOperationException($"Durable Task client binding is missing for the invocation {context.InvocationId}.");
+ }
+
+ context.GetInvocationResult().Value = await BuiltInFunctions.RunAgentHttpAsync(
+ httpRequestData,
+ durableTaskClient,
+ context);
+ return;
+ }
+
+ // If not HTTP invocation, It will be entity invocation path.
+ if (dispatcher == null)
+ {
+ throw new InvalidOperationException($"Task entity dispatcher binding is missing for the invocation {context.InvocationId}.");
+ }
+ if (durableTaskClient == null)
+ {
+ throw new InvalidOperationException($"Durable Task client binding is missing for the invocation {context.InvocationId}.");
+ }
+
+ await BuiltInFunctions.InvokeAgentAsync(
+ dispatcher,
+ durableTaskClient,
+ context);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
new file mode 100644
index 0000000000..80e2a2dfaa
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net;
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+internal static class BuiltInFunctions
+{
+ internal static readonly string RunAgentHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunAgentHttpAsync)}";
+ internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}";
+
+ // Exposed as an entity trigger via AgentFunctionsProvider
+ public static async Task InvokeAgentAsync(
+ [EntityTrigger] TaskEntityDispatcher dispatcher,
+ [DurableClient] DurableTaskClient client,
+ FunctionContext functionContext)
+ {
+ // This should never be null except if the function trigger is misconfigured.
+ ArgumentNullException.ThrowIfNull(dispatcher);
+ ArgumentNullException.ThrowIfNull(client);
+ ArgumentNullException.ThrowIfNull(functionContext);
+
+ // Create a combined service provider that includes both the existing services
+ // and the DurableTaskClient instance
+ IServiceProvider combinedServiceProvider = new CombinedServiceProvider(functionContext.InstanceServices, client);
+
+ // This method is the entry point for the agent entity.
+ // It will be invoked by the Azure Functions runtime when the entity is called.
+ await dispatcher.DispatchAsync(new AgentEntity(combinedServiceProvider, functionContext.CancellationToken));
+ }
+
+ public static async Task RunAgentHttpAsync(
+ [HttpTrigger] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ FunctionContext context)
+ {
+ // The session ID is an optional query string parameter.
+ string? threadIdFromQuery = req.Query["threadId"];
+
+ // If no session ID is provided, use a new one based on the function name and invocation ID.
+ // This may be better than a random one because it can be correlated with the function invocation.
+ // Specifying a session ID is how the caller correlates multiple calls to the same agent session.
+ AgentSessionId sessionId = string.IsNullOrEmpty(threadIdFromQuery)
+ ? new AgentSessionId(GetAgentName(context), context.InvocationId)
+ : AgentSessionId.Parse(threadIdFromQuery);
+
+ string? message = await req.ReadAsStringAsync();
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ HttpResponseData response = req.CreateResponse(HttpStatusCode.BadRequest);
+ await response.WriteAsJsonAsync(
+ new { error = "Run request cannot be empty." },
+ context.CancellationToken);
+ return response;
+ }
+
+ AIAgent agentProxy = client.AsDurableAgentProxy(context, GetAgentName(context));
+
+ AgentRunResponse agentResponse = await agentProxy.RunAsync(
+ message: new ChatMessage(ChatRole.User, message),
+ thread: new DurableAgentThread(sessionId),
+ options: null,
+ cancellationToken: context.CancellationToken);
+
+ HttpResponseData httpResponse = req.CreateResponse(HttpStatusCode.OK);
+ httpResponse.Headers.Add("X-Agent-Thread", sessionId.ToString());
+
+ // If the caller accepts JSON, return the entire response object as JSON.
+ // TODO: Need to define a standard response schema for agent responses.
+ // https://github.com/Azure/durable-agent-framework/issues/113
+ if (req.Headers.TryGetValues("Accept", out IEnumerable? acceptValues) &&
+ acceptValues.Contains("application/json", StringComparer.OrdinalIgnoreCase))
+ {
+ await httpResponse.WriteAsJsonAsync(
+ new { response = agentResponse, threadId = sessionId },
+ context.CancellationToken);
+ }
+ else
+ {
+ // The default is to return the response as text/plain.
+ httpResponse.Headers.Add("Content-Type", "text/plain");
+ await httpResponse.WriteStringAsync(agentResponse.Text, context.CancellationToken);
+ }
+
+ return httpResponse;
+ }
+
+ private static string GetAgentName(FunctionContext context)
+ {
+ // Remove the trailing _http from the function name
+ string functionName = context.FunctionDefinition.Name;
+ if (!functionName.EndsWith("_http", StringComparison.Ordinal))
+ {
+ // This should never happen because the function metadata provider ensures
+ // that the function name ends with '_http'.
+ throw new InvalidOperationException(
+ $"Built-in HTTP trigger function name '{functionName}' does not end with '_http'.");
+ }
+
+ return functionName[..^5];
+ }
+
+ ///
+ /// A service provider that combines the original service provider with an additional DurableTaskClient instance.
+ ///
+ private sealed class CombinedServiceProvider(IServiceProvider originalProvider, DurableTaskClient client)
+ : IServiceProvider, IKeyedServiceProvider
+ {
+ private readonly IServiceProvider _originalProvider = originalProvider;
+ private readonly DurableTaskClient _client = client;
+
+ public object? GetKeyedService(Type serviceType, object? serviceKey)
+ {
+ if (this._originalProvider is IKeyedServiceProvider keyedProvider)
+ {
+ return keyedProvider.GetKeyedService(serviceType, serviceKey);
+ }
+
+ return null;
+ }
+
+ public object GetRequiredKeyedService(Type serviceType, object? serviceKey)
+ {
+ if (this._originalProvider is IKeyedServiceProvider keyedProvider)
+ {
+ return keyedProvider.GetRequiredKeyedService(serviceType, serviceKey);
+ }
+
+ throw new InvalidOperationException("The original service provider does not support keyed services.");
+ }
+
+ public object? GetService(Type serviceType)
+ {
+ // If the requested service is DurableTaskClient, return our instance
+ if (serviceType == typeof(DurableTaskClient))
+ {
+ return this._client;
+ }
+
+ // Otherwise try to get the service from the original provider
+ return this._originalProvider.GetService(serviceType);
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
new file mode 100644
index 0000000000..e908deac89
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Release History
+
+## v1.0.0-preview.* (Unreleased)
+
+- Initial public release.
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
new file mode 100644
index 0000000000..a8cfd62053
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableAgentFunctionMetadataTransformer.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Transforms function metadata by registering durable agent functions for each configured agent.
+///
+/// This transformer adds both entity trigger and HTTP trigger functions for every agent registered in the application.
+internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadataTransformer
+{
+ private readonly ILogger _logger;
+ private readonly IReadOnlyDictionary> _agents;
+
+#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - Azure Functions does not use single-file publishing
+ private static readonly string s_builtInFunctionsScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
+#pragma warning restore IL3000
+
+ public DurableAgentFunctionMetadataTransformer(
+ IReadOnlyDictionary> agents,
+ ILogger logger)
+ {
+ this._agents = agents ?? throw new ArgumentNullException(nameof(agents));
+ this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public string Name => nameof(DurableAgentFunctionMetadataTransformer);
+
+ public void Transform(IList original)
+ {
+ this._logger.LogInformation("Transforming function metadata to add durable agent functions. Initial function count: {FunctionCount}", original.Count);
+
+ foreach (string agentName in this._agents.Keys)
+ {
+ this._logger.LogInformation("Registering functions for agent: {AgentName}", agentName);
+
+ // Each agent type gets its own entity trigger function.
+ // We do this 1:1 mapping for improved telemetry.
+ original.Add(CreateAgentTrigger(agentName));
+
+ // Each agent type gets its own HTTP trigger function.
+ // TODO: Put this behind a configuration option.
+ original.Add(CreateHttpTrigger(agentName, $"agents/{agentName}/run", nameof(BuiltInFunctions.RunAgentHttpAsync)));
+ }
+ }
+
+ private static DefaultFunctionMetadata CreateAgentTrigger(string name)
+ {
+ return new DefaultFunctionMetadata()
+ {
+ Name = AgentSessionId.ToEntityName(name),
+ Language = "dotnet-isolated",
+ RawBindings =
+ [
+ """{"name":"dispatcher","type":"entityTrigger","direction":"In"}""",
+ """{"name":"client","type":"durableClient","direction":"In"}"""
+ ],
+ EntryPoint = BuiltInFunctions.RunAgentEntityFunctionEntryPoint,
+ ScriptFile = s_builtInFunctionsScriptFile,
+ };
+ }
+
+ private static DefaultFunctionMetadata CreateHttpTrigger(string name, string route, string dotnetMethodName)
+ {
+ return new DefaultFunctionMetadata()
+ {
+ Name = $"{name}_http",
+ Language = "dotnet-isolated",
+ RawBindings =
+ [
+ $$"""{"name":"req","type":"httpTrigger","direction":"In","authLevel":"function","methods": ["post"],"route":"{{route}}"}""",
+ """{"name":"$return","type":"http","direction":"Out"}""",
+ """{"name":"client","type":"durableClient","direction":"In"}"""
+ ],
+ EntryPoint = BuiltInFunctions.RunAgentHttpFunctionEntryPoint,
+ ScriptFile = s_builtInFunctionsScriptFile,
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs
new file mode 100644
index 0000000000..7628a3f3bf
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Extension methods for the class.
+///
+public static class DurableTaskClientExtensions
+{
+ ///
+ /// Converts a to a durable agent proxy.
+ ///
+ /// The to convert.
+ /// The for the current function invocation.
+ /// The name of the agent.
+ /// A durable agent proxy.
+ /// Thrown when or is null.
+ /// Thrown when is null or empty.
+ public static AIAgent AsDurableAgentProxy(
+ this DurableTaskClient durableClient,
+ FunctionContext context,
+ string agentName)
+ {
+ ArgumentNullException.ThrowIfNull(durableClient);
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentException.ThrowIfNullOrEmpty(agentName);
+
+ DefaultDurableAgentClient agentClient = ActivatorUtilities.CreateInstance(
+ context.InstanceServices,
+ durableClient);
+
+ return new DurableAIAgentProxy(agentName, agentClient);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
new file mode 100644
index 0000000000..614b530dc5
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// Extension methods for the class.
+///
+public static class FunctionsApplicationBuilderExtensions
+{
+ ///
+ /// Configures the application to use durable agents with a builder pattern.
+ ///
+ /// The functions application builder.
+ /// A delegate to configure the durable agents.
+ /// The functions application builder.
+ public static FunctionsApplicationBuilder ConfigureDurableAgents(
+ this FunctionsApplicationBuilder builder,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ // The main agent services registration is done in Microsoft.DurableTask.Agents.
+ builder.Services.ConfigureDurableAgents(configure);
+
+ builder.Services.TryAddSingleton();
+
+ // Handling of built-in function execution for Agent HTTP or Entity invocations.
+ builder.UseWhen(static context =>
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));
+ builder.Services.AddSingleton();
+
+ return builder;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj
new file mode 100644
index 0000000000..7f11ef8fc5
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj
@@ -0,0 +1,43 @@
+
+
+
+ $(ProjectsCoreTargetFrameworks)
+ $(ProjectsDebugCoreTargetFrameworks)
+ enable
+
+ $(NoWarn);CA2007
+
+ false
+
+
+
+
+
+
+ Azure Functions extensions for Microsoft Agent Framework
+ Provides durable agent hosting and orchestration support for Microsoft Agent Framework workloads.
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Middlewares/BuiltInFunctionExecutionMiddleware.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Middlewares/BuiltInFunctionExecutionMiddleware.cs
new file mode 100644
index 0000000000..3dc1a58943
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/Middlewares/BuiltInFunctionExecutionMiddleware.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Invocation;
+using Microsoft.Azure.Functions.Worker.Middleware;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+///
+/// This middleware sets a custom function executor for invocation of functions that have the built-in method as the entrypoint.
+///
+internal sealed class BuiltInFunctionExecutionMiddleware(BuiltInFunctionExecutor builtInFunctionExecutor)
+ : IFunctionsWorkerMiddleware
+{
+ private readonly BuiltInFunctionExecutor _builtInFunctionExecutor = builtInFunctionExecutor;
+
+ public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
+ {
+ // We set our custom function executor for this invocation.
+ context.Features.Set(this._builtInFunctionExecutor);
+
+ await next(context);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md
new file mode 100644
index 0000000000..7536790a97
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/README.md
@@ -0,0 +1,177 @@
+# Microsoft.Agents.AI.Hosting.AzureFunctions
+
+This package adds Azure Functions integration and serverless hosting for Microsoft Agent Framework on Azure Functions. It builds upon the [Microsoft.Agents.AI.DurableTask](https://www.nuget.org/packages/Microsoft.Agents.AI.DurableTask) package to provide the following capabilities:
+
+- Stateful, durable execution of agents in distributed, serverless environments
+- Automatic conversation history management in supported [Durable Functions backends](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-task-hubs/choose-durable-functions-backends)
+- Long-running agent workflows as "durable orchestrator" functions
+- Tools and [dashboards](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler-dashboard) for managing and monitoring agents and agent workflows
+
+## Install the package
+
+From the command-line:
+
+```bash
+dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions
+```
+
+Or directly in your project file:
+
+```xml
+
+
+
+```
+
+## Usage Examples
+
+For a comprehensive tour of all the functionality, concepts, and APIs, check out the [Azure Functions samples](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/AzureFunctions) in the [Durable extensions for Agent Framework repository](https://github.com/microsoft/agent-framework).
+
+### Hosting single agents
+
+This package provides a `ConfigureDurableAgents` extension method on the `FunctionsApplicationBuilder` class to configure the application to host Microsoft Agent Framework agents. These hosted agents are automatically registered as durable entities with the Durable Task runtime and can be invoked via HTTP or Durable Task orchestrator functions.
+
+```csharp
+// Create agents using the standard Microsoft Agent Framework.
+// Invocable via HTTP via http://localhost:7071/api/agents/SpamDetectionAgent/run
+AIAgent spamDetector = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You are a spam detection assistant that identifies spam emails.",
+ name: "SpamDetectionAgent");
+
+AIAgent emailAssistant = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You are an email assistant that helps users draft responses to emails with professionalism.",
+ name: "EmailAssistantAgent");
+
+// Configure the Functions application to host the agents.
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableAgents(options =>
+ {
+ options.AddAIAgent(spamDetector);
+ options.AddAIAgent(emailAssistant);
+ })
+ .Build();
+app.Run();
+```
+
+By default, each agent can be invoked via a built-in HTTP trigger function at the route `http[s]://[host]/api/agents/{agentName}/run`.
+
+### Orchestrating hosted agents
+
+This package also provides a set of extension methods such as `GetAgent` on the [`TaskOrchestrationContext`](https://learn.microsoft.com/dotnet/api/microsoft.durabletask.taskorchestrationcontext) class for interacting with hosted agents within orchestrations.
+
+```csharp
+[Function(nameof(SpamDetectionOrchestration))]
+public static async Task SpamDetectionOrchestration(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+{
+ Email email = context.GetInput() ?? throw new InvalidOperationException("Email is required");
+
+ // Get the spam detection agent
+ DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent");
+ AgentThread spamThread = spamDetectionAgent.GetNewThread();
+
+ // Step 1: Check if the email is spam
+ AgentRunResponse spamDetectionResponse = await spamDetectionAgent.RunAsync(
+ message:
+ $"""
+ Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) and 'reason' (string) fields:
+ Email ID: {email.EmailId}
+ Content: {email.EmailContent}
+ """,
+ thread: spamThread);
+ DetectionResult result = spamDetectionResponse.Result;
+
+ // Step 2: Conditional logic based on spam detection result
+ if (result.IsSpam)
+ {
+ // Handle spam email
+ return await context.CallActivityAsync(nameof(HandleSpamEmail), result.Reason);
+ }
+ else
+ {
+ // Generate and send response for legitimate email
+ DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent");
+ AgentThread emailThread = emailAssistantAgent.GetNewThread();
+
+ AgentRunResponse emailAssistantResponse = await emailAssistantAgent.RunAsync(
+ message:
+ $"""
+ Draft a professional response to this email. Return a JSON response with a 'response' field containing the reply:
+
+ Email ID: {email.EmailId}
+ Content: {email.EmailContent}
+ """,
+ thread: emailThread);
+
+ EmailResponse emailResponse = emailAssistantResponse.Result;
+ return await context.CallActivityAsync(nameof(SendEmail), emailResponse.Response);
+ }
+}
+```
+
+### Scheduling orchestrations from custom code tools
+
+Agents can also schedule and interact with orchestrations from custom code tools. This is useful for long-running tool use cases where orchestrations need to be executed in the context of the agent.
+
+The `DurableAgentContext.Current` *AsyncLocal* property provides access to the current agent context, which can be used to schedule and interact with orchestrations.
+
+```csharp
+class Tools
+{
+ [Description("Starts a content generation workflow and returns the instance ID for tracking.")]
+ public string StartContentGenerationWorkflow(
+ [Description("The topic for content generation")] string topic)
+ {
+ // ContentGenerationWorkflow is an orchestrator function defined in the same project.
+ string instanceId = DurableAgentContext.Current.ScheduleNewOrchestration(
+ name: nameof(ContentGenerationWorkflow),
+ input: topic);
+
+ // Return the instance ID so that it gets added to the LLM context.
+ return instanceId;
+ }
+
+ [Description("Gets the status of a content generation workflow.")]
+ public async Task GetContentGenerationStatus(
+ [Description("The instance ID of the workflow to check")] string instanceId,
+ [Description("Whether to include detailed information")] bool includeDetails = true)
+ {
+ OrchestrationMetadata? status = await DurableAgentContext.Current.Client.GetOrchestrationStatusAsync(
+ instanceId,
+ includeDetails);
+ return status ?? throw new InvalidOperationException($"Workflow instance '{instanceId}' not found.");
+ }
+}
+```
+
+These tools are registered with the agent using the `tools` parameter when creating the agent.
+
+```csharp
+Tools tools = new();
+AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You are a content generation assistant that helps users generate content.",
+ name: "ContentGenerationAgent",
+ tools: [
+ AIFunctionFactory.Create(tools.StartContentGenerationWorkflow),
+ AIFunctionFactory.Create(tools.GetContentGenerationStatus)
+ ]);
+
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableAgents(options => options.AddAIAgent(agent))
+ .Build();
+app.Run();
+```
+
+## Feedback & Contributing
+
+We welcome feedback and contributions in [our GitHub repo](https://github.com/microsoft/agent-framework).
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/AgentSessionIdTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/AgentSessionIdTests.cs
new file mode 100644
index 0000000000..03d171b7b3
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/AgentSessionIdTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.DurableTask.Entities;
+
+namespace Microsoft.Agents.AI.DurableTask.UnitTests;
+
+public sealed class AgentSessionIdTests
+{
+ [Fact]
+ public void ParseValidSessionId()
+ {
+ const string Name = "test-agent";
+ const string Key = "12345";
+ string sessionIdString = $"@dafx-{Name}@{Key}";
+ AgentSessionId sessionId = AgentSessionId.Parse(sessionIdString);
+
+ Assert.Equal(Name, sessionId.Name);
+ Assert.Equal(Key, sessionId.Key);
+ }
+
+ [Fact]
+ public void ParseInvalidSessionId()
+ {
+ const string InvalidSessionIdString = "@test-agent@12345"; // Missing "dafx-" prefix
+ Assert.Throws(() => AgentSessionId.Parse(InvalidSessionIdString));
+ }
+
+ [Fact]
+ public void FromEntityId()
+ {
+ const string Name = "test-agent";
+ const string Key = "12345";
+
+ EntityInstanceId entityId = new($"dafx-{Name}", Key);
+ AgentSessionId sessionId = (AgentSessionId)entityId;
+
+ Assert.Equal(Name, sessionId.Name);
+ Assert.Equal(Key, sessionId.Key);
+ }
+
+ [Fact]
+ public void FromInvalidEntityId()
+ {
+ const string Name = "test-agent";
+ const string Key = "12345";
+
+ EntityInstanceId entityId = new(Name, Key); // Missing "dafx-" prefix
+
+ Assert.Throws(() =>
+ {
+ // This assignment should throw an exception because
+ // the entity ID is not a valid agent session ID.
+ AgentSessionId sessionId = entityId;
+ });
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentThreadTests.cs
new file mode 100644
index 0000000000..7e5a776beb
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/DurableAgentThreadTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+
+namespace Microsoft.Agents.AI.DurableTask.UnitTests;
+
+public sealed class DurableAgentThreadTests
+{
+ [Fact]
+ public void BuiltInSerialization()
+ {
+ AgentSessionId sessionId = AgentSessionId.WithRandomKey("test-agent");
+ AgentThread thread = new DurableAgentThread(sessionId);
+
+ JsonElement serializedThread = thread.Serialize();
+
+ // Expected format: "{\"sessionId\":\"@dafx-test-agent@\"}"
+ string expectedSerializedThread = $"{{\"sessionId\":\"@dafx-{sessionId.Name}@{sessionId.Key}\"}}";
+ Assert.Equal(expectedSerializedThread, serializedThread.ToString());
+
+ DurableAgentThread deserializedThread = DurableAgentThread.Deserialize(serializedThread);
+ Assert.Equal(sessionId, deserializedThread.SessionId);
+ }
+
+ [Fact]
+ public void STJSerialization()
+ {
+ AgentSessionId sessionId = AgentSessionId.WithRandomKey("test-agent");
+ AgentThread thread = new DurableAgentThread(sessionId);
+
+ // Need to specify the type explicitly because STJ, unlike other serializers,
+ // does serialization based on the static type of the object, not the runtime type.
+ string serializedThread = JsonSerializer.Serialize(thread, typeof(DurableAgentThread));
+
+ // Expected format: "{\"sessionId\":\"@dafx-test-agent@\"}"
+ string expectedSerializedThread = $"{{\"sessionId\":\"@dafx-{sessionId.Name}@{sessionId.Key}\"}}";
+ Assert.Equal(expectedSerializedThread, serializedThread);
+
+ DurableAgentThread? deserializedThread = JsonSerializer.Deserialize(serializedThread);
+ Assert.NotNull(deserializedThread);
+ Assert.Equal(sessionId, deserializedThread.SessionId);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj
new file mode 100644
index 0000000000..02ed1b58e7
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(ProjectsCoreTargetFrameworks)
+ $(ProjectsDebugCoreTargetFrameworks)
+ enable
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/DurableAgentFunctionMetadataTransformerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/DurableAgentFunctionMetadataTransformerTests.cs
new file mode 100644
index 0000000000..9a924c1bfd
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/DurableAgentFunctionMetadataTransformerTests.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests;
+
+public sealed class DurableAgentFunctionMetadataTransformerTests
+{
+ [Theory]
+ [InlineData(0)] // Empty original metadata list
+ [InlineData(3)] // Non-empty original metadata list
+ public void Transform_AddsAgentAndHttpTriggers_ForEachAgent(int initialMetadataEntryCount)
+ {
+ Dictionary> agents = new()
+ {
+ { "testAgent", _ => null! }
+ };
+ DurableAgentFunctionMetadataTransformer transformer = new(agents, GetTestLogger());
+ List metadataList = BuildFunctionMetadataList(initialMetadataEntryCount);
+
+ transformer.Transform(metadataList);
+
+ Assert.Equal(initialMetadataEntryCount + 2, metadataList.Count); // each agent adds 2 functions (http + entity).
+
+ DefaultFunctionMetadata agentTrigger = Assert.IsType(metadataList[initialMetadataEntryCount]);
+ Assert.Equal("dafx-testAgent", agentTrigger.Name);
+ Assert.Equal("dotnet-isolated", agentTrigger.Language);
+ Assert.Contains("type\":\"entityTrigger", agentTrigger.RawBindings![0]);
+
+ DefaultFunctionMetadata httpTrigger = Assert.IsType(metadataList[initialMetadataEntryCount + 1]);
+ Assert.Equal("testAgent_http", httpTrigger.Name);
+ Assert.Equal("dotnet-isolated", httpTrigger.Language);
+ Assert.Contains("type\":\"httpTrigger", httpTrigger.RawBindings![0]);
+ Assert.Contains("route\":\"agents/testAgent/run", httpTrigger.RawBindings[0]);
+ }
+
+ [Fact]
+ public void Transform_AddsTriggers_ForMultipleAgents()
+ {
+ Dictionary> agents = new()
+ {
+ { "agentA", _ => null! },
+ { "agentB", _ => null! },
+ { "agentC", _ => null! }
+ };
+ DurableAgentFunctionMetadataTransformer transformer = new(agents, GetTestLogger());
+ const int InitialMetadataEntryCount = 2;
+ List metadataList = BuildFunctionMetadataList(InitialMetadataEntryCount);
+
+ transformer.Transform(metadataList);
+
+ Assert.Equal(InitialMetadataEntryCount + (agents.Count * 2), metadataList.Count);
+
+ foreach (string agentName in agents.Keys)
+ {
+ // The agent's entity trigger name is prefixed with "dafx-"
+ DefaultFunctionMetadata entityMeta =
+ Assert.IsType(
+ Assert.Single(metadataList, m => m.Name == "dafx-" + agentName));
+ Assert.NotNull(entityMeta.RawBindings);
+ Assert.Contains("entityTrigger", entityMeta.RawBindings[0]);
+
+ DefaultFunctionMetadata httpMeta =
+ Assert.IsType(
+ Assert.Single(metadataList, m => m.Name == agentName + "_http"));
+ Assert.NotNull(httpMeta.RawBindings);
+ Assert.Contains("httpTrigger", httpMeta.RawBindings[0]);
+ Assert.Contains($"agents/{agentName}/run", httpMeta.RawBindings[0]);
+ }
+ }
+
+ private static List BuildFunctionMetadataList(int numberOfFunctions)
+ {
+ List list = [];
+ for (int i = 0; i < numberOfFunctions; i++)
+ {
+ list.Add(new DefaultFunctionMetadata
+ {
+ Language = "dotnet-isolated",
+ Name = $"SingleAgentOrchestration{i + 1}",
+ EntryPoint = "MyApp.Functions.SingleAgentOrchestration",
+ RawBindings =
+ [
+ "{\r\n \"name\": \"context\",\r\n \"direction\": \"In\",\r\n \"type\": \"orchestrationTrigger\",\r\n \"properties\": {}\r\n }"
+ ],
+ ScriptFile = "MyApp.dll"
+ });
+ }
+
+ return list;
+ }
+
+ private static NullLogger GetTestLogger() => new();
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj
new file mode 100644
index 0000000000..d3842800b9
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(ProjectsCoreTargetFrameworks)
+ $(ProjectsDebugCoreTargetFrameworks)
+ enable
+
+
+
+
+
+
+