Merge branch 'main' into feature-foundry-agents

This commit is contained in:
Chris
2025-11-14 13:59:40 -08:00
committed by GitHub
Unverified
7 changed files with 223 additions and 4 deletions
@@ -17,10 +17,14 @@ public static class AIAgentExtensions
/// <param name="services">The service provider.</param>
/// <returns>The durable agent proxy.</returns>
/// <exception cref="ArgumentException">
/// Thrown when the agent is a DurableAIAgent instance or if the agent has no name.
/// Thrown when the agent is a <see cref="DurableAIAgent"/> instance or if the agent has no name.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Thrown if <paramref name="services"/> does not contain an <see cref="IDurableAgentClient"/>.
/// Thrown if <paramref name="services"/> does not contain an <see cref="IDurableAgentClient"/>
/// or if durable agents have not been configured on the service collection.
/// </exception>
/// <exception cref="AgentNotRegisteredException">
/// Thrown when the agent with the specified name has not been registered.
/// </exception>
public static AIAgent AsDurableAgentProxy(this AIAgent agent, IServiceProvider services)
{
@@ -33,6 +37,10 @@ public static class AIAgentExtensions
}
string agentName = agent.Name ?? throw new ArgumentException("Agent must have a name.", nameof(agent));
// Validate that the agent is registered
ServiceCollectionExtensions.ValidateAgentIsRegistered(services, agentName);
IDurableAgentClient agentClient = services.GetRequiredService<IDurableAgentClient>();
return new DurableAIAgentProxy(agentName, agentClient);
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Agents.AI.DurableTask;
/// <summary>
/// Exception thrown when an agent with the specified name has not been registered.
/// </summary>
public sealed class AgentNotRegisteredException : InvalidOperationException
{
// Not used, but required by static analysis.
private AgentNotRegisteredException()
{
this.AgentName = string.Empty;
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentNotRegisteredException"/> class with the agent name.
/// </summary>
/// <param name="agentName">The name of the agent that was not registered.</param>
public AgentNotRegisteredException(string agentName)
: base(GetMessage(agentName))
{
this.AgentName = agentName;
}
/// <summary>
/// Initializes a new instance of the <see cref="AgentNotRegisteredException"/> class with the agent name and an inner exception.
/// </summary>
/// <param name="agentName">The name of the agent that was not registered.</param>
/// <param name="innerException">The exception that is the cause of the current exception.</param>
public AgentNotRegisteredException(string agentName, Exception? innerException)
: base(GetMessage(agentName), innerException)
{
this.AgentName = agentName;
}
/// <summary>
/// Gets the name of the agent that was not registered.
/// </summary>
public string AgentName { get; }
private static string GetMessage(string agentName)
{
ArgumentException.ThrowIfNullOrEmpty(agentName);
return $"No agent named '{agentName}' was registered. Ensure the agent is registered using {nameof(ServiceCollectionExtensions.ConfigureDurableAgents)} before using it in an orchestration.";
}
}
@@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Microsoft.DurableTask;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DurableTask;
@@ -59,6 +60,9 @@ public sealed class DurableAIAgent : AIAgent
/// <param name="options">Optional run options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response from the agent.</returns>
/// <exception cref="AgentNotRegisteredException">Thrown when the agent has not been registered.</exception>
/// <exception cref="ArgumentException">Thrown when the provided thread is not valid for a durable agent.</exception>
/// <exception cref="NotSupportedException">Thrown when cancellation is requested (cancellation is not supported for durable agents).</exception>
public override async Task<AgentRunResponse> RunAsync(
IEnumerable<ChatMessage> messages,
AgentThread? thread = null,
@@ -95,7 +99,17 @@ public sealed class DurableAIAgent : AIAgent
}
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
return await this._context.Entities.CallEntityAsync<AgentRunResponse>(durableThread.SessionId, nameof(AgentEntity.RunAgentAsync), request);
try
{
return await this._context.Entities.CallEntityAsync<AgentRunResponse>(
durableThread.SessionId,
nameof(AgentEntity.RunAgentAsync),
request);
}
catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
{
throw new AgentNotRegisteredException(this._agentName, e);
}
}
/// <summary>
@@ -80,7 +80,7 @@ public static class ServiceCollectionExtensions
DurableAgentsOptions options = new();
configure(options);
var agents = options.GetAgentFactories();
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>> agents = options.GetAgentFactories();
// The agent dictionary contains the real agent factories, which is used by the agent entities.
services.AddSingleton(agents);
@@ -98,6 +98,30 @@ public static class ServiceCollectionExtensions
return options;
}
/// <summary>
/// Validates that an agent with the specified name has been registered.
/// </summary>
/// <param name="services">The service provider.</param>
/// <param name="agentName">The name of the agent to validate.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when the agent dictionary is not registered in the service provider.
/// </exception>
/// <exception cref="AgentNotRegisteredException">
/// Thrown when the agent with the specified name has not been registered.
/// </exception>
internal static void ValidateAgentIsRegistered(IServiceProvider services, string agentName)
{
IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>? agents =
services.GetService<IReadOnlyDictionary<string, Func<IServiceProvider, AIAgent>>>()
?? throw new InvalidOperationException(
$"Durable agents have not been configured. Ensure {nameof(ConfigureDurableAgents)} has been called on the service collection.");
if (!agents.ContainsKey(agentName))
{
throw new AgentNotRegisteredException(agentName);
}
}
private sealed class DefaultDataConverter : DataConverter
{
// Use durable agent options (web defaults + camel case by default) with case-insensitive matching.
@@ -21,6 +21,12 @@ public static class DurableTaskClientExtensions
/// <returns>A durable agent proxy.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="durableClient"/> or <paramref name="context"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="agentName"/> is null or empty.</exception>
/// <exception cref="InvalidOperationException">
/// Thrown when durable agents have not been configured on the service collection.
/// </exception>
/// <exception cref="AgentNotRegisteredException">
/// Thrown when the agent has not been registered.
/// </exception>
public static AIAgent AsDurableAgentProxy(
this DurableTaskClient durableClient,
FunctionContext context,
@@ -30,6 +36,9 @@ public static class DurableTaskClientExtensions
ArgumentNullException.ThrowIfNull(context);
ArgumentException.ThrowIfNullOrEmpty(agentName);
// Validate that the agent is registered
DurableTask.ServiceCollectionExtensions.ValidateAgentIsRegistered(context.InstanceServices, agentName);
DefaultDurableAgentClient agentClient = ActivatorUtilities.CreateInstance<DefaultDurableAgentClient>(
context.InstanceServices,
durableClient);