// Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.Workflows; using Microsoft.Agents.AI.Workflows.Checkpointing; namespace Microsoft.Agents.AI.DurableTask; /// /// Represents an executor in the workflow with its metadata. /// /// The unique identifier of the executor. /// Indicates whether this executor is an agentic executor. /// The request port if this executor is a request port executor; otherwise, null. public sealed record WorkflowExecutorInfo(string ExecutorId, bool IsAgenticExecutor, RequestPort? RequestPort = null) { /// /// Gets a value indicating whether this executor is a request port executor (human-in-the-loop). /// public bool IsRequestPortExecutor => this.RequestPort is not null; } /// /// Represents a level of executors that can be executed in parallel (Fan-Out). /// All executors in the same level have their dependencies satisfied by previous levels. /// /// The level number (0-based, starting from the root executor). /// The executors that can run in parallel at this level. /// Indicates if this level is a Fan-In point (has executors with multiple predecessors). public sealed record WorkflowExecutionLevel(int Level, List Executors, bool IsFanIn); /// /// Provides helper methods for analyzing and executing workflows. /// public static class WorkflowHelper { /// /// Accepts a workflow instance and returns a list of executors with metadata in the order they should be executed. /// /// The workflow instance to analyze. /// A list of executor information in topological order (execution order). public static List GetExecutorsFromWorkflowInOrder(Workflow workflow) { WorkflowExecutionPlan plan = GetExecutionPlan(workflow); // Flatten the levels into a single list for backward compatibility List result = []; foreach (WorkflowExecutionLevel level in plan.Levels) { result.AddRange(level.Executors); } return result; } /// /// Analyzes the workflow and returns an execution plan that supports Fan-Out/Fan-In patterns. /// Executors at the same level can be executed in parallel (Fan-Out). /// Fan-In points are identified where multiple executors converge. /// /// The workflow instance to analyze. /// An execution plan with parallel execution levels. public static WorkflowExecutionPlan GetExecutionPlan(Workflow workflow) { ArgumentNullException.ThrowIfNull(workflow); Dictionary executors = workflow.ReflectExecutors(); Dictionary> edges = workflow.ReflectEdges(); Dictionary<(string SourceId, string TargetId), Func?> edgeConditions = workflow.GetEdgeConditions(); WorkflowExecutionPlan plan = new(); // Build adjacency lists (successors and predecessors) Dictionary> successors = new(executors.Count); Dictionary> predecessors = new(executors.Count); int[] inDegree = new int[executors.Count]; Dictionary executorIndex = new(executors.Count); // Initialize all executors and extract their output types int index = 0; foreach (KeyValuePair executor in executors) { executorIndex[executor.Key] = index++; successors[executor.Key] = []; predecessors[executor.Key] = []; // Extract output type from executor type (e.g., Executor -> TOutput) plan.ExecutorOutputTypes[executor.Key] = GetExecutorOutputType(executor.Value.ExecutorType); } // Build the graph from edges foreach (KeyValuePair> edgeGroup in edges) { string sourceId = edgeGroup.Key; List sourceSuccessors = successors[sourceId]; foreach (EdgeInfo edge in edgeGroup.Value) { foreach (string sinkId in edge.Connection.SinkIds) { if (executorIndex.TryGetValue(sinkId, out int sinkIdx)) { sourceSuccessors.Add(sinkId); predecessors[sinkId].Add(sourceId); inDegree[sinkIdx]++; } } } } // Store edge conditions in the plan foreach (KeyValuePair<(string SourceId, string TargetId), Func?> condition in edgeConditions) { plan.EdgeConditions[condition.Key] = condition.Value; } // Store the graph structure in the plan (reuse the built lists directly) foreach (string executorId in executors.Keys) { plan.Predecessors[executorId] = predecessors[executorId]; plan.Successors[executorId] = successors[executorId]; } // Build execution levels using queue-based Kahn's algorithm // Process all nodes with in-degree 0 at once (same level) for parallel execution Queue currentLevel = new(); foreach (KeyValuePair kvp in executorIndex) { if (inDegree[kvp.Value] == 0) { currentLevel.Enqueue(kvp.Key); } } int levelNumber = 0; int processedCount = 0; while (currentLevel.Count > 0) { List levelExecutors = new(currentLevel.Count); Queue nextLevel = new(); bool isFanIn = false; while (currentLevel.Count > 0) { string executorId = currentLevel.Dequeue(); processedCount++; ExecutorBinding executorBinding = executors[executorId]; bool isAgentic = IsAgentExecutorType(executorBinding.ExecutorType); RequestPort? requestPort = (executorBinding is RequestPortBinding rpb) ? rpb.Port : null; levelExecutors.Add(new WorkflowExecutorInfo(executorId, isAgentic, requestPort)); // Check Fan-In for this executor if (predecessors[executorId].Count > 1) { isFanIn = true; } // Decrement in-degree of all successors and enqueue those ready for next level foreach (string successor in successors[executorId]) { int successorIdx = executorIndex[successor]; if (--inDegree[successorIdx] == 0) { nextLevel.Enqueue(successor); } } } plan.Levels.Add(new WorkflowExecutionLevel(levelNumber, levelExecutors, isFanIn)); levelNumber++; currentLevel = nextLevel; } // Handle cycle detection: if not all executors were processed, there's a cycle if (processedCount < executors.Count) { List remainingExecutors = []; foreach (KeyValuePair executor in executors) { if (inDegree[executorIndex[executor.Key]] > 0) { bool isAgentic = IsAgentExecutorType(executor.Value.ExecutorType); RequestPort? requestPort = (executor.Value is RequestPortBinding rpb) ? rpb.Port : null; remainingExecutors.Add(new WorkflowExecutorInfo(executor.Key, isAgentic, requestPort)); } } if (remainingExecutors.Count > 0) { bool isFanIn = remainingExecutors.Exists(e => predecessors[e.ExecutorId].Count > 1); plan.Levels.Add(new WorkflowExecutionLevel(levelNumber, remainingExecutors, isFanIn)); } } return plan; } /// /// Determines whether the specified executor type is an agentic executor. /// /// The executor type to check. /// true if the executor is an agentic executor; otherwise, false. 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.FullName ?? executorType.Name; string assemblyName = executorType.Assembly.GetName().Name ?? string.Empty; return typeName.Contains("AIAgentHostExecutor", StringComparison.OrdinalIgnoreCase) && assemblyName.Contains("Microsoft.Agents.AI", StringComparison.OrdinalIgnoreCase); } /// /// Extracts the output type from an executor type. /// For Executor<TInput, TOutput>, returns TOutput. /// For Executor<TInput>, returns null (void output). /// /// The executor type to analyze. /// The output type, or null if the executor has no typed output. private static Type? GetExecutorOutputType(Type executorType) { // Walk up the inheritance chain to find Executor or Executor Type? currentType = executorType; while (currentType is not null) { if (currentType.IsGenericType) { Type genericDefinition = currentType.GetGenericTypeDefinition(); Type[] genericArgs = currentType.GetGenericArguments(); // Check for Executor (2 type parameters) if (genericArgs.Length == 2 && genericDefinition.Name.StartsWith("Executor", StringComparison.Ordinal)) { return genericArgs[1]; // TOutput } // Check for Executor (1 type parameter) - void return if (genericArgs.Length == 1 && genericDefinition.Name.StartsWith("Executor", StringComparison.Ordinal)) { return null; } } currentType = currentType.BaseType; } return null; } }