diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs
index 4f5619ac76..5eac1b84e0 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AIAgentExtensions.cs
@@ -17,10 +17,14 @@ public static class AIAgentExtensions
/// The service provider.
/// The durable agent proxy.
///
- /// Thrown when the agent is a DurableAIAgent instance or if the agent has no name.
+ /// Thrown when the agent is a instance or if the agent has no name.
///
///
- /// Thrown if does not contain an .
+ /// Thrown if does not contain an
+ /// or if durable agents have not been configured on the service collection.
+ ///
+ ///
+ /// Thrown when the agent with the specified name has not been registered.
///
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();
return new DurableAIAgentProxy(agentName, agentClient);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentNotRegisteredException.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentNotRegisteredException.cs
new file mode 100644
index 0000000000..fc051fa0b2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/AgentNotRegisteredException.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Exception thrown when an agent with the specified name has not been registered.
+///
+public sealed class AgentNotRegisteredException : InvalidOperationException
+{
+ // Not used, but required by static analysis.
+ private AgentNotRegisteredException()
+ {
+ this.AgentName = string.Empty;
+ }
+
+ ///
+ /// Initializes a new instance of the class with the agent name.
+ ///
+ /// The name of the agent that was not registered.
+ public AgentNotRegisteredException(string agentName)
+ : base(GetMessage(agentName))
+ {
+ this.AgentName = agentName;
+ }
+
+ ///
+ /// Initializes a new instance of the class with the agent name and an inner exception.
+ ///
+ /// The name of the agent that was not registered.
+ /// The exception that is the cause of the current exception.
+ public AgentNotRegisteredException(string agentName, Exception? innerException)
+ : base(GetMessage(agentName), innerException)
+ {
+ this.AgentName = agentName;
+ }
+
+ ///
+ /// Gets the name of the agent that was not registered.
+ ///
+ 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.";
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
index 52907c5816..1a117aff14 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAIAgent.cs
@@ -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
/// Optional run options.
/// The cancellation token.
/// The response from the agent.
+ /// Thrown when the agent has not been registered.
+ /// Thrown when the provided thread is not valid for a durable agent.
+ /// Thrown when cancellation is requested (cancellation is not supported for durable agents).
public override async Task RunAsync(
IEnumerable messages,
AgentThread? thread = null,
@@ -95,7 +99,17 @@ public sealed class DurableAIAgent : AIAgent
}
RunRequest request = new([.. messages], responseFormat, enableToolCalls, enableToolNames);
- return await this._context.Entities.CallEntityAsync(durableThread.SessionId, nameof(AgentEntity.RunAgentAsync), request);
+ try
+ {
+ return await this._context.Entities.CallEntityAsync(
+ durableThread.SessionId,
+ nameof(AgentEntity.RunAgentAsync),
+ request);
+ }
+ catch (EntityOperationFailedException e) when (e.FailureDetails.ErrorType == "EntityTaskNotFound")
+ {
+ throw new AgentNotRegisteredException(this._agentName, e);
+ }
}
///
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
index c611206d6a..2f435e0541 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs
@@ -80,7 +80,7 @@ public static class ServiceCollectionExtensions
DurableAgentsOptions options = new();
configure(options);
- var agents = options.GetAgentFactories();
+ IReadOnlyDictionary> 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;
}
+ ///
+ /// Validates that an agent with the specified name has been registered.
+ ///
+ /// The service provider.
+ /// The name of the agent to validate.
+ ///
+ /// Thrown when the agent dictionary is not registered in the service provider.
+ ///
+ ///
+ /// Thrown when the agent with the specified name has not been registered.
+ ///
+ internal static void ValidateAgentIsRegistered(IServiceProvider services, string agentName)
+ {
+ IReadOnlyDictionary>? agents =
+ services.GetService>>()
+ ?? 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.
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs
index 7628a3f3bf..0977d756cb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableTaskClientExtensions.cs
@@ -21,6 +21,12 @@ public static class DurableTaskClientExtensions
/// A durable agent proxy.
/// Thrown when or is null.
/// Thrown when is null or empty.
+ ///
+ /// Thrown when durable agents have not been configured on the service collection.
+ ///
+ ///
+ /// Thrown when the agent has not been registered.
+ ///
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(
context.InstanceServices,
durableClient);
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs
index 241e05a843..ad57ea9a52 100644
--- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ExternalClientTests.cs
@@ -212,4 +212,26 @@ public sealed class ExternalClientTests(ITestOutputHelper outputHelper) : IDispo
Assert.NotEmpty(response.Text);
Assert.Contains("John Doe", response.Text);
}
+
+ [Fact]
+ public void AsDurableAgentProxy_ThrowsWhenAgentNotRegistered()
+ {
+ // Setup: Register one agent but try to use a different one
+ AIAgent registeredAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent(
+ instructions: "You are a helpful assistant.",
+ name: "RegisteredAgent");
+
+ using TestHelper testHelper = TestHelper.Start([registeredAgent], this._outputHelper);
+
+ // Create an agent with a different name that isn't registered
+ AIAgent unregisteredAgent = TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent(
+ instructions: "You are a helpful assistant.",
+ name: "UnregisteredAgent");
+
+ // Act & Assert: Should throw AgentNotRegisteredException
+ AgentNotRegisteredException exception = Assert.Throws(
+ () => unregisteredAgent.AsDurableAgentProxy(testHelper.Services));
+
+ Assert.Equal("UnregisteredAgent", exception.AgentName);
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs
new file mode 100644
index 0000000000..6b905f2623
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/OrchestrationTests.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using System.Reflection;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using OpenAI;
+using Xunit.Abstractions;
+
+namespace Microsoft.Agents.AI.DurableTask.IntegrationTests;
+
+///
+/// Tests for orchestration execution scenarios with Durable Task Agents.
+///
+[Collection("Sequential")]
+[Trait("Category", "Integration")]
+public sealed class OrchestrationTests(ITestOutputHelper outputHelper) : IDisposable
+{
+ private static readonly TimeSpan s_defaultTimeout = Debugger.IsAttached
+ ? TimeSpan.FromMinutes(5)
+ : TimeSpan.FromSeconds(30);
+
+ private static readonly IConfiguration s_configuration =
+ new ConfigurationBuilder()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .AddEnvironmentVariables()
+ .Build();
+
+ private readonly ITestOutputHelper _outputHelper = outputHelper;
+ private readonly CancellationTokenSource _cts = new(delay: s_defaultTimeout);
+
+ private CancellationToken TestTimeoutToken => this._cts.Token;
+
+ public void Dispose() => this._cts.Dispose();
+
+ [Fact]
+ public async Task GetAgent_ThrowsWhenAgentNotRegisteredAsync()
+ {
+ // Define an orchestration that tries to use an unregistered agent
+ static async Task TestOrchestrationAsync(TaskOrchestrationContext context)
+ {
+ // Get an agent that hasn't been registered
+ DurableAIAgent agent = context.GetAgent("NonExistentAgent");
+
+ // This should throw when RunAsync is called because the agent doesn't exist
+ await agent.RunAsync("Hello");
+ return "Should not reach here";
+ }
+
+ // Setup: Create test helper without registering "NonExistentAgent"
+ using TestHelper testHelper = TestHelper.Start(
+ this._outputHelper,
+ configureAgents: agents =>
+ {
+ // Register a different agent, but not "NonExistentAgent"
+ agents.AddAIAgentFactory(
+ "OtherAgent",
+ sp => TestHelper.GetAzureOpenAIChatClient(s_configuration).CreateAIAgent(
+ name: "OtherAgent",
+ instructions: "You are a test agent."));
+ },
+ durableTaskRegistry: registry =>
+ registry.AddOrchestratorFunc(
+ name: nameof(TestOrchestrationAsync),
+ orchestrator: TestOrchestrationAsync));
+
+ DurableTaskClient client = testHelper.GetClient();
+
+ // Act: Start the orchestration
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
+ orchestratorName: nameof(TestOrchestrationAsync),
+ cancellation: this.TestTimeoutToken);
+
+ // Wait for the orchestration to complete and check for failure
+ OrchestrationMetadata status = await client.WaitForInstanceCompletionAsync(
+ instanceId,
+ getInputsAndOutputs: true,
+ this.TestTimeoutToken);
+
+ // Assert: Verify the orchestration failed with the expected exception
+ Assert.NotNull(status);
+ Assert.Equal(OrchestrationRuntimeStatus.Failed, status.RuntimeStatus);
+ Assert.NotNull(status.FailureDetails);
+
+ // Verify the exception type is AgentNotRegisteredException
+ Assert.True(
+ status.FailureDetails.ErrorType == typeof(AgentNotRegisteredException).FullName,
+ $"Expected AgentNotRegisteredException but got ErrorType: {status.FailureDetails.ErrorType}, Message: {status.FailureDetails.ErrorMessage}");
+
+ // Verify the exception message contains the agent name
+ Assert.Contains("NonExistentAgent", status.FailureDetails.ErrorMessage, StringComparison.OrdinalIgnoreCase);
+ }
+}