// Copyright (c) Microsoft. All rights reserved. // ConfigureAwait Usage in Orchestration Code: // This file uses ConfigureAwait(true) because it runs within orchestration context. // Durable Task orchestrations require deterministic replay - the same code must execute // identically across replays. ConfigureAwait(true) ensures continuations run on the // orchestration's synchronization context, which is essential for replay correctness. // Using ConfigureAwait(false) here could cause non-deterministic behavior during replay. using System.Text.Json; using Microsoft.Agents.AI.Workflows; using Microsoft.DurableTask; using Microsoft.Extensions.Logging; namespace Microsoft.Agents.AI.DurableTask.Workflows; /// /// Dispatches workflow executors to activities, AI agents, sub-orchestrations, or external events (human-in-the-loop). /// /// /// Called during the dispatch phase of each superstep by /// DurableWorkflowRunner.DispatchExecutorsInParallelAsync. For each executor that has /// pending input, this dispatcher determines whether the executor is an AI agent (stateful, /// backed by Durable Entities), a request port (human-in-the-loop, backed by external events), /// a sub-workflow (dispatched as a sub-orchestration), or a regular activity, and invokes the /// appropriate Durable Task API. /// The serialised string result is returned to the runner for the routing phase. /// internal static class DurableExecutorDispatcher { /// /// Dispatches an executor based on its type (activity, AI agent, request port, or sub-workflow). /// /// The task orchestration context. /// Information about the executor to dispatch. /// The message envelope containing input and type information. /// The shared state dictionary to pass to the executor. /// The live workflow status used to publish events and pending request port state. /// The logger for tracing. /// The result from the executor. internal static async Task DispatchAsync( TaskOrchestrationContext context, WorkflowExecutorInfo executorInfo, DurableMessageEnvelope envelope, Dictionary sharedState, DurableWorkflowLiveStatus liveStatus, ILogger logger) { logger.LogDispatchingExecutor(executorInfo.ExecutorId, executorInfo.IsAgenticExecutor); if (executorInfo.IsRequestPortExecutor) { return await ExecuteRequestPortAsync(context, executorInfo, envelope.Message, liveStatus, logger).ConfigureAwait(true); } if (executorInfo.IsAgenticExecutor) { return await ExecuteAgentAsync(context, executorInfo, logger, envelope.Message).ConfigureAwait(true); } if (executorInfo.IsSubworkflowExecutor) { return await ExecuteSubWorkflowAsync(context, executorInfo, envelope.Message).ConfigureAwait(true); } return await ExecuteActivityAsync(context, executorInfo, envelope.Message, envelope.InputTypeName, sharedState).ConfigureAwait(true); } private static async Task ExecuteActivityAsync( TaskOrchestrationContext context, WorkflowExecutorInfo executorInfo, string input, string? inputTypeName, Dictionary sharedState) { string executorName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId); string activityName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName); DurableActivityInput activityInput = new() { Input = input, InputTypeName = inputTypeName, State = sharedState }; string serializedInput = JsonSerializer.Serialize(activityInput, DurableWorkflowJsonContext.Default.DurableActivityInput); return await context.CallActivityAsync(activityName, serializedInput).ConfigureAwait(true); } /// /// Executes a request port executor by waiting for an external event (human-in-the-loop). /// /// /// When the workflow reaches a executor, the orchestration publishes /// the pending request to and waits for an external actor /// (e.g., a UI or API) to raise the corresponding event via /// . /// Multiple RequestPorts may be dispatched in parallel during a fan-out superstep. /// Each adds its pending request to . /// The wait has no built-in timeout; for time-limited approvals, callers can combine /// context.CreateTimer with Task.WhenAny in a wrapper executor. /// private static async Task ExecuteRequestPortAsync( TaskOrchestrationContext context, WorkflowExecutorInfo executorInfo, string input, DurableWorkflowLiveStatus liveStatus, ILogger logger) { RequestPort requestPort = executorInfo.RequestPort!; string eventName = requestPort.Id; logger.LogWaitingForExternalEvent(eventName); // Publish pending request so external clients can discover what input is needed liveStatus.PendingEvents.Add(new PendingRequestPortStatus(EventName: eventName, Input: input)); context.SetCustomStatus(liveStatus); // Wait until the external actor raises the event string response = await context.WaitForExternalEvent(eventName).ConfigureAwait(true); // Remove this pending request after receiving the response liveStatus.PendingEvents.RemoveAll(p => p.EventName == eventName); context.SetCustomStatus(liveStatus.Events.Count > 0 || liveStatus.PendingEvents.Count > 0 ? liveStatus : null); logger.LogReceivedExternalEvent(eventName); return response; } /// /// Executes an AI agent executor through Durable Entities. /// /// /// AI agents are stateful and maintain conversation history. They use Durable Entities /// to persist state across orchestration replays. /// private static async Task ExecuteAgentAsync( TaskOrchestrationContext context, WorkflowExecutorInfo executorInfo, ILogger logger, string input) { string agentName = WorkflowNamingHelper.GetExecutorName(executorInfo.ExecutorId); DurableAIAgent agent = context.GetAgent(agentName); if (agent is null) { logger.LogAgentNotFound(agentName); return $"Agent '{agentName}' not found"; } AgentSession session = await agent.CreateSessionAsync().ConfigureAwait(true); AgentResponse response = await agent.RunAsync(input, session).ConfigureAwait(true); return response.Text; } /// /// Dispatches a sub-workflow executor as a sub-orchestration. /// /// /// Sub-workflows run as separate orchestration instances, providing independent /// checkpointing, replay, and hierarchical visualization in the DTS dashboard. /// The input is wrapped in so the sub-orchestration /// can extract it using the same envelope structure. The sub-orchestration returns a /// directly (deserialized by the Durable Task SDK), /// which this method converts to a so the parent /// workflow's result processing picks up both the result and any accumulated events. /// private static async Task ExecuteSubWorkflowAsync( TaskOrchestrationContext context, WorkflowExecutorInfo executorInfo, string input) { string orchestrationName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorInfo.SubWorkflow!.Name!); DurableWorkflowInput workflowInput = new() { Input = input }; DurableWorkflowResult? workflowResult = await context.CallSubOrchestratorAsync( orchestrationName, workflowInput).ConfigureAwait(true); return ConvertWorkflowResultToExecutorOutput(workflowResult); } /// /// Converts a from a sub-orchestration /// into a JSON string. This bridges the sub-workflow's /// output format to the parent workflow's result processing, preserving both the result /// and any accumulated events from the sub-workflow. /// private static string ConvertWorkflowResultToExecutorOutput(DurableWorkflowResult? workflowResult) { if (workflowResult is null) { return string.Empty; } // Propagate the result, events, and sent messages from the sub-workflow. // SentMessages carry the sub-workflow's output for typed routing in the parent, // matching the in-process WorkflowHostExecutor behavior. // Shared state is not included because each workflow instance maintains its own // independent shared state; it is not shared between parent and sub-workflows. DurableExecutorOutput executorOutput = new() { Result = workflowResult.Result, Events = workflowResult.Events ?? [], SentMessages = workflowResult.SentMessages ?? [], HaltRequested = workflowResult.HaltRequested, }; return JsonSerializer.Serialize(executorOutput, DurableWorkflowJsonContext.Default.DurableExecutorOutput); } }