This commit is contained in:
Shyju Krishnankutty
2026-01-21 20:19:23 -08:00
Unverified
parent 61e37cdc6e
commit 63a2283033
6 changed files with 37 additions and 90 deletions
@@ -79,7 +79,7 @@ public sealed class DurableWorkflowOptions
private static void RegisterExecutors(Workflow workflow, ExecutorRegistry registry)
{
foreach (KeyValuePair<string, ExecutorInfo> executor in workflow.ReflectExecutors())
foreach (KeyValuePair<string, ExecutorBinding> executor in workflow.ReflectExecutors())
{
int underscoreIndex = executor.Key.IndexOf('_');
string executorName = underscoreIndex > 0 ? executor.Key[..underscoreIndex] : executor.Key;
@@ -89,9 +89,9 @@ public sealed class DurableWorkflowOptions
private static void RegisterAgenticExecutors(Workflow workflow, DurableAgentsOptions agentOptions)
{
foreach (AIAgent agent in workflow.EnumerateAgentExecutors())
foreach (KeyValuePair<string, ExecutorBinding> executor in workflow.ReflectExecutors())
{
if (agent.Name is not null && !agentOptions.ContainsAgent(agent.Name))
if (executor.Value.RawValue is AIAgent agent && agent.Name is not null && !agentOptions.ContainsAgent(agent.Name))
{
agentOptions.AddAIAgent(agent, workflowOnly: true);
}
@@ -39,7 +39,13 @@ internal sealed class ExecutorRegistry
ArgumentException.ThrowIfNullOrEmpty(executorId);
ArgumentNullException.ThrowIfNull(workflow);
this._executors.TryAdd(executorName, new ExecutorRegistration(executorId, workflow));
Dictionary<string, ExecutorBinding> bindings = workflow.ReflectExecutors();
if (!bindings.TryGetValue(executorId, out ExecutorBinding? binding))
{
throw new InvalidOperationException($"Executor '{executorId}' not found in workflow.");
}
this._executors.TryAdd(executorName, new ExecutorRegistration(executorId, binding));
}
}
@@ -47,8 +53,8 @@ internal sealed class ExecutorRegistry
/// Represents a registered executor with its associated workflow.
/// </summary>
/// <param name="ExecutorId">The full executor ID (may include GUID suffix).</param>
/// <param name="Workflow">The workflow containing the executor.</param>
internal sealed record ExecutorRegistration(string ExecutorId, Workflow Workflow)
/// <param name="Binding">The executor binding from the workflow.</param>
internal sealed record ExecutorRegistration(string ExecutorId, ExecutorBinding Binding)
{
/// <summary>
/// Creates an instance of the executor.
@@ -56,8 +62,13 @@ internal sealed record ExecutorRegistration(string ExecutorId, Workflow Workflow
/// <param name="runId">A unique identifier for the run context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The created executor instance.</returns>
public ValueTask<Executor> CreateExecutorInstanceAsync(string runId, CancellationToken cancellationToken = default)
public async ValueTask<Executor> CreateExecutorInstanceAsync(string runId, CancellationToken cancellationToken = default)
{
return this.Workflow.CreateExecutorInstanceAsync(this.ExecutorId, runId, cancellationToken);
if (this.Binding.FactoryAsync is null)
{
throw new InvalidOperationException($"Cannot create executor '{this.ExecutorId}': Binding is a placeholder.");
}
return await this.Binding.FactoryAsync(runId).ConfigureAwait(false);
}
}
@@ -87,7 +87,7 @@ public static class WorkflowHelper
{
ArgumentNullException.ThrowIfNull(workflow);
Dictionary<string, ExecutorInfo> executors = workflow.ReflectExecutors();
Dictionary<string, ExecutorBinding> executors = workflow.ReflectExecutors();
Dictionary<string, HashSet<EdgeInfo>> edges = workflow.ReflectEdges();
WorkflowExecutionPlan plan = new();
@@ -177,9 +177,9 @@ public static class WorkflowHelper
{
processed.Add(executorId);
if (executors.TryGetValue(executorId, out ExecutorInfo? executorInfo))
if (executors.TryGetValue(executorId, out ExecutorBinding? executorBinding))
{
bool isAgentic = IsAgentExecutorType(executorInfo.ExecutorType);
bool isAgentic = IsAgentExecutorType(executorBinding.ExecutorType);
levelExecutors.Add(new WorkflowExecutorInfo(executorId, isAgentic));
}
@@ -202,13 +202,13 @@ public static class WorkflowHelper
/// </summary>
/// <param name="executorType">The executor type to check.</param>
/// <returns><c>true</c> if the executor is an agentic executor; otherwise, <c>false</c>.</returns>
internal static bool IsAgentExecutorType(TypeId executorType)
internal static bool IsAgentExecutorType(Type executorType)
{
// hack for now. In the future, the MAF type could expose something which can help with this.
// Check if the type name or assembly indicates it's an agent executor
// This includes AgentRunStreamingExecutor, AgentExecutor, ChatClientAgent wrappers, etc.
string typeName = executorType.TypeName;
string assemblyName = executorType.AssemblyName;
string typeName = executorType.FullName ?? executorType.Name;
string assemblyName = executorType.Assembly.GetName().Name ?? string.Empty;
return typeName.Contains("AIAgentHostExecutor", StringComparison.OrdinalIgnoreCase) &&
assemblyName.Contains("Microsoft.Agents.AI", StringComparison.OrdinalIgnoreCase);
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.Extensions.Logging;
@@ -66,11 +66,11 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
}
}
Dictionary<string, ExecutorInfo> executorInfos = workflow.Value.ReflectExecutors();
Dictionary<string, ExecutorBinding> executorBindings = workflow.Value.ReflectExecutors();
foreach (string executorId in executorIds)
{
if (executorInfos.TryGetValue(executorId, out ExecutorInfo? executorInfo))
if (executorBindings.TryGetValue(executorId, out ExecutorBinding? executorBinding))
{
string executorName = WorkflowNamingHelper.GetExecutorName(executorId);
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
@@ -83,14 +83,14 @@ internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMeta
}
// Check if the executor type is an agent-related type
if (WorkflowHelper.IsAgentExecutorType(executorInfo.ExecutorType))
if (WorkflowHelper.IsAgentExecutorType(executorBinding.ExecutorType))
{
this._logger.LogAddingAgentEntityFunction(executorId, executorInfo.ExecutorType.TypeName, workflow.Key);
this._logger.LogAddingAgentEntityFunction(executorId, executorBinding.ExecutorType.FullName ?? executorBinding.ExecutorType.Name, workflow.Key);
//original.Add(CreateAgentTrigger(functionName));
}
else
{
this._logger.LogAddingActivityFunction(executorId, executorInfo.ExecutorType.TypeName, workflow.Key);
this._logger.LogAddingActivityFunction(executorId, executorBinding.ExecutorType.FullName ?? executorBinding.ExecutorType.Name, workflow.Key);
original.Add(CreateActivityTrigger(functionName));
}
}
@@ -2,36 +2,16 @@
namespace Microsoft.Agents.AI.Workflows.Checkpointing;
/// <summary>
/// Represents information about an executor in a workflow, including its type and identifier.
/// </summary>
/// <param name="ExecutorType">The type identifier of the executor.</param>
/// <param name="ExecutorId">The unique identifier of the executor instance.</param>
public sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId)
internal sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId)
{
/// <summary>
/// Determines whether this executor info matches a specific executor type by generic parameter.
/// </summary>
/// <typeparam name="T">The executor type to match against.</typeparam>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch<T>() where T : Executor =>
this.ExecutorType.IsMatch<T>()
&& this.ExecutorId == typeof(T).Name;
/// <summary>
/// Determines whether this executor info matches a given executor instance.
/// </summary>
/// <param name="executor">The executor instance to match against.</param>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch(Executor executor) =>
this.ExecutorType.IsMatch(executor.GetType())
&& this.ExecutorId == executor.Id;
/// <summary>
/// Determines whether this executor info matches a given executor binding.
/// </summary>
/// <param name="binding">The executor binding to match against.</param>
/// <returns><c>true</c> if the executor type and ID match; otherwise, <c>false</c>.</returns>
public bool IsMatch(ExecutorBinding binding) =>
this.ExecutorType.IsMatch(binding.ExecutorType)
&& this.ExecutorId == binding.Id;
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -37,15 +37,12 @@ public class Workflow
}
/// <summary>
/// Gets information about all executors in the workflow, keyed by their ID.
/// Gets all executor bindings in the workflow, keyed by their ID.
/// </summary>
/// <returns>A dictionary mapping executor IDs to their <see cref="ExecutorInfo"/>.</returns>
public Dictionary<string, ExecutorInfo> ReflectExecutors()
/// <returns>A dictionary mapping executor IDs to their <see cref="ExecutorBinding"/>.</returns>
public Dictionary<string, ExecutorBinding> ReflectExecutors()
{
return this.ExecutorBindings.Values.ToDictionary(
keySelector: binding => binding.Id,
elementSelector: RepresentationExtensions.ToExecutorInfo
);
return new Dictionary<string, ExecutorBinding>(this.ExecutorBindings);
}
internal Dictionary<string, RequestPort> Ports { get; init; } = [];
@@ -226,45 +223,4 @@ public class Workflow
return startExecutor.DescribeProtocol();
}
/// <summary>
/// Enumerates all AIAgent instances that are directly bound in this workflow.
/// </summary>
/// <remarks>
/// This method only returns agents that are directly bound as executor values.
/// It does not include agents created by factory functions, as those require
/// an IServiceProvider to instantiate.
/// </remarks>
/// <returns>An enumerable collection of AIAgent instances found in the workflow.</returns>
public IEnumerable<AIAgent> EnumerateAgentExecutors()
{
foreach (ExecutorBinding binding in this.ExecutorBindings.Values)
{
if (binding.RawValue is AIAgent agent)
{
yield return agent;
}
}
}
/// <summary>
/// Creates an instance of the specified executor.
/// </summary>
/// <param name="executorId">The identifier of the executor to create.</param>
/// <param name="runId">A unique identifier for the run context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>A <see cref="ValueTask{Executor}"/> representing the asynchronous operation.</returns>
/// <remarks>
/// This method is useful for Azure Functions scenarios where you need to create executor instances
/// outside of the normal workflow execution flow.
/// </remarks>
public async ValueTask<Executor> CreateExecutorInstanceAsync(string executorId, string runId, CancellationToken cancellationToken = default)
{
if (!this.ExecutorBindings.TryGetValue(executorId, out ExecutorBinding? binding))
{
throw new InvalidOperationException($"Executor '{executorId}' not found in workflow.");
}
return await binding.CreateInstanceAsync(runId).ConfigureAwait(false);
}
}