mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Ensure duplicate agent registrations are properly handled.
This commit is contained in:
+9
-21
@@ -6,7 +6,8 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to agent-specific options for functions agents by name.
|
||||
/// Returns default options (HTTP trigger enabled, MCP tool disabled) when no explicit options were configured.
|
||||
/// Returns <see langword="false"/> when no explicit options have been configured for an agent,
|
||||
/// which distinguishes standalone agents from those auto-registered by workflows.
|
||||
/// </summary>
|
||||
internal sealed class DefaultFunctionsAgentOptionsProvider(IReadOnlyDictionary<string, FunctionsAgentOptions> functionsAgentOptions)
|
||||
: IFunctionsAgentOptionsProvider
|
||||
@@ -14,32 +15,19 @@ internal sealed class DefaultFunctionsAgentOptionsProvider(IReadOnlyDictionary<s
|
||||
private readonly IReadOnlyDictionary<string, FunctionsAgentOptions> _functionsAgentOptions =
|
||||
functionsAgentOptions ?? throw new ArgumentNullException(nameof(functionsAgentOptions));
|
||||
|
||||
// Default options. HTTP trigger enabled, MCP tool disabled.
|
||||
private static readonly FunctionsAgentOptions s_defaultOptions = new()
|
||||
{
|
||||
HttpTrigger = { IsEnabled = true },
|
||||
McpToolTrigger = { IsEnabled = false }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the options associated with the specified agent name.
|
||||
/// If not found, a default options instance (with HTTP trigger enabled) is returned.
|
||||
/// Returns <see langword="false"/> when no options have been explicitly configured for the agent.
|
||||
/// </summary>
|
||||
/// <param name="agentName">The name of the agent whose options are to be retrieved. Cannot be null or empty.</param>
|
||||
/// <param name="options">The options for the specified agent. Will never be null.</param>
|
||||
/// <returns>Always true. Returns configured options if present; otherwise default fallback options.</returns>
|
||||
/// <param name="options">
|
||||
/// When this method returns <see langword="true"/>, contains the options for the specified agent;
|
||||
/// otherwise, <see langword="null"/>.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if options were found for the agent; otherwise, <see langword="false"/>.</returns>
|
||||
public bool TryGet(string agentName, [NotNullWhen(true)] out FunctionsAgentOptions? options)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(agentName);
|
||||
|
||||
if (this._functionsAgentOptions.TryGetValue(agentName, out FunctionsAgentOptions? existing))
|
||||
{
|
||||
options = existing;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not defined, return default options.
|
||||
options = s_defaultOptions;
|
||||
return true;
|
||||
return this._functionsAgentOptions.TryGetValue(agentName, out options);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-15
@@ -6,9 +6,13 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms function metadata by registering durable agent functions for each configured agent.
|
||||
/// Transforms function metadata by registering durable agent functions for each explicitly configured agent.
|
||||
/// </summary>
|
||||
/// <remarks>This transformer adds both entity trigger and HTTP trigger functions for every agent registered in the application.</remarks>
|
||||
/// <remarks>
|
||||
/// This transformer adds entity, HTTP, and MCP tool trigger functions for agents that have
|
||||
/// explicit <see cref="FunctionsAgentOptions"/>. Agents auto-registered by workflows
|
||||
/// (which lack explicit options) are handled by <see cref="DurableWorkflowsFunctionMetadataTransformer"/>.
|
||||
/// </remarks>
|
||||
internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadataTransformer
|
||||
{
|
||||
private readonly ILogger<DurableAgentFunctionMetadataTransformer> _logger;
|
||||
@@ -38,24 +42,27 @@ internal sealed class DurableAgentFunctionMetadataTransformer : IFunctionMetadat
|
||||
{
|
||||
string agentName = kvp.Key;
|
||||
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "entity");
|
||||
// Only generate triggers for agents with explicit Functions agent options.
|
||||
// Agents auto-registered by workflows are handled by DurableWorkflowsFunctionMetadataTransformer.
|
||||
if (!this._functionsAgentOptionsProvider.TryGet(agentName, out FunctionsAgentOptions? agentTriggerOptions))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "entity");
|
||||
original.Add(FunctionMetadataFactory.CreateEntityTrigger(agentName));
|
||||
|
||||
if (this._functionsAgentOptionsProvider.TryGet(agentName, out FunctionsAgentOptions? agentTriggerOptions))
|
||||
if (agentTriggerOptions.HttpTrigger.IsEnabled)
|
||||
{
|
||||
if (agentTriggerOptions.HttpTrigger.IsEnabled)
|
||||
{
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "http");
|
||||
original.Add(FunctionMetadataFactory.CreateHttpTrigger(agentName, $"agents/{agentName}/run", BuiltInFunctions.RunAgentHttpFunctionEntryPoint));
|
||||
}
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "http");
|
||||
original.Add(FunctionMetadataFactory.CreateHttpTrigger(agentName, $"agents/{agentName}/run", BuiltInFunctions.RunAgentHttpFunctionEntryPoint));
|
||||
}
|
||||
|
||||
if (agentTriggerOptions.McpToolTrigger.IsEnabled)
|
||||
{
|
||||
AIAgent agent = kvp.Value(this._serviceProvider);
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "mcpTool");
|
||||
original.Add(CreateMcpToolTrigger(agentName, agent.Description));
|
||||
}
|
||||
if (agentTriggerOptions.McpToolTrigger.IsEnabled)
|
||||
{
|
||||
AIAgent agent = kvp.Value(this._serviceProvider);
|
||||
this._logger.LogRegisteringTriggerForAgent(agentName, "mcpTool");
|
||||
original.Add(CreateMcpToolTrigger(agentName, agent.Description));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -134,4 +134,17 @@ public static class DurableAgentsOptionsExtensions
|
||||
{
|
||||
return new Dictionary<string, FunctionsAgentOptions>(s_agentOptions, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures every agent in <paramref name="agentNames"/> has an entry in the
|
||||
/// options registry. Agents that already have explicit options are left untouched.
|
||||
/// New entries receive the default configuration (HTTP trigger enabled, MCP tool disabled).
|
||||
/// </summary>
|
||||
internal static void EnsureDefaultOptionsForAll(IEnumerable<string> agentNames)
|
||||
{
|
||||
foreach (string name in agentNames)
|
||||
{
|
||||
s_agentOptions.TryAdd(name, new FunctionsAgentOptions { HttpTrigger = { IsEnabled = true } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -27,9 +27,16 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// Create/get shared options BEFORE the DurableTask library call so it can find them.
|
||||
FunctionsDurableOptions sharedOptions = GetOrCreateSharedOptions(builder.Services);
|
||||
|
||||
// The main agent services registration is done in Microsoft.DurableTask.Agents.
|
||||
builder.Services.ConfigureDurableAgents(configure);
|
||||
|
||||
// Ensure all agents registered through this path have default FunctionsAgentOptions.
|
||||
// This distinguishes them from agents auto-registered by workflows.
|
||||
DurableAgentsOptionsExtensions.EnsureDefaultOptionsForAll(sharedOptions.Agents.GetAgentFactories().Keys);
|
||||
|
||||
builder.Services.TryAddSingleton<IFunctionsAgentOptionsProvider>(_ =>
|
||||
new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
|
||||
|
||||
@@ -67,7 +74,7 @@ public static class FunctionsApplicationBuilderExtensions
|
||||
|
||||
builder.Services.ConfigureDurableOptions(configure);
|
||||
|
||||
if (sharedOptions.Agents.GetAgentFactories().Count > 0)
|
||||
if (DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot().Count > 0)
|
||||
{
|
||||
builder.Services.TryAddSingleton<IFunctionsAgentOptionsProvider>(_ =>
|
||||
new DefaultFunctionsAgentOptionsProvider(DurableAgentsOptionsExtensions.GetAgentOptionsSnapshot()));
|
||||
|
||||
+5
-2
@@ -50,8 +50,11 @@ internal sealed class DurableWorkflowsFunctionMetadataTransformer : IFunctionMet
|
||||
int initialCount = original.Count;
|
||||
this._logger.LogTransformingFunctionMetadata(initialCount);
|
||||
|
||||
// Track registered function names to avoid duplicates when workflows share executors.
|
||||
HashSet<string> registeredFunctions = [];
|
||||
// Seed with existing function names to avoid duplicates across transformers
|
||||
// (e.g., when DurableAgentFunctionMetadataTransformer already registered entity triggers).
|
||||
HashSet<string> registeredFunctions = new(
|
||||
original.Select(f => f.Name!),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
DurableWorkflowOptions workflowOptions = this._options.Workflows;
|
||||
foreach (var workflow in workflowOptions.Workflows)
|
||||
|
||||
+39
@@ -148,6 +148,45 @@ public sealed class DurableAgentFunctionMetadataTransformerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transform_SkipsAgents_WithoutExplicitOptions()
|
||||
{
|
||||
// Arrange: two agents in the dictionary, but only one has explicit FunctionsAgentOptions.
|
||||
// This simulates a workflow-auto-registered agent (workflowAgent) alongside a standalone agent.
|
||||
Dictionary<string, Func<IServiceProvider, AIAgent>> agents = new()
|
||||
{
|
||||
{ "standaloneAgent", _ => new TestAgent("standaloneAgent", "Standalone agent") },
|
||||
{ "workflowAgent", _ => new TestAgent("workflowAgent", "Auto-registered by workflow") }
|
||||
};
|
||||
|
||||
FunctionsAgentOptions standaloneOptions = new();
|
||||
standaloneOptions.HttpTrigger.IsEnabled = true;
|
||||
|
||||
// Only standaloneAgent has explicit options; workflowAgent does not.
|
||||
IFunctionsAgentOptionsProvider agentOptionsProvider = new FakeOptionsProvider(new Dictionary<string, FunctionsAgentOptions>
|
||||
{
|
||||
{ "standaloneAgent", standaloneOptions }
|
||||
});
|
||||
|
||||
List<IFunctionMetadata> metadataList = [];
|
||||
|
||||
DurableAgentFunctionMetadataTransformer transformer = new(
|
||||
agents,
|
||||
NullLogger<DurableAgentFunctionMetadataTransformer>.Instance,
|
||||
new FakeServiceProvider(),
|
||||
agentOptionsProvider);
|
||||
|
||||
// Act
|
||||
transformer.Transform(metadataList);
|
||||
|
||||
// Assert: only standaloneAgent should have triggers (entity + http = 2).
|
||||
// workflowAgent should be skipped entirely.
|
||||
Assert.Equal(2, metadataList.Count);
|
||||
Assert.Contains(metadataList, m => m.Name == "dafx-standaloneAgent");
|
||||
Assert.Contains(metadataList, m => m.Name == "http-standaloneAgent");
|
||||
Assert.DoesNotContain(metadataList, m => m.Name!.Contains("workflowAgent"));
|
||||
}
|
||||
|
||||
private static List<IFunctionMetadata> BuildFunctionMetadataList(int numberOfFunctions)
|
||||
{
|
||||
List<IFunctionMetadata> list = [];
|
||||
|
||||
Reference in New Issue
Block a user