// 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);
}
}