diff --git a/dotnet/src/Microsoft.Agents.Workflows/Config.cs b/dotnet/src/Microsoft.Agents.Workflows/Config.cs
new file mode 100644
index 0000000000..cdd2ce1534
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows/Config.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows;
+
+///
+/// Configuration for an object with a string identifier. For example, object.
+///
+/// A unique identifier for the configurable object.
+public class Config(string? id = null)
+{
+ ///
+ /// A unique identifier for the configurable object.
+ ///
+ ///
+ /// If not provided, the configured object will generate its own identifier.
+ ///
+ public string? Id => id;
+}
+
+///
+/// Configuration for an object with a string identifier and options of type .
+///
+/// The type of options for the configurable object.
+/// The options for the configurable object.
+/// A unique identifier for the configurable object.
+public class Config(TOptions? options = default, string? id = null) : Config(id)
+{
+ ///
+ /// Options for the configured object.
+ ///
+ public TOptions? Options => options;
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/ConfigurationExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows/ConfigurationExtensions.cs
new file mode 100644
index 0000000000..c3309116c6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows/ConfigurationExtensions.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows;
+
+///
+/// Extensions methods for creating Configured objects
+///
+public static class ConfigurationExtensions
+{
+ ///
+ /// Creates a new configuration that treats the subject as its base type, allowing configuration to be applied at
+ /// the parent type level.
+ ///
+ /// The type of the original subject being configured. Must inherit from or implement TParent.
+ /// The base type or interface to which the configuration will be upcast.
+ /// The existing configuration for the subject type to be upcast to its parent type. Cannot be null.
+ /// A new instance that applies the original configuration logic to the parent type.
+ public static Configured Super(this Configured configured) where TSubject : TParent
+ => new(async config => await configured.FactoryAsync(config).ConfigureAwait(false), configured.Id, configured.Raw);
+
+ ///
+ /// Creates a new configuration that treats the subject as its base type, allowing configuration to be applied at
+ /// the parent type level.
+ ///
+ /// The type of the original subject being configured. Must inherit from or implement TParent.
+ /// The base type or interface to which the configuration will be upcast.
+ /// The type of configuration options for the original subject being configured.
+ /// The existing configuration for the subject type to be upcast to its parent type. Cannot be null.
+ /// A new instance that applies the original configuration logic to the parent type.
+ public static Configured Super(this Configured configured) where TSubject : TParent
+ => configured.Memoize().Super();
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Configured.cs b/dotnet/src/Microsoft.Agents.Workflows/Configured.cs
new file mode 100644
index 0000000000..d88da9277f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows/Configured.cs
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.Workflows;
+
+///
+/// Helper methods for creating instances.
+///
+public static class Configured
+{
+ ///
+ /// Creates a instance from an existing subject instance.
+ ///
+ ///
+ /// The subject instance. If the subject implements , its ID will be used
+ /// and checked against the provided ID (if any).
+ ///
+ ///
+ /// A unique identifier for the configured subject. This is required if the subject does not implement
+ ///
+ ///
+ ///
+ /// The raw representation of the subject instance.
+ ///
+ ///
+ public static Configured FromInstance(TSubject subject, string? id = null, object? raw = null)
+ {
+ if (subject is IIdentified identified)
+ {
+ if (id != null && identified.Id != id)
+ {
+ throw new ArgumentException($"Provided ID '{id}' does not match subject's ID '{identified.Id}'.", nameof(id));
+ }
+
+ return new Configured((_) => new(subject), id: identified.Id, raw: raw ?? subject);
+ }
+
+ if (id == null)
+ {
+ throw new ArgumentNullException(nameof(id), "ID must be provided when the subject does not implement IIdentified.");
+ }
+
+ return new Configured((_) => new(subject), id, raw: raw ?? subject);
+ }
+}
+
+///
+/// A representation of a preconfigured, lazy-instantiatable instance of .
+///
+/// The type of the preconfigured subject.
+/// A factory to intantiate the subject when desired.
+/// The unique identifier for the configured subject.
+///
+public class Configured(Func> factoryAsync, string id, object? raw = null)
+{
+ ///
+ /// The raw representation of the configured object, if any.
+ ///
+ public object? Raw => raw;
+
+ ///
+ /// Gets the configured identifier for the subject.
+ ///
+ public string Id => id;
+
+ ///
+ /// Gets the factory function to create an instance of given a .
+ ///
+ public Func> FactoryAsync => factoryAsync;
+
+ ///
+ /// The configuration for this configured instance.
+ ///
+ public Config Configuration => new(this.Id);
+
+ ///
+ /// Gets a "partially" applied factory function that only requires no parameters to create an instance of
+ /// with the provided instance.
+ ///
+ internal Func> BoundFactoryAsync => () => this.FactoryAsync(this.Configuration);
+}
+
+///
+/// A representation of a preconfigured, lazy-instantiatable instance of .
+///
+/// The type of the preconfigured subject.
+/// The type of configuration options for the preconfigured subject.
+/// A factory to intantiate the subject when desired.
+/// The unique identifier for the configured subject.
+/// Additional configuration options for the subject.
+///
+public class Configured(Func, ValueTask> factoryAsync, string id, TOptions? options = default, object? raw = null)
+{
+ ///
+ /// The raw representation of the configured object, if any.
+ ///
+ public object? Raw => raw;
+
+ ///
+ /// Gets the configured identifier for the subject.
+ ///
+ public string Id => id;
+
+ ///
+ /// Gets the options associated with this instance.
+ ///
+ public TOptions? Options => options;
+
+ ///
+ /// Gets the factory function to create an instance of given a .
+ ///
+ public Func, ValueTask> FactoryAsync => factoryAsync;
+
+ ///
+ /// The configuration for this configured instance.
+ ///
+ public Config Configuration => new(this.Options, this.Id);
+
+ ///
+ /// Gets a "partially" applied factory function that only requires no parameters to create an instance of
+ /// with the provided instance.
+ ///
+ internal Func> BoundFactoryAsync => () => this.CreateValidatingMemoizedFactory()(this.Configuration);
+
+ private Func> CreateValidatingMemoizedFactory()
+ {
+ return FactoryAsync;
+
+ async ValueTask FactoryAsync(Config configuration)
+ {
+ if (this.Id != configuration.Id)
+ {
+ throw new InvalidOperationException($"Requested instance ID '{configuration.Id}' does not match configured ID '{this.Id}'.");
+ }
+
+ TSubject subject = await this.FactoryAsync(this.Configuration).ConfigureAwait(false);
+
+ if (this.Id != null && subject is IIdentified identified && identified.Id != this.Id)
+ {
+ throw new InvalidOperationException($"Created instance ID '{identified.Id}' does not match configured ID '{this.Id}'.");
+ }
+
+ return subject;
+ }
+ }
+
+ ///
+ /// Memoizes and erases the typed configuration options for the subject.
+ ///
+ public Configured Memoize() => new(this.CreateValidatingMemoizedFactory(), this.Id);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Executor.cs b/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
index 97519ec0ad..c3e704ed69 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Executor.cs
@@ -7,6 +7,7 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Execution;
+using Microsoft.Agents.Workflows.Reflection;
namespace Microsoft.Agents.Workflows;
@@ -21,20 +22,17 @@ public abstract class Executor : IIdentified
///
public string Id { get; }
+ private readonly ExecutorOptions _options;
+
///
/// Initialize the executor with a unique identifier
///
- /// A optional unique identifier for the executor. If null, a type-tagged
- /// UUID will be generated.
- protected Executor(string? id = null) : this(ExecutorOptions.Default, id)
- {
- }
-
- private readonly ExecutorOptions _options;
- internal Executor(ExecutorOptions options, string? id = null)
+ /// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+ /// Configuration options for the executor. If null, default options will be used.
+ protected Executor(string? id = null, ExecutorOptions? options = null)
{
this.Id = id ?? $"{this.GetType().Name}/{Guid.NewGuid():N}";
- this._options = options;
+ this._options = options ?? ExecutorOptions.Default;
}
///
@@ -144,3 +142,43 @@ public abstract class Executor : IIdentified
///
public bool CanHandle(Type messageType) => this.Router.CanHandle(messageType);
}
+
+///
+/// Provides a simple executor implementation that uses a single message handler function to process incoming messages.
+///
+/// The type of input message.
+/// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+/// Configuration options for the executor. If null, default options will be used.
+public abstract class Executor(string? id = null, ExecutorOptions? options = null)
+ : Executor(id, options), IMessageHandler
+{
+ ///
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
+ {
+ return routeBuilder.AddHandler(this.HandleAsync);
+ }
+
+ ///
+ public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context);
+}
+
+///
+/// Provides a simple executor implementation that uses a single message handler function to process incoming messages.
+///
+/// The type of input message.
+/// The type of output message.
+/// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+/// Configuration options for the executor. If null, default options will be used.
+public abstract class Executor(string? id = null, ExecutorOptions? options = null)
+ : Executor(id, options ?? ExecutorOptions.Default),
+ IMessageHandler
+{
+ ///
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
+ {
+ return routeBuilder.AddHandler(this.HandleAsync);
+ }
+
+ ///
+ public abstract ValueTask HandleAsync(TInput message, IWorkflowContext context);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/ExecutorIsh.cs b/dotnet/src/Microsoft.Agents.Workflows/ExecutorIsh.cs
index dd026f541d..53a2fb201f 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/ExecutorIsh.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/ExecutorIsh.cs
@@ -1,12 +1,86 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Specialized;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows;
+///
+/// Extension methods for configuring executors and functions as instances.
+///
+public static class ExecutorIshConfigurationExtensions
+{
+ ///
+ /// Configures a factory method for creating an of type , with
+ /// the specified id and options.
+ ///
+ ///
+ /// Although this will generally result in a delay-instantiated once messages are available
+ /// for it, if this is used as a start node of a typed via ,
+ /// it will be instantiated as part of the workflow's construction, to validate that its input type matches the
+ /// demanded TInput.
+ ///
+ /// The type of the resulting executor
+ /// The type of options object to be passed to the factory method.
+ /// The factory method.
+ /// An id for the executor to be instantiated.
+ /// An optional parameter specifying the options.
+ /// An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.
+ public static ExecutorIsh ConfigureFactory(this Func, ValueTask> factoryAsync, string id, TOptions? options = null)
+ where TExecutor : Executor
+ where TOptions : ExecutorOptions
+ {
+ Configured configured = new(factoryAsync, id, options);
+
+ return new ExecutorIsh(configured.Super(), typeof(TExecutor), ExecutorIsh.Type.Executor);
+ }
+
+ private static ExecutorIsh ToExecutorIsh(this FunctionExecutor executor, Delegate raw)
+ {
+ return new ExecutorIsh(Configured.FromInstance(executor, raw: raw)
+ .Super, Executor>(),
+ typeof(FunctionExecutor),
+ ExecutorIsh.Type.Function);
+ }
+
+ private static ExecutorIsh ToExecutorIsh(this FunctionExecutor executor, Delegate raw)
+ {
+ return new ExecutorIsh(Configured.FromInstance(executor, raw: raw)
+ .Super, Executor>(),
+ typeof(FunctionExecutor),
+ ExecutorIsh.Type.Function);
+ }
+
+ ///
+ /// Configures a function-based asynchronous message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// A delegate that defines the asynchronous function to execute for each input message.
+ /// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorIsh AsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null)
+ => new FunctionExecutor(messageHandlerAsync, id, options).ToExecutorIsh(messageHandlerAsync);
+
+ ///
+ /// Configures a function-based asynchronous message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// The type of output message.
+ /// A delegate that defines the asynchronous function to execute for each input message.
+ /// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorIsh AsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null)
+ => new FunctionExecutor(messageHandlerAsync, id, options).ToExecutorIsh(messageHandlerAsync);
+}
+
///
/// A tagged union representing an object that can function like an in a ,
/// or a reference to one by ID.
@@ -31,6 +105,10 @@ public sealed class ExecutorIsh :
///
Executor,
///
+ /// A function delegate to be wrapped as an executor.
+ ///
+ Function,
+ ///
/// An for servicing external requests.
///
InputPort,
@@ -46,7 +124,10 @@ public sealed class ExecutorIsh :
public Type ExecutorType { get; init; }
private readonly string? _idValue;
- private readonly Executor? _executorValue;
+
+ private readonly Configured? _configuredExecutor;
+ private readonly System.Type? _configuredExecutorType;
+
internal readonly InputPort? _inputPortValue;
private readonly AIAgent? _aiAgentValue;
@@ -60,6 +141,13 @@ public sealed class ExecutorIsh :
this._idValue = Throw.IfNull(id);
}
+ internal ExecutorIsh(Configured configured, System.Type configuredExecutorType, ExecutorIsh.Type type)
+ {
+ this.ExecutorType = type;
+ this._configuredExecutor = configured;
+ this._configuredExecutorType = configuredExecutorType;
+ }
+
///
/// Initializes a new instance of the ExecutorIsh class using the specified executor.
///
@@ -67,7 +155,8 @@ public sealed class ExecutorIsh :
public ExecutorIsh(Executor executor)
{
this.ExecutorType = Type.Executor;
- this._executorValue = Throw.IfNull(executor);
+ this._configuredExecutor = Configured.FromInstance(Throw.IfNull(executor));
+ this._configuredExecutorType = executor.GetType();
}
///
@@ -96,18 +185,20 @@ public sealed class ExecutorIsh :
public string Id => this.ExecutorType switch
{
Type.Unbound => this._idValue ?? throw new InvalidOperationException("This ExecutorIsh is unbound and has no ID."),
- Type.Executor => this._executorValue!.Id,
+ Type.Executor => this._configuredExecutor!.Id,
Type.InputPort => this._inputPortValue!.Id,
Type.Agent => this._aiAgentValue!.Id,
+ Type.Function => this._configuredExecutor!.Id,
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
};
internal object? RawData => this.ExecutorType switch
{
Type.Unbound => this._idValue,
- Type.Executor => this._executorValue,
+ Type.Executor => this._configuredExecutor!.Raw ?? this._configuredExecutor,
Type.InputPort => this._inputPortValue,
Type.Agent => this._aiAgentValue,
+ Type.Function => this._configuredExecutor!.Raw ?? this._configuredExecutor,
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
};
@@ -122,9 +213,10 @@ public sealed class ExecutorIsh :
private System.Type RuntimeType => this.ExecutorType switch
{
Type.Unbound => throw new InvalidOperationException($"ExecutorIsh with ID '{this.Id}' is unbound."),
- Type.Executor => this._executorValue!.GetType(),
+ Type.Executor => this._configuredExecutorType!,
Type.InputPort => typeof(RequestInfoExecutor),
Type.Agent => typeof(AIAgentHostExecutor),
+ Type.Function => this._configuredExecutorType!,
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
};
@@ -132,12 +224,13 @@ public sealed class ExecutorIsh :
/// Gets an that can be used to obtain an instance
/// corresponding to this .
///
- private Func ExecutorProvider => this.ExecutorType switch
+ private Func> ExecutorProvider => this.ExecutorType switch
{
Type.Unbound => throw new InvalidOperationException($"Executor with ID '{this.Id}' is unbound."),
- Type.Executor => () => this._executorValue!,
- Type.InputPort => () => new RequestInfoExecutor(this._inputPortValue!),
- Type.Agent => () => new AIAgentHostExecutor(this._aiAgentValue!),
+ Type.Executor => this._configuredExecutor!.BoundFactoryAsync,
+ Type.InputPort => () => new(new RequestInfoExecutor(this._inputPortValue!)),
+ Type.Agent => () => new(new AIAgentHostExecutor(this._aiAgentValue!)),
+ Type.Function => this._configuredExecutor!.BoundFactoryAsync,
_ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
};
@@ -225,9 +318,10 @@ public sealed class ExecutorIsh :
return this.ExecutorType switch
{
Type.Unbound => $"'{this.Id}':",
- Type.Executor => $"'{this.Id}':{this._executorValue!.GetType().Name}",
+ Type.Executor => $"'{this.Id}':{this._configuredExecutorType!.Name}",
Type.InputPort => $"'{this.Id}':Input({this._inputPortValue!.Request.Name}->{this._inputPortValue!.Response.Name})",
Type.Agent => $"{this.Id}':AIAgent(@{this._aiAgentValue!.GetType().Name})",
+ Type.Function => $"'{this.Id}':{this._configuredExecutorType!.Name}",
_ => $"'{this.Id}':"
};
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/ExecutorOptions.cs b/dotnet/src/Microsoft.Agents.Workflows/ExecutorOptions.cs
index a19630bf31..02cadf601f 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/ExecutorOptions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/ExecutorOptions.cs
@@ -5,7 +5,7 @@ namespace Microsoft.Agents.Workflows;
///
/// Configuration options for Executor behavior.
///
-public sealed class ExecutorOptions
+public class ExecutorOptions
{
///
/// The default runner configuration.
diff --git a/dotnet/src/Microsoft.Agents.Workflows/ExecutorRegistration.cs b/dotnet/src/Microsoft.Agents.Workflows/ExecutorRegistration.cs
index bb1e060107..76dbba8ed3 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/ExecutorRegistration.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/ExecutorRegistration.cs
@@ -1,9 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
-using ExecutorFactoryF = System.Func;
+using ExecutorFactoryF = System.Func>;
namespace Microsoft.Agents.Workflows;
@@ -11,7 +12,7 @@ internal class ExecutorRegistration(string id, Type executorType, ExecutorFactor
{
public string Id { get; } = Throw.IfNullOrEmpty(id);
public Type ExecutorType { get; } = Throw.IfNull(executorType);
- public ExecutorFactoryF Provider { get; } = Throw.IfNull(provider);
+ public ExecutorFactoryF ProviderAsync { get; } = Throw.IfNull(provider);
internal object? RawExecutorishData { get; } = rawData;
@@ -28,5 +29,5 @@ internal class ExecutorRegistration(string id, Type executorType, ExecutorFactor
return executor;
}
- public Executor CreateInstance() => this.CheckId(this.Provider());
+ public async ValueTask CreateInstanceAsync() => this.CheckId(await this.ProviderAsync().ConfigureAwait(false));
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/FunctionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/FunctionExecutor.cs
new file mode 100644
index 0000000000..502e58008b
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows/FunctionExecutor.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.Workflows;
+
+///
+/// Executes a user-provided asynchronous function in response to workflow messages of the specified input type.
+///
+/// The type of input message.
+/// A delegate that defines the asynchronous function to execute for each input message.
+/// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+/// Configuration options for the executor. If null, default options will be used.
+public class FunctionExecutor(Func handlerAsync,
+ string? id = null,
+ ExecutorOptions? options = null) : Executor(id, options)
+{
+ internal static Func WrapAction(Action handlerSync)
+ {
+ return RunActionAsync;
+
+ ValueTask RunActionAsync(TInput input, IWorkflowContext workflowContext, CancellationToken cancellation)
+ {
+ handlerSync(input, workflowContext, cancellation);
+ return default;
+ }
+ }
+
+ ///
+ public override ValueTask HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A synchronous function to execute for each input message and workflow context.
+ public FunctionExecutor(Action handlerSync) : this(WrapAction(handlerSync))
+ { }
+}
+
+///
+/// Executes a user-provided asynchronous function in response to workflow messages of the specified input type,
+///
+/// The type of input message.
+/// The type of output message.
+/// A delegate that defines the asynchronous function to execute for each input message.
+/// A optional unique identifier for the executor. If null, a type-tagged UUID will be generated.
+/// Configuration options for the executor. If null, default options will be used.
+public class FunctionExecutor(Func> handlerAsync,
+ string? id = null,
+ ExecutorOptions? options = null) : Executor(id, options)
+{
+ internal static Func> WrapFunc(Func handlerSync)
+ {
+ return RunFuncAsync;
+
+ ValueTask RunFuncAsync(TInput input, IWorkflowContext workflowContext, CancellationToken cancellation)
+ {
+ TOutput result = handlerSync(input, workflowContext, cancellation);
+ return new ValueTask(result);
+ }
+ }
+ ///
+ public override ValueTask HandleAsync(TInput message, IWorkflowContext context) => handlerAsync(message, context, default);
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// A synchronous function to execute for each input message and workflow context.
+ public FunctionExecutor(Func handlerSync) : this(WrapFunc(handlerSync))
+ { }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows/InProc/InProcessRunnerContext.cs b/dotnet/src/Microsoft.Agents.Workflows/InProc/InProcessRunnerContext.cs
index 15c4b9c8b2..3a57dd9302 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/InProc/InProcessRunnerContext.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/InProc/InProcessRunnerContext.cs
@@ -35,7 +35,7 @@ internal class InProcessRunnerContext : IRunnerContext
throw new InvalidOperationException($"Executor with ID '{executorId}' is not registered.");
}
- this._executors[executorId] = executor = registration.Provider();
+ this._executors[executorId] = executor = await registration.ProviderAsync().ConfigureAwait(false);
tracer?.TraceActivated(executorId);
if (executor is RequestInfoExecutor requestInputExecutor)
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Reflection/ReflectingExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/Reflection/ReflectingExecutor.cs
index 9b0ca4a74e..f5854bf29d 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Reflection/ReflectingExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Reflection/ReflectingExecutor.cs
@@ -16,8 +16,8 @@ public class ReflectingExecutor<
] TExecutor
> : Executor where TExecutor : ReflectingExecutor
{
- ///
- protected ReflectingExecutor(string? id = null) : base(id)
+ ///
+ protected ReflectingExecutor(string? id = null, ExecutorOptions? options = null) : base(id, options)
{ }
///
diff --git a/dotnet/src/Microsoft.Agents.Workflows/Specialized/RequestInfoExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows/Specialized/RequestInfoExecutor.cs
index 4cb151666f..9b12ff1515 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/Specialized/RequestInfoExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/Specialized/RequestInfoExecutor.cs
@@ -20,7 +20,7 @@ internal class RequestInfoExecutor : Executor
};
private readonly bool _allowWrapped;
- public RequestInfoExecutor(InputPort port, bool allowWrapped = true) : base(RequestInfoExecutor.DefaultOptions, port.Id)
+ public RequestInfoExecutor(InputPort port, bool allowWrapped = true) : base(port.Id, RequestInfoExecutor.DefaultOptions)
{
this.Port = port;
diff --git a/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
index 1a37415d19..49ad08c33b 100644
--- a/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows/WorkflowBuilder.cs
@@ -2,7 +2,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows;
@@ -205,6 +208,32 @@ public class WorkflowBuilder
return this;
}
+ [SuppressMessage("Reliability", "CA2008:Do not create tasks without passing a TaskScheduler",
+ Justification = "We explicitly set the TaskScheduler when we create the TaskFactory")]
+ [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits",
+ Justification = "This runs the thread on the thread pool")]
+ private static TResult RunSync(Func> funcAsync)
+ {
+ TaskFactory factory = new(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
+
+ // See ASP.Net.Identity's implementation of AsyncHelper
+ // https://github.com/aspnet/AspNetIdentity/blob/main/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs
+
+ // Capture the current culture and UI culture
+ var culture = System.Globalization.CultureInfo.CurrentCulture;
+ var uiCulture = System.Globalization.CultureInfo.CurrentUICulture;
+
+ return factory.StartNew(PropagateCultureAndInvoke).Unwrap().GetAwaiter().GetResult();
+
+ Task PropagateCultureAndInvoke()
+ {
+ // Set the culture and UI culture to the captured values
+ System.Globalization.CultureInfo.CurrentCulture = culture;
+ System.Globalization.CultureInfo.CurrentUICulture = uiCulture;
+ return funcAsync().AsTask();
+ }
+ }
+
///
/// Builds and returns a workflow instance configured to process messages of the specified input type.
///
@@ -228,9 +257,7 @@ public class WorkflowBuilder
throw new InvalidOperationException($"Start executor with ID '{this._startExecutorId}' is not bound.");
}
- // TODO: Delay-instantiate the start executor, and ensure it take input of type T
- Executor startExecutor = startRegistration.Provider();
-
+ Executor startExecutor = RunSync(startRegistration.CreateInstanceAsync);
if (!startExecutor.InputTypes.Any(t => t.IsAssignableFrom(typeof(T))))
{
// We have no handlers for the input type T, which means the built workflow will not be able to
diff --git a/dotnet/tests/Microsoft.Agents.Workflows.UnitTests/RepresentationTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.UnitTests/RepresentationTests.cs
index 1809d0d908..294b58a4de 100644
--- a/dotnet/tests/Microsoft.Agents.Workflows.UnitTests/RepresentationTests.cs
+++ b/dotnet/tests/Microsoft.Agents.Workflows.UnitTests/RepresentationTests.cs
@@ -47,42 +47,49 @@ public class RepresentationTests
return current;
}
- private static void RunExecutorishInfoMatchTest(ExecutorIsh target)
+ private static async ValueTask RunExecutorishInfoMatchTestAsync(ExecutorIsh target)
{
ExecutorRegistration registration = target.Registration;
ExecutorInfo info = registration.ToExecutorInfo();
- info.IsMatch(registration.Provider()).Should().BeTrue();
+ info.IsMatch(await registration.ProviderAsync()).Should().BeTrue();
}
[Fact]
- public void Test_Executorish_Infos()
+ public async Task Test_Executorish_InfosAsync()
{
int testsRun = 0;
- RunExecutorishTest(new TestExecutor());
- RunExecutorishTest(TestInputPort);
- RunExecutorishTest(new TestAgent());
+ await RunExecutorishTest(new TestExecutor());
+ await RunExecutorishTest(TestInputPort);
+ await RunExecutorishTest(new TestAgent());
+
+ Func function = MessageHandlerAsync;
+ await RunExecutorishTest(function.AsExecutor("FunctionExecutor"));
if (Enum.GetValues(typeof(ExecutorIsh.Type)).Length > testsRun + 1)
{
Assert.Fail("Not all ExecutorIsh types were tested.");
}
- void RunExecutorishTest(ExecutorIsh executorish)
+ async ValueTask RunExecutorishTest(ExecutorIsh executorish)
{
- RunExecutorishInfoMatchTest(executorish);
+ await RunExecutorishInfoMatchTestAsync(executorish);
testsRun++;
}
+
+ async ValueTask MessageHandlerAsync(int message, IWorkflowContext workflowContext, CancellationToken cancellation = default)
+ {
+ }
}
[Fact]
- public void Test_SpecializedExecutor_Infos()
+ public async Task Test_SpecializedExecutor_InfosAsync()
{
- RunExecutorishInfoMatchTest(new AIAgentHostExecutor(new TestAgent()));
- RunExecutorishInfoMatchTest(new RequestInfoExecutor(TestInputPort));
+ await RunExecutorishInfoMatchTestAsync(new AIAgentHostExecutor(new TestAgent()));
+ await RunExecutorishInfoMatchTestAsync(new RequestInfoExecutor(TestInputPort));
OutputCollectorExecutor> outputCollector = new(StreamingAggregators.Union());
- RunExecutorishInfoMatchTest(outputCollector);
+ await RunExecutorishInfoMatchTestAsync(outputCollector);
}
private static string Source(string id) => $"Source/{id}";