From c83011b30d91cadac8d688be7579261dbe604dc3 Mon Sep 17 00:00:00 2001
From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Date: Mon, 3 Nov 2025 08:34:55 -0800
Subject: [PATCH 01/15] .NET: Peibekwe/workflows cancellation token fix (#1740)
* Propagate cancellation token down the stack
* Added unit tests to cover workflow cancellation scenarios
* Updated tests based on feedback to simplify assert.
* Create custom AsyncEnumrable to gracefully handle cancellation for Channel reader. Tailor cancellation tests to declarative scenarios.
* Update comment and naming for readability.
* Fixing minor stylistic recommendation.
---------
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
---
...NonThrowingChannelReaderAsyncEnumerable.cs | 61 ++++++++++++++++++
.../Execution/StreamingRunEventStream.cs | 8 +--
.../DeclarativeWorkflowTest.cs | 63 +++++++++++++++++--
3 files changed, 121 insertions(+), 11 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/Execution/NonThrowingChannelReaderAsyncEnumerable.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/NonThrowingChannelReaderAsyncEnumerable.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/NonThrowingChannelReaderAsyncEnumerable.cs
new file mode 100644
index 0000000000..aaae42f2f1
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/NonThrowingChannelReaderAsyncEnumerable.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.Workflows.Execution;
+
+///
+/// A custom IAsyncEnumerable implementation that reads from a ChannelReader,
+/// and suppresses OperationCanceledException when the cancellation token is triggered.
+///
+internal sealed class NonThrowingChannelReaderAsyncEnumerable(ChannelReader reader) : IAsyncEnumerable
+{
+ private class Enumerator(ChannelReader reader, CancellationToken cancellationToken) : IAsyncEnumerator
+ {
+ private T? _current;
+ public T Current => this._current ?? throw new InvalidOperationException("Enumeration not started.");
+
+ public ValueTask DisposeAsync()
+ {
+ // no-op - the reader should not be disposed.
+ return default;
+ }
+
+ ///
+ /// Moves to the next item in the channel.
+ ///
+ /// If successful, returns true, otherwise false.
+ public async ValueTask MoveNextAsync()
+ {
+ try
+ {
+ bool hasData = await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false);
+ if (hasData)
+ {
+ this._current = await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Swallow cancellation exceptions to prevent throwing from the enumerator
+ // Enables clean cancellation and aligns with the expected behavior of IAsyncEnumerable.
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// Returns an async enumerator that reads items from the channel.
+ /// If cancellation is requested, the enumeration exits silently without throwing.
+ ///
+ /// An optional cancellation token from the caller.
+ /// An async enumerator over the channel items.
+ public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
+ => new Enumerator(reader, cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/StreamingRunEventStream.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/StreamingRunEventStream.cs
index 718ebcd11c..e6afbb5440 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/StreamingRunEventStream.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Execution/StreamingRunEventStream.cs
@@ -139,11 +139,9 @@ internal sealed class StreamingRunEventStream : IRunEventStream
// Get the current epoch - we'll only respond to completion signals from this epoch or later
int myEpoch = Volatile.Read(ref this._completionEpoch) + 1;
- // Simply read from channel - all coordination is handled by Channel infrastructure
- // Note: When cancellation is requested, ReadAllAsync may throw OperationCanceledException
- // or may complete the enumeration. We check IsCancellationRequested explicitly at superstep
- // boundaries to ensure clean cancellation.
- await foreach (WorkflowEvent evt in this._eventChannel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
+ // Use custom async enumerable to avoid exceptions on cancellation.
+ NonThrowingChannelReaderAsyncEnumerable eventStream = new(this._eventChannel.Reader);
+ await foreach (WorkflowEvent evt in eventStream.WithCancellation(cancellationToken).ConfigureAwait(false))
{
// Filter out internal signals used for run loop coordination
if (evt is InternalHaltSignal completionSignal)
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
index ae650352c5..6f2282902f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs
@@ -225,6 +225,54 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
Assert.True(visitor.HasUnsupportedActions);
}
+ [Theory]
+ [InlineData("CaseInsensitive.yaml", "end_when_match")]
+ [InlineData("ClearAllVariables.yaml", "clear_all")]
+ [InlineData("Condition.yaml", "setVariable_test")]
+ [InlineData("ConditionElse.yaml", "setVariable_test")]
+ [InlineData("EndConversation.yaml", "end_all")]
+ [InlineData("EndDialog.yaml", "end_all")]
+ [InlineData("EditTable.yaml", "edit_var")]
+ [InlineData("EditTableV2.yaml", "edit_var")]
+ [InlineData("Goto.yaml", "goto_end")]
+ [InlineData("LoopBreak.yaml", "break_loop_now")]
+ [InlineData("LoopContinue.yaml", "foreach_loop")]
+ [InlineData("LoopEach.yaml", "foreach_loop")]
+ [InlineData("MixedScopes.yaml", "activity_input")]
+ [InlineData("ParseValue.yaml", "parse_var")]
+ [InlineData("ParseValueList.yaml", "parse_var")]
+ [InlineData("ResetVariable.yaml", "clear_var")]
+ [InlineData("SendActivity.yaml", "activity_input")]
+ [InlineData("SetVariable.yaml", "set_var")]
+ [InlineData("SetTextVariable.yaml", "set_text")]
+ public async Task CancelRunAsync(string workflowPath, string expectedExecutedId)
+ {
+ // Arrange
+ const string WorkflowInput = "Test input message";
+ Workflow workflow = this.CreateWorkflow(workflowPath, WorkflowInput);
+ await using StreamingRun run = await InProcessExecution.StreamAsync(workflow: workflow, input: WorkflowInput);
+
+ // Act
+ await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
+ {
+ this.WorkflowEvents.Add(workflowEvent);
+
+ if (workflowEvent is DeclarativeActionInvokedEvent actionInvokedEvent && actionInvokedEvent.ActionId == expectedExecutedId)
+ {
+ // Cancel run after the specified declarative action is invoked.
+ await run.CancelRunAsync();
+ }
+ }
+ RunStatus currentRunStatus = await run.GetStatusAsync();
+ this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
+
+ // Assert
+ Assert.Equal(expected: RunStatus.Ended, actual: currentRunStatus);
+ Assert.NotEmpty(this.WorkflowEventCounts);
+ Assert.Contains(this.WorkflowEvents.OfType(), e => e.ActionId == expectedExecutedId);
+ Assert.DoesNotContain(this.WorkflowEvents.OfType(), e => e.ActionId == expectedExecutedId);
+ }
+
private void AssertExecutionCount(int expectedCount)
{
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorInvokedEvent)]);
@@ -256,12 +304,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
private async Task RunWorkflowAsync(string workflowPath, TInput workflowInput) where TInput : notnull
{
- using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowPath));
- Mock mockAgentProvider = CreateMockProvider($"{workflowInput}");
- DeclarativeWorkflowOptions workflowContext = new(mockAgentProvider.Object) { LoggerFactory = this.Output };
-
- Workflow workflow = DeclarativeWorkflowBuilder.Build(yamlReader, workflowContext);
-
+ Workflow workflow = this.CreateWorkflow(workflowPath, workflowInput);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, workflowInput);
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
@@ -303,6 +346,14 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow
this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
}
+ private Workflow CreateWorkflow(string workflowPath, TInput workflowInput) where TInput : notnull
+ {
+ using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowPath));
+ Mock mockAgentProvider = CreateMockProvider($"{workflowInput}");
+ DeclarativeWorkflowOptions workflowContext = new(mockAgentProvider.Object) { LoggerFactory = this.Output };
+ return DeclarativeWorkflowBuilder.Build(yamlReader, workflowContext);
+ }
+
private static Mock CreateMockProvider(string input)
{
Mock mockAgentProvider = new(MockBehavior.Strict);
From 91c66f8a2c68c4f283b24fa02ee32d8570af1545 Mon Sep 17 00:00:00 2001
From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Date: Mon, 3 Nov 2025 17:53:38 +0000
Subject: [PATCH 02/15] .NET: Sample demonstrating the use of agents in
workflows (#1836)
* add sample demonstrating the use of agents in workflows
* Update dotnet/samples/Catalog/AgentsInWorkflows/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* use type instead of var
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
dotnet/agent-framework-dotnet.slnx | 1 +
.../AgentsInWorkflows.csproj | 23 +++++++++
.../Catalog/AgentsInWorkflows/Program.cs | 48 +++++++++++++++++++
.../Catalog/AgentsInWorkflows/README.md | 26 ++++++++++
4 files changed, 98 insertions(+)
create mode 100644 dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
create mode 100644 dotnet/samples/Catalog/AgentsInWorkflows/Program.cs
create mode 100644 dotnet/samples/Catalog/AgentsInWorkflows/README.md
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 95812435d8..cbda6c2809 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -137,6 +137,7 @@
+
diff --git a/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
new file mode 100644
index 0000000000..284ea6ceec
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Catalog/AgentsInWorkflows/Program.cs b/dotnet/samples/Catalog/AgentsInWorkflows/Program.cs
new file mode 100644
index 0000000000..3e01f6e717
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentsInWorkflows/Program.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample demonstrates how to integrate AI agents into a workflow pipeline.
+// Three translation agents are connected sequentially to create a translation chain:
+// English → French → Spanish → English, showing how agents can be composed as workflow executors.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+// Set up the Azure OpenAI client
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient();
+
+// Create agents
+AIAgent frenchAgent = GetTranslationAgent("French", chatClient);
+AIAgent spanishAgent = GetTranslationAgent("Spanish", chatClient);
+AIAgent englishAgent = GetTranslationAgent("English", chatClient);
+
+// Build the workflow by adding executors and connecting them
+Workflow workflow = new WorkflowBuilder(frenchAgent)
+ .AddEdge(frenchAgent, spanishAgent)
+ .AddEdge(spanishAgent, englishAgent)
+.Build();
+
+// Execute the workflow
+await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
+
+// Must send the turn token to trigger the agents.
+// The agents are wrapped as executors. When they receive messages,
+// they will cache the messages and only start processing when they receive a TurnToken.
+await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
+await foreach (WorkflowEvent evt in run.WatchStreamAsync())
+{
+ if (evt is AgentRunUpdateEvent executorComplete)
+ {
+ Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
+ }
+}
+
+static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
+ new(chatClient, $"You are a translation assistant that translates the provided text to {targetLanguage}.");
diff --git a/dotnet/samples/Catalog/AgentsInWorkflows/README.md b/dotnet/samples/Catalog/AgentsInWorkflows/README.md
new file mode 100644
index 0000000000..a92012157e
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentsInWorkflows/README.md
@@ -0,0 +1,26 @@
+# What this sample demonstrates
+
+This sample demonstrates the use of AI agents as executors within a workflow.
+
+This workflow uses three translation agents:
+1. French Agent - translates input text to French
+2. Spanish Agent - translates French text to Spanish
+3. English Agent - translates Spanish text back to English
+
+The agents are connected sequentially, creating a translation chain that demonstrates how AI-powered components can be seamlessly integrated into workflow pipelines.
+
+## Prerequisites
+
+Before you begin, ensure you have the following prerequisites:
+
+- .NET 8.0 SDK or later
+- Azure OpenAI service endpoint and deployment configured
+- Azure CLI installed and authenticated (for Azure credential authentication)
+
+**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+
+Set the following environment variables:
+
+```powershell
+$env:AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" # Replace with your Azure OpenAI resource endpoint
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini
\ No newline at end of file
From b25b0af49b355f55330957790d62914bb3f4944e Mon Sep 17 00:00:00 2001
From: Jacob Alber
Date: Mon, 3 Nov 2025 13:20:35 -0500
Subject: [PATCH 03/15] .NET: [BREAKING] Unify ExecutorIsh and
ExecutorRegistration, unify/simplify APIs (#1637)
* refactor: Unify ExecutorIsh and ExecutorRegistration => ExecutorBinding
* Switch to more modern Record type-tree for Sum Types
* Unify APIs for getting ExecutorBinding
* Fix an issue where workflows consisting entirely of cross-run shareable executors which are not instance-resettable do not properly clear state when running non-concurrently.
* feat: Simplify function-to-executor pattern
* refactor: Normalize API naming
---
.../01_ExecutorsAndEdges/Program.cs | 21 +-
.../_Foundational/06_SubWorkflows/Program.cs | 2 +-
.../README.md | 2 +-
.../Interpreter/WorkflowModelBuilder.cs | 6 +-
.../AIAgentBinding.cs | 27 ++
.../AgentWorkflowBuilder.cs | 8 +-
.../Checkpointing/ExecutorInfo.cs | 6 +-
.../Checkpointing/RepresentationExtensions.cs | 10 +-
.../Checkpointing/WorkflowInfo.cs | 6 +-
.../ConfiguredExecutorBinding.cs | 34 ++
.../Microsoft.Agents.AI.Workflows/Executor.cs | 2 +
.../ExecutorBinding.cs | 129 ++++++
.../ExecutorBindingExtensions.cs | 434 ++++++++++++++++++
.../ExecutorInstanceBinding.cs | 39 ++
.../ExecutorIsh.cs | 369 ---------------
.../ExecutorPlaceholder.cs | 23 +
.../ExecutorRegistration.cs | 72 ---
.../FunctionExecutor.cs | 8 +-
.../GroupChatWorkflowBuilder.cs | 4 +-
.../InProc/InProcessRunnerContext.cs | 2 +-
.../RequestPortBinding.cs | 29 ++
.../Specialized/GroupChatHost.cs | 4 +-
.../SubworkflowBinding.cs | 44 ++
.../SwitchBuilder.cs | 12 +-
.../Visualization/WorkflowVisualizer.cs | 10 +-
.../Microsoft.Agents.AI.Workflows/Workflow.cs | 20 +-
.../WorkflowBuilder.cs | 75 ++-
.../WorkflowBuilderExtensions.cs | 16 +-
.../JsonSerializationTests.cs | 2 +-
.../RepresentationTests.cs | 40 +-
.../07_GroupChat_Workflow_HostAsAgent.cs | 20 +-
.../Sample/08_Subworkflow_Simple.cs | 6 +-
.../Sample/09_Subworkflow_ExternalRequest.cs | 32 +-
.../SampleSmokeTest.cs | 2 +
.../WorkflowBuilderSmokeTests.cs | 18 +-
35 files changed, 939 insertions(+), 595 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentBinding.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ConfiguredExecutorBinding.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBinding.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorInstanceBinding.cs
delete mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorIsh.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorPlaceholder.cs
delete mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorRegistration.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/RequestPortBinding.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows/SubworkflowBinding.cs
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
index 68e2effd04..af1dcb50d9 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/01_ExecutorsAndEdges/Program.cs
@@ -20,7 +20,9 @@ public static class Program
private static async Task Main()
{
// Create the executors
- UppercaseExecutor uppercase = new();
+ Func uppercaseFunc = s => s.ToUpperInvariant();
+ var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
+
ReverseTextExecutor reverse = new();
// Build the workflow by connecting executors sequentially
@@ -40,23 +42,6 @@ public static class Program
}
}
-///
-/// First executor: converts input text to uppercase.
-///
-internal sealed class UppercaseExecutor() : Executor("UppercaseExecutor")
-{
- ///
- /// Processes the input message by converting it to uppercase.
- ///
- /// The input text to convert
- /// Workflow context for accessing workflow services and adding events
- /// The to monitor for cancellation requests.
- /// The default is .
- /// The input text converted to uppercase
- public override ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
- ValueTask.FromResult(message.ToUpperInvariant()); // The return value will be sent as a message along an edge to subsequent executors
-}
-
///
/// Second executor: reverses the input text and completes the workflow.
///
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/Program.cs b/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/Program.cs
index de00c35ae8..7f9980e047 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/06_SubWorkflows/Program.cs
@@ -40,7 +40,7 @@ public static class Program
.Build();
// Step 2: Configure the sub-workflow as an executor for use in the parent workflow
- ExecutorIsh subWorkflowExecutor = subWorkflow.ConfigureSubWorkflow("TextProcessingSubWorkflow");
+ ExecutorBinding subWorkflowExecutor = subWorkflow.BindAsExecutor("TextProcessingSubWorkflow");
// Step 3: Build a main workflow that uses the sub-workflow as an executor
Console.WriteLine("Building main workflow that uses the sub-workflow as an executor...\n");
diff --git a/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/README.md b/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/README.md
index 1fd263888b..4ec203892b 100644
--- a/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/README.md
+++ b/dotnet/samples/GettingStarted/Workflows/_Foundational/07_MixedWorkflowAgentsAndExecutors/README.md
@@ -138,7 +138,7 @@ I cannot process this request as it appears to contain unsafe content.
## What You'll Learn
-1. **How to mix executors and agents** - Understanding that both are treated as `ExecutorIsh` internally
+1. **How to mix executors and agents** - Understanding that both are treated as `ExecutorBinding` internally
2. **When to use executors vs agents** - Executors for deterministic logic, agents for AI-powered decisions
3. **How to process agent outputs** - Using executors to sync, format, or aggregate agent responses
4. **Building complex pipelines** - Chaining multiple heterogeneous components together
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowModelBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowModelBuilder.cs
index 1d3826371d..d70b0841ce 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowModelBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowModelBuilder.cs
@@ -19,12 +19,12 @@ internal sealed class WorkflowModelBuilder : IModelBuilder>
Debug.WriteLine($"> CONNECT: {source.Id} => {target.Id}{(condition is null ? string.Empty : " (?)")}");
this.WorkflowBuilder.AddEdge(
- GetExecutorIsh(source),
- GetExecutorIsh(target),
+ GetExecutorBinding(source),
+ GetExecutorBinding(target),
condition);
}
- private static ExecutorIsh GetExecutorIsh(IModeledAction action) =>
+ private static ExecutorBinding GetExecutorBinding(IModeledAction action) =>
action switch
{
RequestPortAction port => port.RequestPort,
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentBinding.cs
new file mode 100644
index 0000000000..171f7d9eec
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentBinding.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows.Specialized;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents the workflow binding details for an AI agent, including configuration options for event emission.
+///
+/// The AI agent.
+/// Specifies whether the agent should emit events. If null, the default behavior is applied.
+public record AIAgentBinding(AIAgent Agent, bool EmitEvents = false)
+ : ExecutorBinding(Throw.IfNull(Agent).Name ?? Throw.IfNull(Agent.Id),
+ (_) => new(new AIAgentHostExecutor(Agent, EmitEvents)),
+ typeof(AIAgentHostExecutor),
+ Agent)
+{
+ ///
+ public override bool IsSharedInstance => false;
+
+ ///
+ public override bool SupportsConcurrentSharedExecution => true;
+
+ ///
+ public override bool SupportsResetting => false;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs
index 8b527d5c44..81d254aa53 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AgentWorkflowBuilder.cs
@@ -40,7 +40,7 @@ public static partial class AgentWorkflowBuilder
// Create a builder that chains the agents together in sequence. The workflow simply begins
// with the first agent in the sequence.
WorkflowBuilder? builder = null;
- ExecutorIsh? previous = null;
+ ExecutorBinding? previous = null;
foreach (var agent in agents)
{
AgentRunStreamingExecutor agentExecutor = new(agent, includeInputInOutput: true);
@@ -125,8 +125,8 @@ public static partial class AgentWorkflowBuilder
// so that the final accumulator receives a single list of messages from each agent. Otherwise, the
// accumulator would not be able to determine what came from what agent, as there's currently no
// provenance tracking exposed in the workflow context passed to a handler.
- ExecutorIsh[] agentExecutors = (from agent in agents select (ExecutorIsh)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
- ExecutorIsh[] accumulators = [.. from agent in agentExecutors select (ExecutorIsh)new CollectChatMessagesExecutor($"Batcher/{agent.Id}")];
+ ExecutorBinding[] agentExecutors = (from agent in agents select (ExecutorBinding)new AgentRunStreamingExecutor(agent, includeInputInOutput: false)).ToArray();
+ ExecutorBinding[] accumulators = [.. from agent in agentExecutors select (ExecutorBinding)new CollectChatMessagesExecutor($"Batcher/{agent.Id}")];
builder.AddFanOutEdge(start, targets: agentExecutors);
for (int i = 0; i < agentExecutors.Length; i++)
{
@@ -141,7 +141,7 @@ public static partial class AgentWorkflowBuilder
Func> endFactory =
(string _, string __) => new(new ConcurrentEndExecutor(agentExecutors.Length, aggregator));
- ExecutorIsh end = endFactory.ConfigureFactory(ConcurrentEndExecutor.ExecutorId);
+ ExecutorBinding end = endFactory.BindExecutor(ConcurrentEndExecutor.ExecutorId);
builder.AddFanInEdge(end, sources: accumulators);
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs
index c9994a91d0..6e019b4928 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/ExecutorInfo.cs
@@ -12,7 +12,7 @@ internal sealed record class ExecutorInfo(TypeId ExecutorType, string ExecutorId
this.ExecutorType.IsMatch(executor.GetType())
&& this.ExecutorId == executor.Id;
- public bool IsMatch(ExecutorRegistration registration) =>
- this.ExecutorType.IsMatch(registration.ExecutorType)
- && this.ExecutorId == registration.Id;
+ public bool IsMatch(ExecutorBinding binding) =>
+ this.ExecutorType.IsMatch(binding.ExecutorType)
+ && this.ExecutorId == binding.Id;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/RepresentationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/RepresentationExtensions.cs
index 1878cbdd21..3d76c965bd 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/RepresentationExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/RepresentationExtensions.cs
@@ -9,10 +9,10 @@ namespace Microsoft.Agents.AI.Workflows.Checkpointing;
internal static class RepresentationExtensions
{
- public static ExecutorInfo ToExecutorInfo(this ExecutorRegistration registration)
+ public static ExecutorInfo ToExecutorInfo(this ExecutorBinding binding)
{
- Throw.IfNull(registration);
- return new ExecutorInfo(new TypeId(registration.ExecutorType), registration.Id);
+ Throw.IfNull(binding);
+ return new ExecutorInfo(new TypeId(binding.ExecutorType), binding.Id);
}
public static EdgeInfo ToEdgeInfo(this Edge edge)
@@ -38,8 +38,8 @@ internal static class RepresentationExtensions
Throw.IfNull(workflow);
Dictionary executors =
- workflow.Registrations.Values.ToDictionary(
- keySelector: registration => registration.Id,
+ workflow.ExecutorBindings.Values.ToDictionary(
+ keySelector: binding => binding.Id,
elementSelector: ToExecutorInfo);
Dictionary> edges = workflow.Edges.Keys.ToDictionary(
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs
index 59a41db405..f40882265a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Checkpointing/WorkflowInfo.cs
@@ -47,10 +47,10 @@ internal sealed class WorkflowInfo
}
// Validate the executors
- if (workflow.Registrations.Count != this.Executors.Count ||
+ if (workflow.ExecutorBindings.Count != this.Executors.Count ||
this.Executors.Keys.Any(
- executorId => workflow.Registrations.TryGetValue(executorId, out ExecutorRegistration? registration)
- && !this.Executors[executorId].IsMatch(registration)))
+ executorId => workflow.ExecutorBindings.TryGetValue(executorId, out ExecutorBinding? binding)
+ && !this.Executors[executorId].IsMatch(binding)))
{
return false;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ConfiguredExecutorBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ConfiguredExecutorBinding.cs
new file mode 100644
index 0000000000..cfbfeba355
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ConfiguredExecutorBinding.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+// TODO: Unwrap the Configured object, just like for SubworkflowBinding
+internal record ConfiguredExecutorBinding(Configured ConfiguredExecutor, Type ExecutorType)
+ : ExecutorBinding(Throw.IfNull(ConfiguredExecutor).Id,
+ ConfiguredExecutor.BoundFactoryAsync,
+ ExecutorType,
+ ConfiguredExecutor.Raw)
+{
+ ///
+ public override bool IsSharedInstance { get; } = ConfiguredExecutor.Raw is Executor;
+
+ protected override async ValueTask ResetCoreAsync()
+ {
+ if (this.ConfiguredExecutor.Raw is IResettableExecutor resettable)
+ {
+ await resettable.ResetAsync().ConfigureAwait(false);
+ }
+
+ return false;
+ }
+
+ ///
+ public override bool SupportsConcurrentSharedExecution => true;
+
+ ///
+ public override bool SupportsResetting => false;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Executor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Executor.cs
index 4c2821476a..e0b53429f9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Executor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Executor.cs
@@ -27,6 +27,8 @@ public abstract class Executor : IIdentified
private static readonly string s_namespace = typeof(Executor).Namespace!;
private static readonly ActivitySource s_activitySource = new(s_namespace);
+ // TODO: Add overloads for binding with a configuration/options object once the Configured hierarchy goes away.
+
///
/// Initialize the executor with a unique identifier
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBinding.cs
new file mode 100644
index 0000000000..f4c196426c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBinding.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents the binding information for a workflow executor, including its identifier, factory method, type, and
+/// optional raw value.
+///
+/// The unique identifier for the executor in the workflow.
+/// A factory function that creates an instance of the executor. The function accepts two string parameters and returns
+/// a ValueTask containing the created Executor instance.
+/// The type of the executor. Must be a type derived from Executor.
+/// An optional raw value associated with the binding.
+public abstract record class ExecutorBinding(string Id, Func>? FactoryAsync, Type ExecutorType, object? RawValue = null)
+ : IIdentified,
+ IEquatable,
+ IEquatable
+{
+ ///
+ /// Gets a value indicating whether the binding is a placeholder (i.e., does not have a factory method defined).
+ ///
+ [MemberNotNullWhen(false, nameof(FactoryAsync))]
+ public bool IsPlaceholder => this.FactoryAsync == null;
+
+ ///
+ /// Gets a value whether the executor created from this binding is a shared instance across all runs.
+ ///
+ public abstract bool IsSharedInstance { get; }
+
+ ///
+ /// Gets a value whether instances of the executor created from this binding can be used in concurrent runs
+ /// from the same instance.
+ ///
+ public abstract bool SupportsConcurrentSharedExecution { get; }
+
+ ///
+ /// Gets a value whether instances of the executor created from this binding can be reset between subsequent
+ /// runs from the same instance. This value is not relevant for executors that .
+ ///
+ public abstract bool SupportsResetting { get; }
+
+ ///
+ public override string ToString() => $"{this.Id}:{(this.IsPlaceholder ? ":" : this.ExecutorType.Name)}";
+
+ private Executor CheckId(Executor executor)
+ {
+ if (executor.Id != this.Id)
+ {
+ throw new InvalidOperationException(
+ $"Executor ID mismatch: expected '{this.Id}', but got '{executor.Id}'.");
+ }
+
+ return executor;
+ }
+
+ internal async ValueTask CreateInstanceAsync(string runId)
+ => !this.IsPlaceholder
+ ? this.CheckId(await this.FactoryAsync(runId).ConfigureAwait(false))
+ : throw new InvalidOperationException(
+ $"Cannot create executor with ID '{this.Id}': Binding ({this.GetType().Name}) is a placeholder.");
+
+ ///
+ public virtual bool Equals(ExecutorBinding? other) =>
+ other is not null && other.Id == this.Id;
+
+ ///
+ public bool Equals(IIdentified? other) =>
+ other is not null && other.Id == this.Id;
+
+ ///
+ public bool Equals(string? other) =>
+ other is not null && other == this.Id;
+
+ internal ValueTask TryResetAsync()
+ {
+ // Non-shared instances do not need resetting
+ if (!this.IsSharedInstance)
+ {
+ return new(true);
+ }
+
+ // If the executor supports concurrent use, then resetting is a no-op.
+ if (!this.SupportsResetting)
+ {
+ return new(false);
+ }
+
+ return this.ResetCoreAsync();
+ }
+
+ ///
+ /// Resets the executor's shared resources to their initial state. Must be overridden by bindings that support
+ /// resetting.
+ ///
+ ///
+ protected virtual ValueTask ResetCoreAsync() => throw new InvalidOperationException("ExecutorBindings that support resetting must override ResetCoreAsync()");
+
+ ///
+ public override int GetHashCode() => this.Id.GetHashCode();
+
+ ///
+ /// Defines an implicit conversion from an Executor to a .
+ ///
+ /// The Executor instance to convert.
+ public static implicit operator ExecutorBinding(Executor executor) => executor.BindExecutor();
+
+ ///
+ /// Defines an implicit conversion from a string identifier to an .
+ ///
+ /// The string identifier to convert to a placeholder.
+ public static implicit operator ExecutorBinding(string id) => new ExecutorPlaceholder(id);
+
+ ///
+ /// Defines an implicit conversion from a to an .
+ ///
+ /// The RequestPort instance to convert.
+ public static implicit operator ExecutorBinding(RequestPort port) => port.BindAsExecutor();
+
+ ///
+ /// Defines an implicit conversion from an to an instance.
+ ///
+ ///
+ public static implicit operator ExecutorBinding(AIAgent agent) => agent.BindAsExecutor();
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
new file mode 100644
index 0000000000..5a5e197541
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
@@ -0,0 +1,434 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Extension methods for configuring executors and functions as instances.
+///
+public static class ExecutorBindingExtensions
+{
+ ///
+ /// Configures an instance for use in a workflow.
+ ///
+ ///
+ /// Note that Executor Ids must be unique within a workflow.
+ ///
+ /// The executor instance.
+ /// An instance wrapping the specified .
+ public static ExecutorBinding BindExecutor(this Executor executor)
+ => new ExecutorInstanceBinding(executor);
+
+ ///
+ /// Configures a factory method for creating an of type , using the
+ /// type name as the id.
+ ///
+ ///
+ /// Note that Executor Ids must be unique within a workflow.
+ ///
+ /// Although this will generally result in a delay-instantiated once messages are available
+ /// for it, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// The type of the resulting executor
+ /// The factory method.
+ /// An instance that resolves to the result of the factory call when messages get sent to it.
+ public static ExecutorBinding BindExecutor(this Func> factoryAsync)
+ where TExecutor : Executor
+ => BindExecutor((config, runId) => factoryAsync(config.Id, runId), id: typeof(TExecutor).Name, options: null);
+
+ ///
+ /// Configures a factory method for creating an of type , using the
+ /// type name as the id.
+ ///
+ ///
+ /// Note that Executor Ids must be unique within a workflow.
+ ///
+ /// Although this will generally result in a delay-instantiated once messages are available
+ /// for it, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// The type of the resulting executor
+ /// The factory method.
+ /// An instance that resolves to the result of the factory call when messages get sent to it.
+ [Obsolete("Use BindExecutor() instead.")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static ExecutorBinding ConfigureFactory(this Func> factoryAsync)
+ where TExecutor : Executor
+ => factoryAsync.BindExecutor();
+
+ ///
+ /// Configures a factory method for creating an of type , with
+ /// the specified id.
+ ///
+ ///
+ /// Although this will generally result in a delay-instantiated once messages are available
+ /// for it, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// The type of the resulting executor
+ /// The factory method.
+ /// An id for the executor to be instantiated.
+ /// An instance that resolves to the result of the factory call when messages get sent to it.
+ public static ExecutorBinding BindExecutor(this Func> factoryAsync, string id)
+ where TExecutor : Executor
+ => BindExecutor((_, runId) => factoryAsync(id, runId), id, options: null);
+
+ ///
+ /// Configures a factory method for creating an of type , with
+ /// the specified id.
+ ///
+ ///
+ /// Although this will generally result in a delay-instantiated once messages are available
+ /// for it, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// The type of the resulting executor
+ /// The factory method.
+ /// An id for the executor to be instantiated.
+ /// An instance that resolves to the result of the factory call when messages get sent to it.
+ [Obsolete("Use BindExecutor() instead.")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static ExecutorBinding ConfigureFactory(this Func> factoryAsync, string id)
+ where TExecutor : Executor
+ => factoryAsync.BindExecutor(id);
+
+ ///
+ /// 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, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// 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 instance that resolves to the result of the factory call when messages get sent to it.
+ public static ExecutorBinding BindExecutor(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
+ where TExecutor : Executor
+ where TOptions : ExecutorOptions
+ {
+ Configured configured = new(factoryAsync, id, options);
+
+ return new ConfiguredExecutorBinding(configured.Super(), typeof(TExecutor));
+ }
+
+ ///
+ /// 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, it will be instantiated if a for the is requested,
+ /// and it is the starting executor.
+ ///
+ /// 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 instance that resolves to the result of the factory call when messages get sent to it.
+ [Obsolete("Use BindExecutor() instead")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static ExecutorBinding ConfigureFactory(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
+ where TExecutor : Executor
+ where TOptions : ExecutorOptions
+ => factoryAsync.BindExecutor(id, options);
+
+ private static ConfiguredExecutorBinding ToBinding(this FunctionExecutor executor, Delegate raw)
+ => new(Configured.FromInstance(executor, raw: raw)
+ .Super, Executor>(),
+ typeof(FunctionExecutor));
+
+ private static ConfiguredExecutorBinding ToBinding(this FunctionExecutor executor, Delegate raw)
+ => new(Configured.FromInstance(executor, raw: raw)
+ .Super, Executor>(),
+ typeof(FunctionExecutor));
+
+ ///
+ /// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
+ ///
+ /// The workflow instance to be executed as a sub-workflow. Cannot be null.
+ /// A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.
+ /// Optional configuration options for the sub-workflow executor. If null, default options are used.
+ /// An ExecutorRegistration instance representing the configured sub-workflow executor.
+ [Obsolete("Use BindAsExecutor() instead")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static ExecutorBinding ConfigureSubWorkflow(this Workflow workflow, string id, ExecutorOptions? options = null)
+ => workflow.BindAsExecutor(id, options);
+
+ ///
+ /// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
+ ///
+ /// The workflow instance to be executed as a sub-workflow. Cannot be null.
+ /// A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.
+ /// Optional configuration options for the sub-workflow executor. If null, default options are used.
+ /// An instance representing the configured sub-workflow executor.
+ public static ExecutorBinding BindAsExecutor(this Workflow workflow, string id, ExecutorOptions? options = null)
+ => new SubworkflowBinding(workflow, id, options);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => new FunctionExecutor(id, messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandlerAsync);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, _, __) => messageHandlerAsync(input)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, ctx, __) => messageHandlerAsync(input, ctx)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, _, ct) => messageHandlerAsync(input, ct)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// A delegate that defines the function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Action messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => new FunctionExecutor(id, messageHandler, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandler);
+
+ ///
+ /// Configures a function-based message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// A delegate that defines the function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Action messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Action)((input, _, __) => messageHandler(input)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// A delegate that defines the function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Action messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Action)((input, ctx, __) => messageHandler(input, ctx)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based message handler as an executor with the specified identifier and
+ /// options.
+ ///
+ /// The type of input message.
+ /// A delegate that defines the function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Action messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Action)((input, _, ct) => messageHandler(input, ct)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// 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 unique identifier for the executor.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => new FunctionExecutor(Throw.IfNull(id), messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToBinding(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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func>)((input, _, __) => messageHandlerAsync(input)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func>)((input, ctx, __) => messageHandlerAsync(input, ctx)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// 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.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func>)((input, _, ct) => messageHandlerAsync(input, ct)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based 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 function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => new FunctionExecutor(id, messageHandler, options, declareCrossRunShareable: threadsafe).ToBinding(messageHandler);
+
+ ///
+ /// Configures a function-based 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 function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, _, __) => messageHandler(input)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based 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 function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, ctx, __) => messageHandler(input, ctx)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based 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 function to execute for each input message.
+ /// An optional unique identifier for the executor. If null, will use the function argument as an id.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func messageHandler, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => ((Func)((input, _, ct) => messageHandler(input, ct)))
+ .BindAsExecutor(id, options, threadsafe);
+
+ ///
+ /// Configures a function-based aggregating executor with the specified identifier and options.
+ ///
+ /// The type of input message.
+ /// The type of the accumulating object.
+ /// A delegate the defines the aggregation procedure
+ /// A unique identifier for the executor.
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
+ /// An instance that wraps the provided asynchronous message handler and configuration.
+ public static ExecutorBinding BindAsExecutor(this Func aggregatorFunc, string id, ExecutorOptions? options = null, bool threadsafe = false)
+ => new AggregatingExecutor(id, aggregatorFunc, options, declareCrossRunShareable: threadsafe);
+
+ ///
+ /// Configure an as an executor for use in a workflow.
+ ///
+ /// The agent instance.
+ /// Specifies whether the agent should emit streaming events.
+ /// An instance that wraps the provided agent.
+ public static ExecutorBinding BindAsExecutor(this AIAgent agent, bool emitEvents = false)
+ => new AIAgentBinding(agent, emitEvents);
+
+ ///
+ /// Configure a as an executor for use in a workflow.
+ ///
+ /// The port configuration.
+ /// Specifies whether the port should accept requests already wrapped in
+ /// .
+ /// A instance that wraps the provided port.
+ public static ExecutorBinding BindAsExecutor(this RequestPort port, bool allowWrappedRequests = true)
+ => new RequestPortBinding(port, allowWrappedRequests);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorInstanceBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorInstanceBinding.cs
new file mode 100644
index 0000000000..916e28a930
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorInstanceBinding.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents the workflow binding details for a shared executor instance, including configuration options
+/// for event emission.
+///
+/// The executor instance to bind. Cannot be null.
+public record ExecutorInstanceBinding(Executor ExecutorInstance)
+ : ExecutorBinding(Throw.IfNull(ExecutorInstance).Id,
+ (_) => new(ExecutorInstance),
+ ExecutorInstance.GetType(),
+ ExecutorInstance)
+{
+ ///
+ public override bool SupportsConcurrentSharedExecution => this.ExecutorInstance.IsCrossRunShareable;
+
+ ///
+ public override bool SupportsResetting => this.ExecutorInstance is IResettableExecutor;
+
+ ///
+ public override bool IsSharedInstance => true;
+
+ ///
+ protected override async ValueTask ResetCoreAsync()
+ {
+ if (this.ExecutorInstance is IResettableExecutor resettable)
+ {
+ await resettable.ResetAsync().ConfigureAwait(false);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorIsh.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorIsh.cs
deleted file mode 100644
index 63807c5576..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorIsh.cs
+++ /dev/null
@@ -1,369 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Agents.AI.Workflows.Specialized;
-using Microsoft.Shared.Diagnostics;
-
-namespace Microsoft.Agents.AI.Workflows;
-
-///
-/// Extension methods for configuring executors and functions as instances.
-///
-public static class ExecutorIshConfigurationExtensions
-{
- ///
- /// Configures a factory method for creating an of type , using the
- /// type name as the id.
- ///
- ///
- /// Note that Executor Ids must be unique within a workflow.
- ///
- /// Although this will generally result in a delay-instantiated once messages are available
- /// for it, it will be instantiated if a for the is requested,
- /// and it is the starting executor.
- ///
- /// The type of the resulting executor
- /// The factory method.
- /// An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.
- public static ExecutorIsh ConfigureFactory(this Func> factoryAsync)
- where TExecutor : Executor
- => ConfigureFactory((config, runId) => factoryAsync(config.Id, runId), typeof(TExecutor).Name, options: null);
-
- ///
- /// Configures a factory method for creating an of type , with
- /// the specified id.
- ///
- ///
- /// Although this will generally result in a delay-instantiated once messages are available
- /// for it, it will be instantiated if a for the is requested,
- /// and it is the starting executor.
- ///
- /// The type of the resulting executor
- /// The factory method.
- /// An id for the executor to be instantiated.
- /// An ExecutorIsh instance that resolves to the result of the factory call when messages get sent to it.
- public static ExecutorIsh ConfigureFactory(this Func> factoryAsync, string id)
- where TExecutor : Executor
- => ConfigureFactory((_, runId) => factoryAsync(id, runId), id, options: null);
-
- ///
- /// 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, it will be instantiated if a for the is requested,
- /// and it is the starting executor.
- ///
- /// 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, string, 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) => new(Configured.FromInstance(executor, raw: raw)
- .Super, Executor>(),
- typeof(FunctionExecutor),
- ExecutorIsh.Type.Function);
-
- private static ExecutorIsh ToExecutorIsh(this FunctionExecutor executor, Delegate raw) => new(Configured.FromInstance(executor, raw: raw)
- .Super, Executor>(),
- typeof(FunctionExecutor),
- ExecutorIsh.Type.Function);
-
- ///
- /// Configures a sub-workflow executor for the specified workflow, using the provided identifier and options.
- ///
- /// The workflow instance to be executed as a sub-workflow. Cannot be null.
- /// A unique identifier for the sub-workflow execution. Used to distinguish this sub-workflow instance.
- /// Optional configuration options for the sub-workflow executor. If null, default options are used.
- /// An ExecutorIsh instance representing the configured sub-workflow executor.
- public static ExecutorIsh ConfigureSubWorkflow(this Workflow workflow, string id, ExecutorOptions? options = null)
- {
- object ownershipToken = new();
- workflow.TakeOwnership(ownershipToken, subworkflow: true);
-
- Configured configured = new(InitHostExecutorAsync, id, options, raw: workflow);
- return new ExecutorIsh(configured.Super(), typeof(WorkflowHostExecutor), ExecutorIsh.Type.Workflow);
-
- ValueTask InitHostExecutorAsync(Config config, string runId)
- {
- return new(new WorkflowHostExecutor(config.Id, workflow, runId, ownershipToken, config.Options));
- }
- }
-
- ///
- /// 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, will use the function argument as an id.
- /// Configuration options for the executor. If null, default options will be used.
- /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
- /// An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.
- public static ExecutorIsh AsExecutor(this Func messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
- => new FunctionExecutor(id, messageHandlerAsync, options, declareCrossRunShareable: threadsafe).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 unique identifier for the executor.
- /// Configuration options for the executor. If null, default options will be used.
- /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
- /// An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.
- public static ExecutorIsh AsExecutor(this Func> messageHandlerAsync, string id, ExecutorOptions? options = null, bool threadsafe = false)
- => new FunctionExecutor(Throw.IfNull(id), messageHandlerAsync, options, declareCrossRunShareable: threadsafe).ToExecutorIsh(messageHandlerAsync);
-
- ///
- /// Configures a function-based aggregating executor with the specified identifier and options.
- ///
- /// The type of input message.
- /// The type of the accumulating object.
- /// A delegate the defines the aggregation procedure
- /// A unique identifier for the executor.
- /// Configuration options for the executor. If null, default options will be used.
- /// Declare that the message handler may be used simultaneously by multiple runs concurrently.
- /// An ExecutorIsh instance that wraps the provided asynchronous message handler and configuration.
- public static ExecutorIsh AsExecutor(this Func aggregatorFunc, string id, ExecutorOptions? options = null, bool threadsafe = false)
- => new AggregatingExecutor(id, aggregatorFunc, options, declareCrossRunShareable: threadsafe);
-}
-
-///
-/// A tagged union representing an object that can function like an in a ,
-/// or a reference to one by ID.
-///
-public sealed class ExecutorIsh :
- IIdentified,
- IEquatable,
- IEquatable,
- IEquatable
-{
- ///
- /// The type of the .
- ///
- public enum Type
- {
- ///
- /// An unbound executor reference, identified only by ID.
- ///
- Unbound,
- ///
- /// An actual instance.
- ///
- Executor,
- ///
- /// A function delegate to be wrapped as an executor.
- ///
- Function,
- ///
- /// An for servicing external requests.
- ///
- RequestPort,
- ///
- /// An instance.
- ///
- Agent,
- ///
- /// A nested instance.
- ///
- Workflow,
- }
-
- ///
- /// Gets the type of data contained in this instance.
- ///
- public Type ExecutorType { get; init; }
-
- private readonly string? _idValue;
-
- private readonly Configured? _configuredExecutor;
- private readonly System.Type? _configuredExecutorType;
-
- internal readonly RequestPort? _requestPortValue;
- private readonly AIAgent? _aiAgentValue;
-
- ///
- /// Initializes a new instance of the class as an unbound reference by ID.
- ///
- /// A unique identifier for an in the
- public ExecutorIsh(string id)
- {
- this.ExecutorType = Type.Unbound;
- this._idValue = Throw.IfNull(id);
- }
-
- internal ExecutorIsh(Configured configured, System.Type configuredExecutorType, Type type)
- {
- this.ExecutorType = type;
- this._configuredExecutor = configured;
- this._configuredExecutorType = configuredExecutorType;
- }
-
- ///
- /// Initializes a new instance of the ExecutorIsh class using the specified executor.
- ///
- /// The executor instance to be wrapped.
- public ExecutorIsh(Executor executor)
- {
- this.ExecutorType = Type.Executor;
- this._configuredExecutor = Configured.FromInstance(Throw.IfNull(executor));
- this._configuredExecutorType = executor.GetType();
- }
-
- ///
- /// Initializes a new instance of the ExecutorIsh class using the specified input port.
- ///
- /// The input port to associate to be wrapped.
- public ExecutorIsh(RequestPort port)
- {
- this.ExecutorType = Type.RequestPort;
- this._requestPortValue = Throw.IfNull(port);
- }
-
- ///
- /// Initializes a new instance of the ExecutorIsh class using the specified AI agent.
- ///
- ///
- public ExecutorIsh(AIAgent aiAgent)
- {
- this.ExecutorType = Type.Agent;
- this._aiAgentValue = Throw.IfNull(aiAgent);
- }
-
- internal bool IsUnbound => this.ExecutorType == Type.Unbound;
-
- ///
- public string Id => this.ExecutorType switch
- {
- Type.Unbound => this._idValue ?? throw new InvalidOperationException("This ExecutorIsh is unbound and has no ID."),
- Type.Executor => this._configuredExecutor!.Id,
- Type.RequestPort => this._requestPortValue!.Id,
- Type.Agent => this._aiAgentValue!.Id,
- Type.Function => this._configuredExecutor!.Id,
- Type.Workflow => this._configuredExecutor!.Id,
- _ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
- };
-
- internal object? RawData => this.ExecutorType switch
- {
- Type.Unbound => this._idValue,
- Type.Executor => this._configuredExecutor!.Raw ?? this._configuredExecutor,
- Type.RequestPort => this._requestPortValue,
- Type.Agent => this._aiAgentValue,
- Type.Function => this._configuredExecutor!.Raw ?? this._configuredExecutor,
- Type.Workflow => this._configuredExecutor!.Raw ?? this._configuredExecutor,
- _ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
- };
-
- ///
- /// Gets the registration details for the current executor.
- ///
- /// The returned registration depends on the type of the executor. If the executor is unbound, an
- /// is thrown. For other executor types, the registration includes the
- /// appropriate ID, type, and provider based on the executor's configuration.
- internal ExecutorRegistration Registration => new(this.Id, this.RuntimeType, this.ExecutorProvider, this.RawData);
-
- private System.Type RuntimeType => this.ExecutorType switch
- {
- Type.Unbound => throw new InvalidOperationException($"ExecutorIsh with ID '{this.Id}' is unbound."),
- Type.Executor => this._configuredExecutorType!,
- Type.RequestPort => typeof(RequestInfoExecutor),
- Type.Agent => typeof(AIAgentHostExecutor),
- Type.Function => this._configuredExecutorType!,
- Type.Workflow => this._configuredExecutorType!,
- _ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
- };
-
- ///
- /// Gets an that can be used to obtain an instance
- /// corresponding to this .
- ///
- private Func> ExecutorProvider => this.ExecutorType switch
- {
- Type.Unbound => throw new InvalidOperationException($"Executor with ID '{this.Id}' is unbound."),
- Type.Executor => this._configuredExecutor!.BoundFactoryAsync,
- Type.RequestPort => (runId) => new(new RequestInfoExecutor(this._requestPortValue!)),
- Type.Agent => (runId) => new(new AIAgentHostExecutor(this._aiAgentValue!)),
- Type.Function => this._configuredExecutor!.BoundFactoryAsync,
- Type.Workflow => this._configuredExecutor!.BoundFactoryAsync,
- _ => throw new InvalidOperationException($"Unknown ExecutorIsh type: {this.ExecutorType}")
- };
-
- ///
- /// Defines an implicit conversion from an instance to an object.
- ///
- /// The instance to convert to .
- public static implicit operator ExecutorIsh(Executor executor) => new(executor);
-
- ///
- /// Defines an implicit conversion from an to an instance.
- ///
- /// The to convert to an .
- public static implicit operator ExecutorIsh(RequestPort inputPort) => new(inputPort);
-
- ///
- /// Defines an implicit conversion from an to an instance.
- ///
- /// The to convert to an .
- public static implicit operator ExecutorIsh(AIAgent aiAgent) => new(aiAgent);
-
- ///
- /// Defines an implicit conversion from a string to an instance.
- ///
- /// The string ID to convert to an .
- public static implicit operator ExecutorIsh(string id) => new(id);
-
- ///
- public bool Equals(ExecutorIsh? other) =>
- other is not null && other.Id == this.Id;
-
- ///
- public bool Equals(IIdentified? other) =>
- other is not null && other.Id == this.Id;
-
- ///
- public bool Equals(string? other) =>
- other is not null && other == this.Id;
-
- ///
- public override bool Equals(object? obj) =>
- obj switch
- {
- null => false,
- ExecutorIsh ish => this.Equals(ish),
- IIdentified identified => this.Equals(identified),
- string str => this.Equals(str),
- _ => false
- };
-
- ///
- public override int GetHashCode() => this.Id.GetHashCode();
-
- ///
- public override string ToString() => this.ExecutorType switch
- {
- Type.Unbound => $"'{this.Id}':",
- Type.Executor => $"'{this.Id}':{this._configuredExecutorType!.Name}",
- Type.RequestPort => $"'{this.Id}':Input({this._requestPortValue!.Request.Name}->{this._requestPortValue!.Response.Name})",
- Type.Agent => $"{this.Id}':AIAgent(@{this._aiAgentValue!.GetType().Name})",
- Type.Function => $"'{this.Id}':{this._configuredExecutorType!.Name}",
- Type.Workflow => $"'{this.Id}':{this._configuredExecutorType!.Name}",
- _ => $"'{this.Id}':"
- };
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorPlaceholder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorPlaceholder.cs
new file mode 100644
index 0000000000..f3dc902839
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorPlaceholder.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents a placeholder entry for an , identified by a unique ID.
+///
+/// The unique identifier for the placeholder registration.
+public record ExecutorPlaceholder(string Id)
+ : ExecutorBinding(Id,
+ null,
+ typeof(Executor),
+ Id)
+{
+ ///
+ public override bool SupportsConcurrentSharedExecution => false;
+
+ ///
+ public override bool SupportsResetting => false;
+
+ ///
+ public override bool IsSharedInstance => false;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorRegistration.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorRegistration.cs
deleted file mode 100644
index eef749966c..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorRegistration.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System;
-using System.Threading.Tasks;
-using Microsoft.Shared.Diagnostics;
-
-using ExecutorFactoryF = System.Func>;
-
-namespace Microsoft.Agents.AI.Workflows;
-
-internal sealed class ExecutorRegistration(string id, Type executorType, ExecutorFactoryF provider, object? rawData)
-{
- public string Id { get; } = Throw.IfNullOrEmpty(id);
- public Type ExecutorType { get; } = Throw.IfNull(executorType);
- private ExecutorFactoryF ProviderAsync { get; } = Throw.IfNull(provider);
-
- public bool IsSharedInstance { get; } = rawData is Executor;
-
- ///
- /// Gets a value whether instances of the executor created from this registration can be reset between subsequent
- /// runs from the same instance. This value is not relevant for executors that .
- ///
- public bool SupportsResetting { get; } = rawData is Executor &&
- // Cross-Run Shareable executors are "trivially" resettable, since they
- // have no on-object state.
- rawData is IResettableExecutor;
-
- ///
- /// Gets a value whether instances of the executor created from this registration can be used in concurrent runs
- /// from the same instance.
- ///
- public bool SupportsConcurrent { get; } = rawData is not Executor executor || executor.IsCrossRunShareable;
-
- internal async ValueTask TryResetAsync()
- {
- // Non-shared instances do not need resetting
- if (!this.IsSharedInstance)
- {
- return true;
- }
-
- // Technically we definitely know this is true, since if rawData is an Executor, if it was not resettable
- // then we would have returned in the first condition, and if rawData is not an Executor, we would have
- // returned in the second condition. That only leaves the possibility of rawData is Executor and also
- // IResettableExecutor.
- if (this.RawExecutorishData is IResettableExecutor resettableExecutor)
- {
- await resettableExecutor.ResetAsync().ConfigureAwait(false);
- return true;
- }
-
- return false;
- }
-
- internal object? RawExecutorishData { get; } = rawData;
-
- public override string ToString() => $"{this.ExecutorType.Name}({this.Id})";
-
- private Executor CheckId(Executor executor)
- {
- if (executor.Id != this.Id)
- {
- throw new InvalidOperationException(
- $"Executor ID mismatch: expected '{this.Id}', but got '{executor.Id}'.");
- }
-
- return executor;
- }
-
- public async ValueTask CreateInstanceAsync(string runId) => this.CheckId(await this.ProviderAsync(runId).ConfigureAwait(false));
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/FunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/FunctionExecutor.cs
index 63db9456b4..a3371dc302 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/FunctionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/FunctionExecutor.cs
@@ -38,7 +38,9 @@ public class FunctionExecutor(string id,
///
/// A unique identifier for the executor.
/// A synchronous function to execute for each input message and workflow context.
- public FunctionExecutor(string id, Action handlerSync) : this(id, WrapAction(handlerSync))
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that this executor may be used simultaneously by multiple runs safely.
+ public FunctionExecutor(string id, Action handlerSync, ExecutorOptions? options = null, bool declareCrossRunShareable = false) : this(id, WrapAction(handlerSync), options, declareCrossRunShareable)
{
}
}
@@ -76,7 +78,9 @@ public class FunctionExecutor(string id,
///
/// A unique identifier for the executor.
/// A synchronous function to execute for each input message and workflow context.
- public FunctionExecutor(string id, Func handlerSync) : this(id, WrapFunc(handlerSync))
+ /// Configuration options for the executor. If null, default options will be used.
+ /// Declare that this executor may be used simultaneously by multiple runs safely.
+ public FunctionExecutor(string id, Func handlerSync, ExecutorOptions? options = null, bool declareCrossRunShareable = false) : this(id, WrapFunc(handlerSync), options, declareCrossRunShareable)
{
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
index 92b73083a9..c02a609f75 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/GroupChatWorkflowBuilder.cs
@@ -50,12 +50,12 @@ public sealed class GroupChatWorkflowBuilder
public Workflow Build()
{
AIAgent[] agents = this._participants.ToArray();
- Dictionary agentMap = agents.ToDictionary(a => a, a => (ExecutorIsh)new AgentRunStreamingExecutor(a, includeInputInOutput: true));
+ Dictionary agentMap = agents.ToDictionary(a => a, a => (ExecutorBinding)new AgentRunStreamingExecutor(a, includeInputInOutput: true));
Func> groupChatHostFactory =
(string id, string runId) => new(new GroupChatHost(id, agents, agentMap, this._managerFactory));
- ExecutorIsh host = groupChatHostFactory.ConfigureFactory(nameof(GroupChatHost));
+ ExecutorBinding host = groupChatHostFactory.BindExecutor(nameof(GroupChatHost));
WorkflowBuilder builder = new(host);
foreach (var participant in agentMap.Values)
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
index 874464812e..1750f779f2 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/InProc/InProcessRunnerContext.cs
@@ -73,7 +73,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
async Task CreateExecutorAsync(string id)
{
- if (!this._workflow.Registrations.TryGetValue(executorId, out var registration))
+ if (!this._workflow.ExecutorBindings.TryGetValue(executorId, out var registration))
{
throw new InvalidOperationException($"Executor with ID '{executorId}' is not registered.");
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/RequestPortBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/RequestPortBinding.cs
new file mode 100644
index 0000000000..726895a1a7
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/RequestPortBinding.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Workflows.Specialized;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents the registration details for a request port, including configuration for allowing wrapped requests.
+///
+/// The request port.
+/// true to allow wrapped requests to be handled by the port; otherwise, false.
+/// The default is true.
+public record RequestPortBinding(RequestPort Port, bool AllowWrapped = true)
+ : ExecutorBinding(Throw.IfNull(Port).Id,
+ (_) => new ValueTask(new RequestInfoExecutor(Port, AllowWrapped)),
+ typeof(RequestInfoExecutor),
+ Port)
+{
+ ///
+ public override bool IsSharedInstance => false;
+
+ ///
+ public override bool SupportsConcurrentSharedExecution => true;
+
+ ///
+ public override bool SupportsResetting => false;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs
index 16f749d5a3..76e3f10bd2 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/GroupChatHost.cs
@@ -10,11 +10,11 @@ namespace Microsoft.Agents.AI.Workflows.Specialized;
internal sealed class GroupChatHost(
string id,
AIAgent[] agents,
- Dictionary agentMap,
+ Dictionary agentMap,
Func, GroupChatManager> managerFactory) : Executor(id), IResettableExecutor
{
private readonly AIAgent[] _agents = agents;
- private readonly Dictionary _agentMap = agentMap;
+ private readonly Dictionary _agentMap = agentMap;
private readonly Func, GroupChatManager> _managerFactory = managerFactory;
private readonly List _pendingMessages = [];
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/SubworkflowBinding.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/SubworkflowBinding.cs
new file mode 100644
index 0000000000..1f29ffe426
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/SubworkflowBinding.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Workflows.Specialized;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows;
+
+///
+/// Represents the workflow binding details for a subworkflow, including its instance, identifier, and optional
+/// executor options.
+///
+///
+///
+///
+public record SubworkflowBinding(Workflow WorkflowInstance, string Id, ExecutorOptions? ExecutorOptions = null)
+ : ExecutorBinding(Throw.IfNull(Id),
+ CreateWorkflowExecutorFactory(WorkflowInstance, Id, ExecutorOptions),
+ typeof(WorkflowHostExecutor),
+ WorkflowInstance)
+{
+ private static Func> CreateWorkflowExecutorFactory(Workflow workflow, string id, ExecutorOptions? options)
+ {
+ object ownershipToken = new();
+ workflow.TakeOwnership(ownershipToken, subworkflow: true);
+
+ return InitHostExecutorAsync;
+
+ ValueTask InitHostExecutorAsync(string runId)
+ {
+ return new(new WorkflowHostExecutor(id, workflow, runId, ownershipToken, options));
+ }
+ }
+
+ ///
+ public override bool IsSharedInstance => false;
+
+ ///
+ public override bool SupportsConcurrentSharedExecution => true;
+
+ ///
+ public override bool SupportsResetting => false;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/SwitchBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/SwitchBuilder.cs
index 306acd4b4e..e180650935 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/SwitchBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/SwitchBuilder.cs
@@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Workflows;
///
public sealed class SwitchBuilder
{
- private readonly List _executors = [];
+ private readonly List _executors = [];
private readonly Dictionary _executorIndicies = [];
private readonly List<(Func