diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
index 1cd1b2bc94..ba57492eda 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
@@ -182,7 +182,13 @@ protected override void Visit(Foreach item)
this.Trace(item);
// Entry point for loop
- ForeachExecutor action = new(item, this._workflowState);
+ ForeachExecutor action = new(item, this._workflowState, this._workflowOptions);
+ if (action.IsParallel)
+ {
+ this.ContinueWith(action);
+ return;
+ }
+
string loopId = ForeachExecutor.Steps.Next(action.Id);
this.ContinueWith(action, condition: null, CompletionHandler);
// Transition to select the next item
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs
index 4be8bb8892..7b9d36b969 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowElementWalker.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.ObjectModel;
namespace Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
@@ -18,6 +19,11 @@ public override bool DefaultVisit(BotElement definition)
if (definition is DialogAction action)
{
action.Accept(this._visitor);
+
+ if (action is Foreach foreachAction && ForeachExecutionOptions.Parse(foreachAction).IsParallel)
+ {
+ return false;
+ }
}
return true;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutionOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutionOptions.cs
new file mode 100644
index 0000000000..401139adf0
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutionOptions.cs
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Globalization;
+using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
+using Microsoft.Agents.ObjectModel;
+
+namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
+
+internal enum ForeachExecutionMode
+{
+ Sequential,
+ Parallel,
+}
+
+///
+/// Strongly typed adapter for Foreach execution fields preserved by the external ObjectModel package.
+///
+///
+/// Once exposes generated properties for these fields, only this adapter needs to change.
+///
+internal sealed record ForeachExecutionOptions(
+ ForeachExecutionMode Mode,
+ int MaxParallelism,
+ TimeSpan? IterationTimeout)
+{
+ internal const string ModePropertyName = "mode";
+ internal const string MaxParallelismPropertyName = "maxParallelism";
+ internal const string TimeoutPropertyName = "timeoutInMilliseconds";
+
+ private const int DefaultMaxParallelism = 4;
+
+ public bool IsParallel => this.Mode == ForeachExecutionMode.Parallel;
+
+ public static ForeachExecutionOptions Parse(Foreach model)
+ {
+ DataValue? modeValue = GetExtensionValue(model, ModePropertyName);
+ DataValue? maxParallelismValue = GetExtensionValue(model, MaxParallelismPropertyName);
+ DataValue? timeoutValue = GetExtensionValue(model, TimeoutPropertyName);
+
+ ForeachExecutionMode mode = ParseMode(model, modeValue);
+ int maxParallelism = ParseInteger(model, MaxParallelismPropertyName, maxParallelismValue) ?? DefaultMaxParallelism;
+ int? timeoutMilliseconds = ParseInteger(model, TimeoutPropertyName, timeoutValue);
+
+ if (mode == ForeachExecutionMode.Sequential && (maxParallelismValue is not null || timeoutValue is not null))
+ {
+ throw InvalidConfiguration(model, $"'{MaxParallelismPropertyName}' and '{TimeoutPropertyName}' require '{ModePropertyName}: Parallel'.");
+ }
+
+ if (maxParallelism <= 0)
+ {
+ throw InvalidConfiguration(model, $"'{MaxParallelismPropertyName}' must be greater than zero.");
+ }
+
+ if (timeoutMilliseconds <= 0)
+ {
+ throw InvalidConfiguration(model, $"'{TimeoutPropertyName}' must be greater than zero when specified.");
+ }
+
+ return new(mode, maxParallelism, timeoutMilliseconds.HasValue ? TimeSpan.FromMilliseconds(timeoutMilliseconds.Value) : null);
+ }
+
+ private static ForeachExecutionMode ParseMode(Foreach model, DataValue? value)
+ {
+ if (value is null)
+ {
+ return ForeachExecutionMode.Sequential;
+ }
+
+ if (value is not StringDataValue stringValue)
+ {
+ throw InvalidConfiguration(model, $"'{ModePropertyName}' must be 'Sequential' or 'Parallel'.");
+ }
+
+ if (string.Equals(stringValue.Value, nameof(ForeachExecutionMode.Sequential), StringComparison.OrdinalIgnoreCase))
+ {
+ return ForeachExecutionMode.Sequential;
+ }
+
+ if (string.Equals(stringValue.Value, nameof(ForeachExecutionMode.Parallel), StringComparison.OrdinalIgnoreCase))
+ {
+ return ForeachExecutionMode.Parallel;
+ }
+
+ throw InvalidConfiguration(model, $"'{ModePropertyName}' must be 'Sequential' or 'Parallel'.");
+ }
+
+ private static int? ParseInteger(Foreach model, string propertyName, DataValue? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ object? rawValue = value.ToFormula().ToObject();
+ if (rawValue is not (byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal))
+ {
+ throw InvalidConfiguration(model, $"'{propertyName}' must be an integer.");
+ }
+
+ decimal decimalValue;
+ try
+ {
+ decimalValue = Convert.ToDecimal(rawValue, CultureInfo.InvariantCulture);
+ }
+ catch (Exception exception) when (exception is FormatException or InvalidCastException or OverflowException)
+ {
+ throw InvalidConfiguration(model, $"'{propertyName}' must be an integer.", exception);
+ }
+
+ if (decimalValue != decimal.Truncate(decimalValue) || decimalValue < int.MinValue || decimalValue > int.MaxValue)
+ {
+ throw InvalidConfiguration(model, $"'{propertyName}' must be an integer.");
+ }
+
+ return decimal.ToInt32(decimalValue);
+ }
+
+ private static DataValue? GetExtensionValue(Foreach model, string propertyName) =>
+ model.ExtensionData?.Properties.TryGetValue(propertyName, out DataValue? value) is true ? value : null;
+
+ private static DeclarativeModelException InvalidConfiguration(Foreach model, string message, Exception? innerException = null) =>
+ new($"Invalid Foreach configuration for '{model.Id.Value}': {message}", innerException);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
index f154ad7f97..db85cbed6e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -29,36 +30,177 @@ public static class Steps
private int _index;
private FormulaValue[] _values;
+ private readonly ForeachExecutionOptions _executionOptions;
+ private readonly DeclarativeWorkflowOptions? _workflowOptions;
+ private readonly WorkflowFormulaState _workflowState;
- public ForeachExecutor(Foreach model, WorkflowFormulaState state)
+ public ForeachExecutor(Foreach model, WorkflowFormulaState state, DeclarativeWorkflowOptions? workflowOptions = null)
: base(model, state)
{
this._values = [];
+ this._executionOptions = ForeachExecutionOptions.Parse(model);
+ this._workflowOptions = workflowOptions;
+ this._workflowState = state;
+
+ if (this._executionOptions.IsParallel)
+ {
+ if (workflowOptions is null)
+ {
+ throw new DeclarativeModelException($"Parallel Foreach '{model.Id.Value}' requires workflow execution options.");
+ }
+
+ ParallelForeachIterationRunner.ValidateBody(model);
+ }
}
public bool HasValue { get; private set; }
- protected override bool IsDiscreteAction => false;
+ public bool IsParallel => this._executionOptions.IsParallel;
+
+ protected override bool IsDiscreteAction => this.IsParallel;
protected override async ValueTask