diff --git a/.github/workflows/python-merge-tests.yml b/.github/workflows/python-merge-tests.yml
index 92a5934f0e..bd5768b968 100644
--- a/.github/workflows/python-merge-tests.yml
+++ b/.github/workflows/python-merge-tests.yml
@@ -60,6 +60,8 @@ jobs:
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ ANTHROPIC_CHAT_MODEL_ID: ${{ vars.ANTHROPIC_CHAT_MODEL_ID }}
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 08532904c4..69d3e03d31 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -65,9 +65,11 @@
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 95812435d8..de8aef42fc 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -67,10 +67,17 @@
+
+
+
+
+
+
+
@@ -123,6 +130,7 @@
+
@@ -137,6 +145,8 @@
+
+
diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props
index c761802029..cc1c3b84ac 100644
--- a/dotnet/nuget/nuget-package.props
+++ b/dotnet/nuget/nuget-package.props
@@ -2,9 +2,9 @@
1.0.0
- $(VersionPrefix)-$(VersionSuffix).251028.1
- $(VersionPrefix)-preview.251028.1
- 1.0.0-preview.251028.1
+ $(VersionPrefix)-$(VersionSuffix).251104.1
+ $(VersionPrefix)-preview.251104.1
+ 1.0.0-preview.251104.1
Debug;Release;Publish
true
diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
new file mode 100644
index 0000000000..c6bab8327e
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
new file mode 100644
index 0000000000..3e86534edd
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG)
+// capabilities to an AI agent. The provider runs a search against an external knowledge base
+// before each model invocation and injects the results into the model context.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Data;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+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";
+
+TextSearchProviderOptions textSearchOptions = new()
+{
+ // Run the search prior to every model invocation and keep a short rolling window of conversation context.
+ SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
+ RecentMessageMemoryLimit = 6,
+};
+
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(new ChatClientAgentOptions
+ {
+ Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
+ AIContextProviderFactory = _ => new TextSearchProvider(MockSearchAsync, textSearchOptions)
+ });
+
+AgentThread thread = agent.GetNewThread();
+
+Console.WriteLine(">> Asking about returns\n");
+Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
+
+Console.WriteLine("\n>> Asking about shipping\n");
+Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
+
+Console.WriteLine("\n>> Asking about product care\n");
+Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
+
+static Task> MockSearchAsync(string query, CancellationToken cancellationToken)
+{
+ // The mock search inspects the user's question and returns pre-defined snippets
+ // that resemble documents stored in an external knowledge source.
+ List results = new();
+
+ if (query.Contains("return", StringComparison.OrdinalIgnoreCase) || query.Contains("refund", StringComparison.OrdinalIgnoreCase))
+ {
+ results.Add(new()
+ {
+ SourceName = "Contoso Outdoors Return Policy",
+ SourceLink = "https://contoso.com/policies/returns",
+ Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
+ });
+ }
+
+ if (query.Contains("shipping", StringComparison.OrdinalIgnoreCase))
+ {
+ results.Add(new()
+ {
+ SourceName = "Contoso Outdoors Shipping Guide",
+ SourceLink = "https://contoso.com/help/shipping",
+ Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
+ });
+ }
+
+ if (query.Contains("tent", StringComparison.OrdinalIgnoreCase) || query.Contains("fabric", StringComparison.OrdinalIgnoreCase))
+ {
+ results.Add(new()
+ {
+ SourceName = "TrailRunner Tent Care Instructions",
+ SourceLink = "https://contoso.com/manuals/trailrunner-tent",
+ Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
+ });
+ }
+
+ return Task.FromResult>(results);
+}
diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/README.md b/dotnet/samples/Catalog/AgentWithTextSearchRag/README.md
new file mode 100644
index 0000000000..614597bed9
--- /dev/null
+++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/README.md
@@ -0,0 +1,41 @@
+# What this sample demonstrates
+
+This sample demonstrates how to use TextSearchProvider to add retrieval augmented generation (RAG) capabilities to an AI agent. The provider runs a search against an external knowledge base before each model invocation and injects the results into the model context.
+
+Key features:
+- Configuring TextSearchProvider with custom search behavior
+- Running searches before AI invocations to provide relevant context
+- Managing conversation memory with a rolling window approach
+- Citing source documents in AI responses
+
+## Prerequisites
+
+Before running this sample, ensure you have:
+
+1. An Azure OpenAI endpoint configured
+2. A deployment of a chat model (e.g., gpt-4o-mini)
+3. Azure CLI installed and authenticated
+
+## Environment Variables
+
+Set the following environment variables:
+
+```powershell
+# Replace with your Azure OpenAI endpoint
+$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/"
+
+# Optional, defaults to gpt-4o-mini
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
+```
+
+## How It Works
+
+The sample uses a mock search function that demonstrates the RAG pattern:
+
+1. When the user asks a question, the TextSearchProvider intercepts it
+2. The search function looks for relevant documents based on the query
+3. Retrieved documents are injected into the model's context
+4. The AI responds using both its training and the provided context
+5. The agent can cite specific source documents in its answers
+
+The mock search function returns pre-defined snippets for demonstration purposes. In a production scenario, you would replace this with actual searches against your knowledge base (e.g., Azure AI Search, vector database, etc.).
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
diff --git a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
index 5edfcf53d9..dd5c6f9c7d 100644
--- a/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentOpenTelemetry/Program.cs
@@ -125,7 +125,7 @@ var agent = new ChatClientAgent(instrumentedChatClient,
instructions: "You are a helpful assistant that provides concise and informative responses.",
tools: [AIFunctionFactory.Create(GetWeatherAsync)])
.AsBuilder()
- .UseOpenTelemetry(SourceName) // enable telemetry at the agent level
+ .UseOpenTelemetry(SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
.Build();
var thread = agent.GetNewThread();
@@ -134,6 +134,8 @@ appLogger.LogInformation("Agent created successfully with ID: {AgentId}", agent.
// Create a parent span for the entire agent session
using var sessionActivity = activitySource.StartActivity("Agent Session");
+Console.WriteLine($"Trace ID: {sessionActivity?.TraceId} ");
+
var sessionId = Guid.NewGuid().ToString("N");
sessionActivity?
.SetTag("agent.name", "OpenTelemetryDemoAgent")
@@ -147,7 +149,7 @@ using (appLogger.BeginScope(new Dictionary { ["SessionId"] = ses
while (true)
{
- Console.Write("You: ");
+ Console.Write("You (or 'exit' to quit): ");
var userInput = Console.ReadLine();
if (string.IsNullOrWhiteSpace(userInput) || userInput.Equals("exit", StringComparison.OrdinalIgnoreCase))
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj
new file mode 100644
index 0000000000..0c8a9f2dfc
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
new file mode 100644
index 0000000000..611e11c22c
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG) capabilities to an AI agent.
+// The sample uses an In-Memory vector store, which can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
+// The TextSearchProvider runs a search against the vector store via the TextSearchStore before each model invocation and injects the results into the model context.
+// The TextSearchStore is a sample store implementation that hardcodes a storage schema and uses the vector store to store and retrieve documents.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Data;
+using Microsoft.Agents.AI.Samples;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.SemanticKernel.Connectors.InMemory;
+using OpenAI;
+
+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";
+var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
+
+AzureOpenAIClient azureOpenAIClient = new(
+ new Uri(endpoint),
+ new AzureCliCredential());
+
+// Create an In-Memory vector store that uses the Azure OpenAI embedding model to generate embeddings.
+VectorStore vectorStore = new InMemoryVectorStore(new()
+{
+ EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
+});
+
+// Create a store that defines a storage schema, and uses the vector store to store and retrieve documents.
+TextSearchStore textSearchStore = new(vectorStore, "product-and-policy-info", 3072);
+
+// Upload sample documents into the store.
+await textSearchStore.UpsertDocumentsAsync(GetSampleDocuments());
+
+// Create an adapter function that the TextSearchProvider can use to run searches against the TextSearchStore.
+Func>> SearchAdapter = async (text, ct) =>
+{
+ // Here we are limiting the search results to the single top result to demonstrate that we are accurately matching
+ // specific search results for each question, but in a real world case, more results should be used.
+ var searchResults = await textSearchStore.SearchAsync(text, 1, ct);
+ return searchResults.Select(r => new TextSearchProvider.TextSearchResult
+ {
+ SourceName = r.SourceName,
+ SourceLink = r.SourceLink,
+ Text = r.Text ?? string.Empty,
+ RawRepresentation = r
+ });
+};
+
+// Configure the options for the TextSearchProvider.
+TextSearchProviderOptions textSearchOptions = new()
+{
+ // Run the search prior to every model invocation.
+ SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
+};
+
+// Create the AI agent with the TextSearchProvider as the AI context provider.
+AIAgent agent = azureOpenAIClient
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(new ChatClientAgentOptions
+ {
+ Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
+ AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
+ ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
+ : new TextSearchProvider(SearchAdapter, textSearchOptions)
+ });
+
+AgentThread thread = agent.GetNewThread();
+
+Console.WriteLine(">> Asking about returns\n");
+Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
+
+Console.WriteLine("\n>> Asking about shipping\n");
+Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
+
+Console.WriteLine("\n>> Asking about product care\n");
+Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
+
+// Produces some sample search documents.
+// Each one contains a source name and link, which the agent can use to cite sources in its responses.
+static IEnumerable GetSampleDocuments()
+{
+ yield return new TextSearchDocument
+ {
+ SourceId = "return-policy-001",
+ SourceName = "Contoso Outdoors Return Policy",
+ SourceLink = "https://contoso.com/policies/returns",
+ Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
+ };
+ yield return new TextSearchDocument
+ {
+ SourceId = "shipping-guide-001",
+ SourceName = "Contoso Outdoors Shipping Guide",
+ SourceLink = "https://contoso.com/help/shipping",
+ Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
+ };
+ yield return new TextSearchDocument
+ {
+ SourceId = "tent-care-001",
+ SourceName = "TrailRunner Tent Care Instructions",
+ SourceLink = "https://contoso.com/manuals/trailrunner-tent",
+ Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
+ };
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs
new file mode 100644
index 0000000000..773d3ff6f3
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// Represents a document that can be used for Retrieval Augmented Generation (RAG) that stores textual data.
+///
+public sealed class TextSearchDocument
+{
+ ///
+ /// Gets or sets an optional list of namespaces that the document should belong to.
+ ///
+ ///
+ /// A namespace is a logical grouping of documents, e.g. may include a group id to scope the document to a specific group of users.
+ ///
+ public IList Namespaces { get; set; } = [];
+
+ ///
+ /// Gets or sets the content as text.
+ ///
+ public string? Text { get; set; }
+
+ ///
+ /// Gets or sets an optional source ID for the document.
+ ///
+ ///
+ /// This ID should be unique within the collection that the document is stored in, and can
+ /// be used to map back to the source artifact for this document.
+ /// If updates need to be made later or the source document was deleted and this document
+ /// also needs to be deleted, this id can be used to find the document again.
+ ///
+ public string? SourceId { get; set; }
+
+ ///
+ /// Gets or sets an optional name for the source document.
+ ///
+ ///
+ /// This can be used to provide display names for citation links when the document is referenced as
+ /// part of a response to a query.
+ ///
+ public string? SourceName { get; set; }
+
+ ///
+ /// Gets or sets an optional link back to the source of the document.
+ ///
+ ///
+ /// This can be used to provide citation links when the document is referenced as
+ /// part of a response to a query.
+ ///
+ public string? SourceLink { get; set; }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs
new file mode 100644
index 0000000000..502c17dba1
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs
@@ -0,0 +1,392 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq.Expressions;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.VectorData;
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// A class that allows for easy storage and retrieval of documents in a Vector Store for Retrieval Augmented Generation (RAG).
+///
+///
+///
+/// This class provides an opinionated schema for storing documents in a vector store. It is valuable for simple scenarios
+/// where you want to store text + embedding, or a reference to an external document + embedding without needing to customize the schema.
+/// If you want to control the schema yourself, use an implementation of directly instead.
+///
+///
+/// This class and its related types are currently provided as a sample implementation, but may be promoted to a first-class supported API in future releases.
+///
+///
+public sealed partial class TextSearchStore : IDisposable
+{
+#if NET
+ [GeneratedRegex(@"\p{L}+", RegexOptions.IgnoreCase, "en-US")]
+ private static partial Regex AnyLanguageWordRegex();
+
+ private static readonly Func> s_defaultWordSegmenter = text => AnyLanguageWordRegex().Matches(text).Select(x => x.Value).ToList();
+#else
+ private static readonly Regex s_anyLanguageWordRegex = new(@"\p{L}+", RegexOptions.Compiled);
+ private static Regex AnyLanguageWordRegex() => s_anyLanguageWordRegex;
+
+ private static readonly Func> s_defaultWordSegmenter = text =>
+ {
+ List words = new();
+ foreach (Match word in AnyLanguageWordRegex().Matches(text))
+ {
+ words.Add(word.Value);
+ }
+ return words;
+ };
+#endif
+
+ private readonly VectorStore _vectorStore;
+ private readonly TextSearchStoreOptions _options;
+ private readonly Func> _wordSegmenter;
+
+ private readonly VectorStoreCollection> _vectorStoreRecordCollection;
+ private readonly SemaphoreSlim _collectionInitializationLock = new(1, 1);
+ private bool _collectionInitialized;
+ private bool _disposedValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The vector store to store and read the memories from.
+ /// The name of the collection in the vector store to store and read the memories from.
+ /// The number of dimensions to use for the memory embeddings.
+ /// Options to configure the behavior of this class.
+ /// Thrown if the key type provided is not supported.
+ public TextSearchStore(
+ VectorStore vectorStore,
+ string collectionName,
+ int vectorDimensions,
+ TextSearchStoreOptions? options = default)
+ {
+ // Verify
+ if (vectorStore is null)
+ {
+ throw new ArgumentNullException(nameof(vectorStore));
+ }
+
+ if (string.IsNullOrWhiteSpace(collectionName))
+ {
+ throw new ArgumentException("Collection name cannot be null or whitespace.", nameof(collectionName));
+ }
+
+ if (vectorDimensions < 1)
+ {
+ throw new ArgumentOutOfRangeException(nameof(vectorDimensions), "Vector dimensions must be greater than zero.");
+ }
+
+ if (options?.KeyType is not null && options.KeyType != typeof(string) && options.KeyType != typeof(Guid))
+ {
+ throw new NotSupportedException($"Unsupported key of type '{options.KeyType.Name}'");
+ }
+
+ if (options?.KeyType is not null && options.KeyType != typeof(string) && options?.UseSourceIdAsPrimaryKey is true)
+ {
+ throw new NotSupportedException($"The {nameof(TextSearchStoreOptions.UseSourceIdAsPrimaryKey)} option can only be used when the key type is 'string'.");
+ }
+
+ // Assign
+ this._vectorStore = vectorStore;
+ this._options = options ?? new TextSearchStoreOptions();
+ this._wordSegmenter = this._options.WordSegmenter ?? s_defaultWordSegmenter;
+
+ // Create a definition so that we can use the dimensions provided at runtime.
+ VectorStoreCollectionDefinition ragDocumentDefinition = new()
+ {
+ Properties = new List()
+ {
+ new VectorStoreKeyProperty("Key", this._options.KeyType ?? typeof(string)),
+ new VectorStoreDataProperty("Namespaces", typeof(List)) { IsIndexed = true },
+ new VectorStoreDataProperty("SourceId", typeof(string)) { IsIndexed = true },
+ new VectorStoreDataProperty("Text", typeof(string)) { IsFullTextIndexed = true },
+ new VectorStoreDataProperty("SourceName", typeof(string)),
+ new VectorStoreDataProperty("SourceLink", typeof(string)),
+ new VectorStoreVectorProperty("TextEmbedding", typeof(string), vectorDimensions),
+ }
+ };
+
+ this._vectorStoreRecordCollection = this._vectorStore.GetDynamicCollection(collectionName, ragDocumentDefinition);
+ }
+
+ ///
+ /// Upserts a batch of text chunks into the vector store.
+ ///
+ /// The text chunks to upload.
+ /// The to monitor for cancellation requests. The default is .
+ /// A task that completes when the documents have been upserted.
+ public async Task UpsertTextAsync(IEnumerable textChunks, CancellationToken cancellationToken = default)
+ {
+ if (textChunks == null)
+ {
+ throw new ArgumentNullException(nameof(textChunks));
+ }
+
+ var vectorStoreRecordCollection = await this.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
+
+ var storageDocuments = textChunks.Select(textChunk =>
+ {
+ // Without text we cannot generate a vector.
+ if (string.IsNullOrWhiteSpace(textChunk))
+ {
+ throw new ArgumentException("One of the provided text chunks is null.", nameof(textChunks));
+ }
+
+ return new Dictionary
+ {
+ { "Key", this.GenerateUniqueKey(null) },
+ { "Namespaces", new List() },
+ { "Text", textChunk },
+ { "TextEmbedding", textChunk },
+ };
+ });
+
+ await vectorStoreRecordCollection.UpsertAsync(storageDocuments, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Upserts a batch of documents into the vector store.
+ ///
+ /// The documents to upload.
+ /// Optional options to control the upsert behavior.
+ /// The to monitor for cancellation requests. The default is .
+ /// A task that completes when the documents have been upserted.
+ public async Task UpsertDocumentsAsync(IEnumerable documents, TextSearchStoreUpsertOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ if (documents is null)
+ {
+ throw new ArgumentNullException(nameof(documents));
+ }
+
+ var vectorStoreRecordCollection = await this.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
+
+ var storageDocuments = documents.Select(document =>
+ {
+ if (document is null)
+ {
+ throw new ArgumentNullException(nameof(documents), "One of the provided documents is null.");
+ }
+
+ // Without text we cannot generate a vector.
+ if (string.IsNullOrWhiteSpace(document.Text))
+ {
+ throw new ArgumentException($"The {nameof(TextSearchDocument.Text)} property must be set.", nameof(document));
+ }
+
+ // If we aren't persisting the text, we need a source id or link to refer back to the original document.
+ if (options?.DoNotPersistSourceText is true && string.IsNullOrWhiteSpace(document.SourceId) && string.IsNullOrWhiteSpace(document.SourceLink))
+ {
+ throw new ArgumentException($"Either the {nameof(TextSearchDocument.SourceId)} or {nameof(TextSearchDocument.SourceLink)} properties must be set when the {nameof(TextSearchStoreUpsertOptions.DoNotPersistSourceText)} setting is true.", nameof(document));
+ }
+
+ var key = this.GenerateUniqueKey(this._options.UseSourceIdAsPrimaryKey ?? false ? document.SourceId : null);
+
+ return new Dictionary()
+ {
+ { "Key", key },
+ { "Namespaces", document.Namespaces.ToList() },
+ { "SourceId", document.SourceId },
+ { "Text", options?.DoNotPersistSourceText is true ? null : document.Text },
+ { "SourceName", document.SourceName },
+ { "SourceLink", document.SourceLink },
+ { "TextEmbedding", document.Text },
+ };
+ });
+
+ await vectorStoreRecordCollection.UpsertAsync(storageDocuments, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Search the database for documents similar to the provided query.
+ ///
+ /// The text query to find similar documents to.
+ /// The maximum number of results to return.
+ /// The to monitor for cancellation requests. The default is .
+ /// The search results.
+ public async Task> SearchAsync(string query, int top, CancellationToken cancellationToken = default)
+ {
+ var searchResult = await this.SearchCoreAsync(query, top, cancellationToken).ConfigureAwait(false);
+
+ return searchResult.Select(x => new TextSearchDocument()
+ {
+ Namespaces = (List)x["Namespaces"]!,
+ Text = (string?)x["Text"],
+ SourceId = (string?)x["SourceId"],
+ SourceName = (string?)x["SourceName"],
+ SourceLink = (string?)x["SourceLink"],
+ });
+ }
+
+ ///
+ /// Internal search implementation with hydration of id / link only storage.
+ ///
+ /// The text query to find similar documents to.
+ /// The maximum number of results to return.
+ /// The to monitor for cancellation requests. The default is .
+ /// The search results.
+ private async Task>> SearchCoreAsync(string query, int top, CancellationToken cancellationToken = default)
+ {
+ // Short circuit if the query is empty.
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return [];
+ }
+
+ var vectorStoreRecordCollection = await this.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
+
+ // If the user has not opted out of hybrid search, check if the vector store supports it.
+ var hybridSearchCollection = this._options.UseHybridSearch ?? true ?
+ vectorStoreRecordCollection.GetService(typeof(IKeywordHybridSearchable>)) as IKeywordHybridSearchable> :
+ null;
+
+ // Optional filter to limit the search to a specific namespace.
+ Expression, bool>>? filter = string.IsNullOrWhiteSpace(this._options.SearchNamespace) ? null : x => ((List)x["Namespaces"]!).Contains(this._options.SearchNamespace);
+
+ // Execute a hybrid search if possible, otherwise perform a regular vector search.
+ var searchResult = hybridSearchCollection is null
+ ? vectorStoreRecordCollection.SearchAsync(
+ query,
+ top,
+ options: new()
+ {
+ Filter = filter,
+ },
+ cancellationToken: cancellationToken)
+ : hybridSearchCollection.HybridSearchAsync(
+ query,
+ this._wordSegmenter(query),
+ top,
+ options: new()
+ {
+ Filter = filter,
+ },
+ cancellationToken: cancellationToken);
+
+ // Retrieve the documents from the search results.
+ List> searchResponseDocs = new();
+ await foreach (var searchResponseDoc in searchResult.WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ searchResponseDocs.Add(searchResponseDoc.Record);
+ }
+
+ // Find any source ids and links for which the text needs to be retrieved.
+ var sourceIdsToRetrieve = searchResponseDocs
+ .Where(x => string.IsNullOrWhiteSpace((string?)x["Text"]))
+ .Select(x => new TextSearchStoreOptions.SourceRetrievalRequest((string?)x["SourceId"], (string?)x["SourceLink"]))
+ .ToList();
+
+ // If we have none, we can return early.
+ if (sourceIdsToRetrieve.Count == 0)
+ {
+ return searchResponseDocs;
+ }
+
+ if (this._options.SourceRetrievalCallback is null)
+ {
+ throw new InvalidOperationException($"The {nameof(TextSearchStoreOptions.SourceRetrievalCallback)} option must be set if retrieving documents without stored text.");
+ }
+
+ // Retrieve the source text for the documents that need it.
+ var retrievalResponses = await this._options.SourceRetrievalCallback(sourceIdsToRetrieve).ConfigureAwait(false);
+
+ if (retrievalResponses is null)
+ {
+ throw new InvalidOperationException($"The {nameof(TextSearchStoreOptions.SourceRetrievalCallback)} must return a non-null value.");
+ }
+
+ // Update the retrieved documents with the retrieved text.
+ return searchResponseDocs.GroupJoin(
+ retrievalResponses,
+ searchResponseDoc => (searchResponseDoc["SourceId"], searchResponseDoc["SourceLink"]),
+ retrievalResponse => (retrievalResponse.SourceId, retrievalResponse.SourceLink),
+ (searchResponseDoc, textRetrievalResponse) => (searchResponseDoc, textRetrievalResponse))
+ .SelectMany(
+ joinedSet => joinedSet.textRetrievalResponse.DefaultIfEmpty(),
+ (combined, textRetrievalResponse) =>
+ {
+ combined.searchResponseDoc["Text"] = textRetrievalResponse?.Text ?? combined.searchResponseDoc["Text"];
+ return combined.searchResponseDoc;
+ });
+ }
+
+ ///
+ /// Thread safe method to get the collection and ensure that it is created at least once.
+ ///
+ /// The to monitor for cancellation requests. The default is .
+ /// The created collection.
+ private async Task>> EnsureCollectionExistsAsync(CancellationToken cancellationToken)
+ {
+ // Return immediately if the collection is already created, no need to do any locking in this case.
+ if (this._collectionInitialized)
+ {
+ return this._vectorStoreRecordCollection;
+ }
+
+ // Wait on a lock to ensure that only one thread can create the collection.
+ await this._collectionInitializationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ // If multiple threads waited on the lock, and the first already created the collection,
+ // we can return immediately without doing any work in subsequent threads.
+ if (this._collectionInitialized)
+ {
+ this._collectionInitializationLock.Release();
+ return this._vectorStoreRecordCollection;
+ }
+
+ // Only the winning thread should reach this point and create the collection.
+ try
+ {
+ await this._vectorStoreRecordCollection.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
+ this._collectionInitialized = true;
+ }
+ finally
+ {
+ this._collectionInitializationLock.Release();
+ }
+
+ return this._vectorStoreRecordCollection;
+ }
+
+ ///
+ /// Generates a unique key for the RAG document.
+ ///
+ /// Source id of the source document for this RAG document.
+ /// A new unique key.
+ /// Thrown if the requested key type is not supported.
+ private object GenerateUniqueKey(string? sourceId)
+ => this._options.KeyType switch
+ {
+ _ when (this._options.KeyType == null || this._options.KeyType == typeof(string)) && !string.IsNullOrWhiteSpace(sourceId) => sourceId!,
+ _ when this._options.KeyType == null || this._options.KeyType == typeof(string) => Guid.NewGuid().ToString(),
+ _ when this._options.KeyType == typeof(Guid) => Guid.NewGuid(),
+
+ _ => throw new NotSupportedException($"Unsupported key of type '{this._options.KeyType.Name}'")
+ };
+
+ ///
+ private void Dispose(bool disposing)
+ {
+ if (!this._disposedValue)
+ {
+ if (disposing)
+ {
+ this._vectorStoreRecordCollection.Dispose();
+ this._collectionInitializationLock.Dispose();
+ }
+
+ this._disposedValue = true;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ this.Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreOptions.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreOptions.cs
new file mode 100644
index 0000000000..53da092c82
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreOptions.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// Contains options for the .
+///
+public sealed class TextSearchStoreOptions
+{
+ ///
+ /// Gets or sets an optional namespace to pre-filter the possible
+ /// records with when doing a vector search.
+ ///
+ public string? SearchNamespace { get; init; }
+
+ ///
+ /// Gets or sets a value indicating whether to use the source ID as the primary key for records.
+ ///
+ ///
+ ///
+ /// Using the source ID as the primary key allows for easy updates from the source for any changed
+ /// records, since those records can just be upserted again, and will overwrite the previous version
+ /// of the same record.
+ ///
+ ///
+ /// This setting can only be used when the chosen key type is a string.
+ ///
+ ///
+ ///
+ /// Defaults to false if not set.
+ ///
+ public bool? UseSourceIdAsPrimaryKey { get; init; }
+
+ ///
+ /// Gets or sets a value indicating whether to use hybrid search if it is available for the provided vector store.
+ ///
+ ///
+ /// Defaults to true if not set.
+ ///
+ public bool? UseHybridSearch { get; init; }
+
+ ///
+ /// Gets or sets a word segmenter function to split search text into separate words for the purposes of hybrid search.
+ /// This will not be used if is set to false .
+ ///
+ ///
+ /// Defaults to a simple text-character-based segmenter that splits the text by any character that is not a text character.
+ ///
+ public Func>? WordSegmenter { get; init; }
+
+ ///
+ /// Gets or sets the type of key to use for records in the text search store.
+ ///
+ ///
+ /// Make sure to pick a key type that is supported by the underlying vector store.
+ /// Note that you have to choose when using .
+ ///
+ /// Defaults to if not set. Only and is currently supported.
+ public Type? KeyType { get; init; }
+
+ ///
+ /// Gets or sets an optional callback to load the source text using the source id or source link
+ /// if the source text is not persisted in the database.
+ ///
+ ///
+ /// The response should include the source id or source link, as provided in the request,
+ /// plus the source text loaded from the source.
+ ///
+ public Func, Task>>? SourceRetrievalCallback { get; init; }
+
+ ///
+ /// Represents a request to the .
+ ///
+ public sealed class SourceRetrievalRequest
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source ID of the document to retrieve.
+ /// The source link of the document to retrieve.
+ public SourceRetrievalRequest(string? sourceId, string? sourceLink)
+ {
+ this.SourceId = sourceId;
+ this.SourceLink = sourceLink;
+ }
+
+ ///
+ /// Gets or sets the source ID of the document to retrieve.
+ ///
+ public string? SourceId { get; set; }
+
+ ///
+ /// Gets or sets the source link of the document to retrieve.
+ ///
+ public string? SourceLink { get; set; }
+ }
+
+ ///
+ /// Represents a response from the .
+ ///
+ public sealed class SourceRetrievalResponse
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The request matching this response.
+ /// The source text that was retrieved.
+ public SourceRetrievalResponse(SourceRetrievalRequest request, string text)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ if (text == null)
+ {
+ throw new ArgumentNullException(nameof(text));
+ }
+
+ this.SourceId = request.SourceId;
+ this.SourceLink = request.SourceLink;
+ this.Text = text;
+ }
+
+ ///
+ /// Gets or sets the source ID of the document that was retrieved.
+ ///
+ public string? SourceId { get; set; }
+
+ ///
+ /// Gets or sets the source link of the document that was retrieved.
+ ///
+ public string? SourceLink { get; set; }
+
+ ///
+ /// Gets or sets the source text of the document that was retrieved.
+ ///
+ public string Text { get; set; }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreUpsertOptions.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreUpsertOptions.cs
new file mode 100644
index 0000000000..127d7de01e
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreUpsertOptions.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// Contains options for .
+///
+public sealed class TextSearchStoreUpsertOptions
+{
+ ///
+ /// Gets or sets a value indicating whether the source text should be persisted in the database.
+ ///
+ ///
+ /// Defaults to if not set.
+ ///
+ public bool DoNotPersistSourceText { get; init; }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj
new file mode 100644
index 0000000000..56e2ad232b
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
new file mode 100644
index 0000000000..e29bb58d04
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
@@ -0,0 +1,134 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use Qdrant to add retrieval augmented generation (RAG) capabilities to an AI agent.
+// While the sample is using Qdrant, it can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
+// The TextSearchProvider runs a search against the vector store before each model invocation and injects the results into the model context.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Data;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.SemanticKernel.Connectors.Qdrant;
+using OpenAI;
+using Qdrant.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";
+var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
+var afOverviewUrl = "https://github.com/MicrosoftDocs/semantic-kernel-docs/blob/main/agent-framework/overview/agent-framework-overview.md";
+var afMigrationUrl = "https://raw.githubusercontent.com/MicrosoftDocs/semantic-kernel-docs/refs/heads/main/agent-framework/migration-guide/from-semantic-kernel/index.md";
+
+AzureOpenAIClient azureOpenAIClient = new(
+ new Uri(endpoint),
+ new AzureCliCredential());
+
+// Create a Qdrant vector store that uses the Azure OpenAI embedding model to generate embeddings.
+QdrantClient client = new("localhost");
+VectorStore vectorStore = new QdrantVectorStore(client, ownsClient: true, new()
+{
+ EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
+});
+
+// Create a collection and upsert some text into it.
+var documentationCollection = vectorStore.GetCollection("documentation");
+await documentationCollection.EnsureCollectionDeletedAsync(); // Clear out any data from previous runs.
+await documentationCollection.EnsureCollectionExistsAsync();
+await UploadDataFromMarkdown(afOverviewUrl, "Microsoft Agent Framework Overview", documentationCollection, 2000, 200);
+await UploadDataFromMarkdown(afMigrationUrl, "Semantic Kernel to Microsoft Agent Framework Migration Guide", documentationCollection, 2000, 200);
+
+// Create an adapter function that the TextSearchProvider can use to run searches against the collection.
+Func>> SearchAdapter = async (text, ct) =>
+{
+ List results = [];
+ await foreach (var result in documentationCollection.SearchAsync(text, 5, cancellationToken: ct))
+ {
+ results.Add(new TextSearchProvider.TextSearchResult
+ {
+ SourceName = result.Record.SourceName,
+ SourceLink = result.Record.SourceLink,
+ Text = result.Record.Text ?? string.Empty,
+ RawRepresentation = result
+ });
+ }
+ return results;
+};
+
+// Configure the options for the TextSearchProvider.
+TextSearchProviderOptions textSearchOptions = new()
+{
+ // Run the search prior to every model invocation.
+ SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
+ // Use up to 4 recent messages when searching so that searches
+ // still produce valuable results even when the user is referring
+ // back to previous messages in their request.
+ RecentMessageMemoryLimit = 5
+};
+
+// Create the AI agent with the TextSearchProvider as the AI context provider.
+AIAgent agent = azureOpenAIClient
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(new ChatClientAgentOptions
+ {
+ Instructions = "You are a helpful support specialist for the Microsoft Agent Framework. Answer questions using the provided context and cite the source document when available. Keep responses brief.",
+ AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
+ ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
+ : new TextSearchProvider(SearchAdapter, textSearchOptions)
+ });
+
+AgentThread thread = agent.GetNewThread();
+
+Console.WriteLine(">> Asking about SK threads\n");
+Console.WriteLine(await agent.RunAsync("Hi! How do I create a thread in Semantic Kernel?", thread));
+
+// Here we are asking a very vague question when taken out of context,
+// but since we are including previous messages in our search using RecentMessageMemoryLimit
+// the RAG search should still produce useful results.
+Console.WriteLine("\n>> Asking about AF threads\n");
+Console.WriteLine(await agent.RunAsync("and in Agent Framework?", thread));
+
+Console.WriteLine("\n>> Contrasting Approaches\n");
+Console.WriteLine(await agent.RunAsync("Please contrast the two approaches", thread));
+
+Console.WriteLine("\n>> Asking about ancestry\n");
+Console.WriteLine(await agent.RunAsync("What are the predecessors to the Agent Framework?", thread));
+
+static async Task UploadDataFromMarkdown(string markdownUrl, string sourceName, VectorStoreCollection vectorStoreCollection, int chunkSize, int overlap)
+{
+ // Download the markdown from the given url.
+ using HttpClient client = new();
+ var markdown = await client.GetStringAsync(new Uri(markdownUrl));
+
+ // Chunk it into separate parts with some overlap between chunks
+ var chunks = new List();
+ for (int i = 0; i < markdown.Length; i += chunkSize)
+ {
+ var chunk = new DocumentationChunk
+ {
+ Key = Guid.NewGuid(),
+ SourceLink = markdownUrl,
+ SourceName = sourceName,
+ Text = markdown.Substring(i, Math.Min(chunkSize + overlap, markdown.Length - i))
+ };
+ chunks.Add(chunk);
+ }
+
+ // Upsert each chunk into the provided vector store.
+ await vectorStoreCollection.UpsertAsync(chunks);
+}
+
+// Data model that defines the database schema we want to use.
+internal sealed class DocumentationChunk
+{
+ [VectorStoreKey]
+ public Guid Key { get; set; }
+ [VectorStoreData]
+ public string SourceLink { get; set; } = string.Empty;
+ [VectorStoreData]
+ public string SourceName { get; set; } = string.Empty;
+ [VectorStoreData]
+ public string Text { get; set; } = string.Empty;
+ [VectorStoreVector(Dimensions: 3072)]
+ public string Embedding => this.Text;
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/README.md b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/README.md
new file mode 100644
index 0000000000..1817f0d8ca
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/README.md
@@ -0,0 +1,60 @@
+# Agent Framework Retrieval Augmented Generation (RAG) with an external Vector Store with a custom schema
+
+This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with an external vector store.
+It also uses a custom schema for the documents stored in the vector store.
+This sample uses Qdrant for the vector store, but this can easily be swapped out for any vector store that has a Microsoft.Extensions.VectorStore implementation.
+
+## Prerequisites
+
+- .NET 8.0 SDK or later
+- Azure OpenAI service endpoint
+- Both a chat completion and embedding deployment configured in the Azure OpenAI resource
+- Azure CLI installed and authenticated (for Azure credential authentication)
+- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
+- An existing Qdrant instance. You can use a managed service or run a local instance using Docker, but the sample assumes the instance is running locally.
+
+**Note**: These samples use Azure OpenAI models. For more information, see [how to deploy Azure OpenAI models with Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-openai).
+
+**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource and have the `Cognitive Services OpenAI Contributor` role. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively).
+
+## Running the sample from the console
+
+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
+$env:AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME="text-embedding-3-large" # Optional, defaults to text-embedding-3-large
+```
+
+If the variables are not set, you will be prompted for the values when running the samples.
+
+To use Qdrant in docker locally, start your Qdrant instance using the default port mappings.
+
+```powershell
+docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
+```
+
+Execute the following command to build the sample:
+
+```powershell
+dotnet build
+```
+
+Execute the following command to run the sample:
+
+```powershell
+dotnet run --no-build
+```
+
+Or just build and run in one step:
+
+```powershell
+dotnet run
+```
+
+## Running the sample from Visual Studio
+
+Open the solution in Visual Studio and set the sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`.
+
+You will be prompted for any required environment variables if they are not already set.
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/README.md b/dotnet/samples/GettingStarted/AgentWithRAG/README.md
new file mode 100644
index 0000000000..f45c2c2540
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/README.md
@@ -0,0 +1,8 @@
+# Agent Framework Retrieval Augmented Generation (RAG)
+
+These samples show how to create an agent with the Agent Framework that uses Retrieval Augmented Generation (RAG) to enhance its responses with information from a knowledge base.
+
+|Sample|Description|
+|---|---|
+|[Basic Text RAG](./AgentWithRAG_Step01_BasicTextRAG/)|This sample demonstrates how to create and run a basic agent with simple text Retrieval Augmented Generation (RAG).|
+|[RAG with external Vector Store and custom schema](./AgentWithRAG_Step02_ExternalDataSourceRAG/)|This sample demonstrates how to create and run an agent that uses Retrieval Augmented Generation (RAG) with an external vector store. It also uses a custom schema for the documents stored in the vector store.|
diff --git a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
index 931ce50014..81a6b29152 100644
--- a/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_TextSearchRag/Program.cs
@@ -28,7 +28,9 @@ AIAgent agent = new AzureOpenAIClient(
.CreateAIAgent(new ChatClientAgentOptions
{
Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
- AIContextProviderFactory = _ => new TextSearchProvider(MockSearchAsync, textSearchOptions)
+ AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
+ ? new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
+ : new TextSearchProvider(MockSearchAsync, textSearchOptions)
});
AgentThread thread = agent.GetNewThread();
@@ -52,9 +54,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "Contoso Outdoors Return Policy",
- Link = "https://contoso.com/policies/returns",
- Value = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
+ SourceName = "Contoso Outdoors Return Policy",
+ SourceLink = "https://contoso.com/policies/returns",
+ Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
});
}
@@ -62,9 +64,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "Contoso Outdoors Shipping Guide",
- Link = "https://contoso.com/help/shipping",
- Value = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
+ SourceName = "Contoso Outdoors Shipping Guide",
+ SourceLink = "https://contoso.com/help/shipping",
+ Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
});
}
@@ -72,9 +74,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "TrailRunner Tent Care Instructions",
- Link = "https://contoso.com/manuals/trailrunner-tent",
- Value = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
+ SourceName = "TrailRunner Tent Care Instructions",
+ SourceLink = "https://contoso.com/manuals/trailrunner-tent",
+ Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
});
}
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
index 32017af194..f824f09991 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/FoundryAgent_Hosted_MCP/Program.cs
@@ -1,52 +1,106 @@
// Copyright (c) Microsoft. All rights reserved.
-// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend.
+// This sample shows how to create and use a simple AI agent with Azure Foundry Agents as the backend, that uses a Hosted MCP Tool.
+// In this case the Azure Foundry Agents service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
+// The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool.
using Azure.AI.Agents.Persistent;
using Azure.Identity;
using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
-var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_MODEL_ID") ?? "gpt-4.1-mini";
+var model = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4.1-mini";
// Get a client to create/retrieve server side agents with.
var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential());
+// **** MCP Tool with Auto Approval ****
+// *************************************
+
// Create an MCP tool definition that the agent can use.
-var mcpTool = new MCPToolDefinition(
- serverLabel: "microsoft_learn",
- serverUrl: "https://learn.microsoft.com/api/mcp");
-mcpTool.AllowedTools.Add("microsoft_docs_search");
-
-// Create a server side persistent agent with the Azure.AI.Agents.Persistent SDK.
-var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
- model: model,
- name: "MicrosoftLearnAgent",
- instructions: "You answer questions by searching the Microsoft Learn content only.",
- tools: [mcpTool]);
-
-// Retrieve an already created server side persistent agent as an AIAgent.
-AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);
-
-// Create run options to configure the agent invocation.
-var runOptions = new ChatClientAgentRunOptions()
+// In this case we allow the tool to always be called without approval.
+var mcpTool = new HostedMcpServerTool(
+ serverName: "microsoft_learn",
+ serverAddress: "https://learn.microsoft.com/api/mcp")
{
- ChatOptions = new()
- {
- RawRepresentationFactory = (_) => new ThreadAndRunOptions()
- {
- ToolResources = new MCPToolResource(serverLabel: "microsoft_learn")
- {
- RequireApproval = new MCPApproval("never"),
- }.ToToolResources()
- }
- }
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
};
+// Create a server side persistent agent with the mcp tool, and expose it as an AIAgent.
+AIAgent agent = await persistentAgentsClient.CreateAIAgentAsync(
+ model: model,
+ options: new()
+ {
+ Name = "MicrosoftLearnAgent",
+ Instructions = "You answer questions by searching the Microsoft Learn content only.",
+ ChatOptions = new()
+ {
+ Tools = [mcpTool]
+ },
+ });
+
// You can then invoke the agent like any other AIAgent.
AgentThread thread = agent.GetNewThread();
-var response = await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", thread, runOptions);
-Console.WriteLine(response);
+Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", thread));
// Cleanup for sample purposes.
await persistentAgentsClient.Administration.DeleteAgentAsync(agent.Id);
+
+// **** MCP Tool with Approval Required ****
+// *****************************************
+
+// Create an MCP tool definition that the agent can use.
+// In this case we require approval before the tool can be called.
+var mcpToolWithApproval = new HostedMcpServerTool(
+ serverName: "microsoft_learn",
+ serverAddress: "https://learn.microsoft.com/api/mcp")
+{
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire
+};
+
+// Create an agent based on Azure OpenAI Responses as the backend.
+AIAgent agentWithRequiredApproval = await persistentAgentsClient.CreateAIAgentAsync(
+ model: model,
+ options: new()
+ {
+ Name = "MicrosoftLearnAgentWithApproval",
+ Instructions = "You answer questions by searching the Microsoft Learn content only.",
+ ChatOptions = new()
+ {
+ Tools = [mcpToolWithApproval]
+ },
+ });
+
+// You can then invoke the agent like any other AIAgent.
+var threadWithRequiredApproval = agentWithRequiredApproval.GetNewThread();
+var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", threadWithRequiredApproval);
+var userInputRequests = response.UserInputRequests.ToList();
+
+while (userInputRequests.Count > 0)
+{
+ // Ask the user to approve each MCP call request.
+ // For simplicity, we are assuming here that only MCP approval requests are being made.
+ var userInputResponses = userInputRequests
+ .OfType()
+ .Select(approvalRequest =>
+ {
+ Console.WriteLine($"""
+ The agent would like to invoke the following MCP Tool, please reply Y to approve.
+ ServerName: {approvalRequest.ToolCall.ServerName}
+ Name: {approvalRequest.ToolCall.ToolName}
+ Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
+ """);
+ return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
+ })
+ .ToList();
+
+ // Pass the user input responses back to the agent for further processing.
+ response = await agentWithRequiredApproval.RunAsync(userInputResponses, threadWithRequiredApproval);
+
+ userInputRequests = response.UserInputRequests.ToList();
+}
+
+Console.WriteLine($"\nAgent: {response}");
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/README.md b/dotnet/samples/GettingStarted/ModelContextProtocol/README.md
index be84bff51f..874afa28b8 100644
--- a/dotnet/samples/GettingStarted/ModelContextProtocol/README.md
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/README.md
@@ -21,6 +21,7 @@ Before you begin, ensure you have the following prerequisites:
|---|---|
|[Agent with MCP server tools](./Agent_MCP_Server/)|This sample demonstrates how to use MCP server tools with a simple agent|
|[Agent with MCP server tools and authorization](./Agent_MCP_Server_Auth/)|This sample demonstrates how to use MCP Server tools from a protected MCP server with a simple agent|
+|[Responses Agent with Hosted MCP tool](./ResponseAgent_Hosted_MCP/)|This sample demonstrates how to use the Hosted MCP tool with the Responses Service, where the service invokes any MCP tools directly|
## Running the samples from the console
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs
new file mode 100644
index 0000000000..19793e64df
--- /dev/null
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/Program.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create and use a simple AI agent with OpenAI Responses as the backend, that uses a Hosted MCP Tool.
+// In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
+// The sample first shows how to use MCP tools with auto approval, and then how to set up a tool that requires approval before it can be invoked and how to approve such a tool.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+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";
+
+// **** MCP Tool with Auto Approval ****
+// *************************************
+
+// Create an MCP tool definition that the agent can use.
+// In this case we allow the tool to always be called without approval.
+var mcpTool = new HostedMcpServerTool(
+ serverName: "microsoft_learn",
+ serverAddress: "https://learn.microsoft.com/api/mcp")
+{
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
+};
+
+// Create an agent based on Azure OpenAI Responses as the backend.
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetOpenAIResponseClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You answer questions by searching the Microsoft Learn content only.",
+ name: "MicrosoftLearnAgent",
+ tools: [mcpTool]);
+
+// You can then invoke the agent like any other AIAgent.
+AgentThread thread = agent.GetNewThread();
+Console.WriteLine(await agent.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", thread));
+
+// **** MCP Tool with Approval Required ****
+// *****************************************
+
+// Create an MCP tool definition that the agent can use.
+// In this case we require approval before the tool can be called.
+var mcpToolWithApproval = new HostedMcpServerTool(
+ serverName: "microsoft_learn",
+ serverAddress: "https://learn.microsoft.com/api/mcp")
+{
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire
+};
+
+// Create an agent based on Azure OpenAI Responses as the backend.
+AIAgent agentWithRequiredApproval = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetOpenAIResponseClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You answer questions by searching the Microsoft Learn content only.",
+ name: "MicrosoftLearnAgentWithApproval",
+ tools: [mcpToolWithApproval]);
+
+// You can then invoke the agent like any other AIAgent.
+var threadWithRequiredApproval = agentWithRequiredApproval.GetNewThread();
+var response = await agentWithRequiredApproval.RunAsync("Please summarize the Azure AI Agent documentation related to MCP Tool calling?", threadWithRequiredApproval);
+var userInputRequests = response.UserInputRequests.ToList();
+
+while (userInputRequests.Count > 0)
+{
+ // Ask the user to approve each MCP call request.
+ // For simplicity, we are assuming here that only MCP approval requests are being made.
+ var userInputResponses = userInputRequests
+ .OfType()
+ .Select(approvalRequest =>
+ {
+ Console.WriteLine($"""
+ The agent would like to invoke the following MCP Tool, please reply Y to approve.
+ ServerName: {approvalRequest.ToolCall.ServerName}
+ Name: {approvalRequest.ToolCall.ToolName}
+ Arguments: {string.Join(", ", approvalRequest.ToolCall.Arguments?.Select(x => $"{x.Key}: {x.Value}") ?? [])}
+ """);
+ return new ChatMessage(ChatRole.User, [approvalRequest.CreateResponse(Console.ReadLine()?.Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false)]);
+ })
+ .ToList();
+
+ // Pass the user input responses back to the agent for further processing.
+ response = await agentWithRequiredApproval.RunAsync(userInputResponses, threadWithRequiredApproval);
+
+ userInputRequests = response.UserInputRequests.ToList();
+}
+
+Console.WriteLine($"\nAgent: {response}");
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/README.md b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/README.md
new file mode 100644
index 0000000000..f84bd8f1b4
--- /dev/null
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/README.md
@@ -0,0 +1,17 @@
+# 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)
+- User has the `Cognitive Services OpenAI Contributor` role for the Azure OpenAI resource.
+
+**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-4.1-mini" # Optional, defaults to gpt-4.1-mini
+```
diff --git a/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj
new file mode 100644
index 0000000000..0eacdab258
--- /dev/null
+++ b/dotnet/samples/GettingStarted/ModelContextProtocol/ResponseAgent_Hosted_MCP/ResponseAgent_Hosted_MCP.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs
index 75f0beb2da..6aa65d56b5 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/Program.cs
@@ -6,7 +6,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
-namespace WorkflowAsAnAgentsSample;
+namespace WorkflowAsAnAgentSample;
///
/// This sample introduces the concepts workflows as agents, where a workflow can be
@@ -61,9 +61,9 @@ public static class Program
Dictionary> buffer = [];
await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread))
{
- if (update.MessageId is null)
+ if (update.MessageId is null || string.IsNullOrEmpty(update.Text))
{
- // skip updates that don't have a message ID
+ // skip updates that don't have a message ID or text
continue;
}
Console.Clear();
diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
index 7d8768c226..736c51d555 100644
--- a/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs
@@ -4,7 +4,7 @@ using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
-namespace WorkflowAsAnAgentsSample;
+namespace WorkflowAsAnAgentSample;
internal static class WorkflowFactory
{
@@ -41,44 +41,43 @@ internal static class WorkflowFactory
///
/// Executor that starts the concurrent processing by sending messages to the agents.
///
- private sealed class ConcurrentStartExecutor() :
- Executor>("ConcurrentStartExecutor")
+ private sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
{
- ///
- /// Starts the concurrent processing by sending messages to the agents.
- ///
- /// The user message to process
- /// Workflow context for accessing workflow services and adding events
- /// The to monitor for cancellation requests.
- /// The default is .
- public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
{
- // Broadcast the message to all connected agents. Receiving agents will queue
- // the message but will not start processing until they receive a turn token.
- await context.SendMessageAsync(message, cancellationToken: cancellationToken);
- // Broadcast the turn token to kick off the agents.
- await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
+ return routeBuilder
+ .AddHandler>(this.RouteMessages)
+ .AddHandler(this.RouteTurnTokenAsync);
+ }
+
+ private ValueTask RouteMessages(List messages, IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
+ }
+
+ private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ return context.SendMessageAsync(token, cancellationToken: cancellationToken);
}
}
///
/// Executor that aggregates the results from the concurrent agents.
///
- private sealed class ConcurrentAggregationExecutor() :
- Executor("ConcurrentAggregationExecutor")
+ private sealed class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor")
{
private readonly List _messages = [];
///
/// Handles incoming messages from the agents and aggregates their responses.
///
- /// The message from the agent
+ /// The messages from the agent
/// Workflow context for accessing workflow services and adding events
/// The to monitor for cancellation requests.
/// The default is .
- public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
- this._messages.Add(message);
+ this._messages.AddRange(message);
if (this._messages.Count == 2)
{
diff --git a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
index dc91338987..e1f71e2311 100644
--- a/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs
@@ -97,21 +97,21 @@ internal sealed class ConcurrentStartExecutor() :
/// Executor that aggregates the results from the concurrent agents.
///
internal sealed class ConcurrentAggregationExecutor() :
- Executor("ConcurrentAggregationExecutor")
+ Executor>("ConcurrentAggregationExecutor")
{
private readonly List _messages = [];
///
/// Handles incoming messages from the agents and aggregates their responses.
///
- /// The message from the agent
+ /// The messages from the agent
/// Workflow context for accessing workflow services and adding events
/// The to monitor for cancellation requests.
/// The default is .
/// A task representing the asynchronous operation
- public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
- this._messages.Add(message);
+ this._messages.AddRange(message);
if (this._messages.Count == 2)
{
diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs
new file mode 100644
index 0000000000..17d7d03b3f
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/Program.cs
@@ -0,0 +1,140 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Azure.Monitor.OpenTelemetry.Exporter;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+using OpenTelemetry;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace WorkflowAsAnAgentObservabilitySample;
+
+///
+/// This sample shows how to enable OpenTelemetry observability for workflows when
+/// using them as s.
+///
+/// In this example, we create a workflow that uses two language agents to process
+/// input concurrently, one that responds in French and another that responds in English.
+///
+/// You will interact with the workflow in an interactive loop, sending messages and receiving
+/// streaming responses from the workflow as if it were an agent who responds in both languages.
+///
+/// OpenTelemetry observability is enabled at multiple levels:
+/// 1. At the chat client level, capturing telemetry for interactions with the Azure OpenAI service.
+/// 2. At the agent level, capturing telemetry for agent operations.
+/// 3. At the workflow level, capturing telemetry for workflow execution.
+///
+/// Traces will be sent to an Aspire dashboard via an OTLP endpoint, and optionally to
+/// Azure Monitor if an Application Insights connection string is provided.
+///
+/// Learn how to set up an Aspire dashboard here:
+/// https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/standalone?tabs=bash
+///
+///
+/// Pre-requisites:
+/// - Foundational samples should be completed first.
+/// - This sample uses concurrent processing.
+/// - An Azure OpenAI endpoint and deployment name.
+/// - An Application Insights resource for telemetry (optional).
+///
+public static class Program
+{
+ private const string SourceName = "Workflow.ApplicationInsightsSample";
+ private static readonly ActivitySource s_activitySource = new(SourceName);
+
+ private static async Task Main()
+ {
+ // Set up observability
+ var applicationInsightsConnectionString = Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING");
+ var otlpEndpoint = Environment.GetEnvironmentVariable("OTLP_ENDPOINT") ?? "http://localhost:4317";
+
+ var resourceBuilder = ResourceBuilder
+ .CreateDefault()
+ .AddService("WorkflowSample");
+
+ var traceProviderBuilder = Sdk.CreateTracerProviderBuilder()
+ .SetResourceBuilder(resourceBuilder)
+ .AddSource("Microsoft.Agents.AI.*") // Agent Framework telemetry
+ .AddSource("Microsoft.Extensions.AI.*") // Extensions AI telemetry
+ .AddSource(SourceName);
+
+ traceProviderBuilder.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint));
+ if (!string.IsNullOrWhiteSpace(applicationInsightsConnectionString))
+ {
+ traceProviderBuilder.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString);
+ }
+
+ using var traceProvider = traceProviderBuilder.Build();
+
+ // 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";
+ var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+ .GetChatClient(deploymentName)
+ .AsIChatClient()
+ .AsBuilder()
+ .UseOpenTelemetry(sourceName: SourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the chat client level
+ .Build();
+
+ // Start a root activity for the application
+ using var activity = s_activitySource.StartActivity("main");
+ Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}");
+
+ // Create the workflow and turn it into an agent with OpenTelemetry instrumentation
+ var workflow = WorkflowHelper.GetWorkflow(chatClient, SourceName);
+ var agent = new OpenTelemetryAgent(workflow.AsAgent("workflow-agent", "Workflow Agent"), SourceName)
+ {
+ EnableSensitiveData = true // enable sensitive data at the agent level such as prompts and responses
+ };
+ var thread = agent.GetNewThread();
+
+ // Start an interactive loop to interact with the workflow as if it were an agent
+ while (true)
+ {
+ Console.WriteLine();
+ Console.Write("User (or 'exit' to quit): ");
+ string? input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
+ {
+ break;
+ }
+
+ await ProcessInputAsync(agent, thread, input);
+ }
+
+ // Helper method to process user input and display streaming responses. To display
+ // multiple interleaved responses correctly, we buffer updates by message ID and
+ // re-render all messages on each update.
+ static async Task ProcessInputAsync(AIAgent agent, AgentThread thread, string input)
+ {
+ Dictionary> buffer = [];
+ await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(input, thread))
+ {
+ if (update.MessageId is null || string.IsNullOrEmpty(update.Text))
+ {
+ // skip updates that don't have a message ID or text
+ continue;
+ }
+ Console.Clear();
+
+ if (!buffer.TryGetValue(update.MessageId, out List? value))
+ {
+ value = [];
+ buffer[update.MessageId] = value;
+ }
+ value.Add(update);
+
+ foreach (var (messageId, segments) in buffer)
+ {
+ string combinedText = string.Concat(segments);
+ Console.WriteLine($"{segments[0].AuthorName}: {combinedText}");
+ Console.WriteLine();
+ }
+ }
+ }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj
new file mode 100644
index 0000000000..17c44eeb75
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowAsAnAgentObservability.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs
new file mode 100644
index 0000000000..816dce50d0
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Observability/WorkflowAsAnAgent/WorkflowHelper.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
+
+namespace WorkflowAsAnAgentObservabilitySample;
+
+internal static class WorkflowHelper
+{
+ ///
+ /// Creates a workflow that uses two language agents to process input concurrently.
+ ///
+ /// The chat client to use for the agents
+ /// The source name for OpenTelemetry instrumentation
+ /// A workflow that processes input using two language agents
+ internal static Workflow GetWorkflow(IChatClient chatClient, string sourceName)
+ {
+ // Create executors
+ var startExecutor = new ConcurrentStartExecutor();
+ var aggregationExecutor = new ConcurrentAggregationExecutor();
+ AIAgent frenchAgent = GetLanguageAgent("French", chatClient, sourceName);
+ AIAgent englishAgent = GetLanguageAgent("English", chatClient, sourceName);
+
+ // Build the workflow by adding executors and connecting them
+ return new WorkflowBuilder(startExecutor)
+ .AddFanOutEdge(startExecutor, targets: [frenchAgent, englishAgent])
+ .AddFanInEdge(aggregationExecutor, sources: [frenchAgent, englishAgent])
+ .WithOutputFrom(aggregationExecutor)
+ .Build();
+ }
+
+ ///
+ /// Creates a language agent for the specified target language.
+ ///
+ /// The target language for translation
+ /// The chat client to use for the agent
+ /// The source name for OpenTelemetry instrumentation
+ /// An AIAgent configured for the specified language
+ private static AIAgent GetLanguageAgent(string targetLanguage, IChatClient chatClient, string sourceName) =>
+ new ChatClientAgent(
+ chatClient,
+ instructions: $"You're a helpful assistant who always responds in {targetLanguage}.",
+ name: $"{targetLanguage}Agent"
+ )
+ .AsBuilder()
+ .UseOpenTelemetry(sourceName, configure: (cfg) => cfg.EnableSensitiveData = true) // enable telemetry at the agent level
+ .Build();
+
+ ///
+ /// Executor that starts the concurrent processing by sending messages to the agents.
+ ///
+ private sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor")
+ {
+ protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder)
+ {
+ return routeBuilder
+ .AddHandler>(this.RouteMessages)
+ .AddHandler(this.RouteTurnTokenAsync);
+ }
+
+ private ValueTask RouteMessages(List messages, IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ return context.SendMessageAsync(messages, cancellationToken: cancellationToken);
+ }
+
+ private ValueTask RouteTurnTokenAsync(TurnToken token, IWorkflowContext context, CancellationToken cancellationToken)
+ {
+ return context.SendMessageAsync(token, cancellationToken: cancellationToken);
+ }
+ }
+
+ ///
+ /// Executor that aggregates the results from the concurrent agents.
+ ///
+ private sealed class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor")
+ {
+ private readonly List _messages = [];
+
+ ///
+ /// Handles incoming messages from the agents and aggregates their responses.
+ ///
+ /// The message from the agent
+ /// Workflow context for accessing workflow services and adding events
+ /// The to monitor for cancellation requests.
+ /// The default is .
+ public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default)
+ {
+ this._messages.AddRange(message);
+
+ if (this._messages.Count == 2)
+ {
+ var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.Text}"));
+ await context.YieldOutputAsync(formattedMessages, cancellationToken);
+ }
+ }
+ }
+}
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.Mem0/Mem0Provider.cs b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
index c68b6313d5..d0f6f78b3f 100644
--- a/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Mem0/Mem0Provider.cs
@@ -131,31 +131,62 @@ public sealed class Mem0Provider : AIContextProvider
Environment.NewLine,
context.RequestMessages.Where(m => !string.IsNullOrWhiteSpace(m.Text)).Select(m => m.Text));
- var memories = (await this._client.SearchAsync(
- this.ApplicationId,
- this.AgentId,
- this.ThreadId,
- this.UserId,
- queryText,
- cancellationToken).ConfigureAwait(false)).ToList();
-
- var contextInstructions = memories.Count == 0
- ? null
- : $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
-
- if (this._logger is not null)
+ try
{
- this._logger.LogInformation("Mem0AIContextProvider retrieved {Count} memories.", memories.Count);
- if (contextInstructions is not null)
+ var memories = (await this._client.SearchAsync(
+ this.ApplicationId,
+ this.AgentId,
+ this.ThreadId,
+ this.UserId,
+ queryText,
+ cancellationToken).ConfigureAwait(false)).ToList();
+
+ var outputMessageText = memories.Count == 0
+ ? null
+ : $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
+
+ if (this._logger is not null)
{
- this._logger.LogTrace("Mem0AIContextProvider instructions: {Instructions}", contextInstructions);
+ this._logger.LogInformation(
+ "Mem0AIContextProvider: Retrieved {Count} memories. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ memories.Count,
+ this.ApplicationId,
+ this.AgentId,
+ this.ThreadId,
+ this.UserId);
+ if (outputMessageText is not null)
+ {
+ this._logger.LogTrace(
+ "Mem0AIContextProvider: Search Results\nInput:{Input}\nOutput:{MessageText}\nApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ queryText,
+ outputMessageText,
+ this.ApplicationId,
+ this.AgentId,
+ this.ThreadId,
+ this.UserId);
+ }
}
- }
- return new AIContext
+ return new AIContext
+ {
+ Messages = [new ChatMessage(ChatRole.User, outputMessageText)]
+ };
+ }
+ catch (ArgumentException)
{
- Messages = [new ChatMessage(ChatRole.User, contextInstructions)]
- };
+ throw;
+ }
+ catch (Exception ex)
+ {
+ this._logger?.LogError(
+ ex,
+ "Mem0AIContextProvider: Failed to search Mem0 for memories due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ this.ApplicationId,
+ this.AgentId,
+ this.ThreadId,
+ this.UserId);
+ return new AIContext();
+ }
}
///
@@ -166,12 +197,20 @@ public sealed class Mem0Provider : AIContextProvider
return; // Do not update memory on failed invocations.
}
- // Persist request and response messages after invocation.
- await this.PersistMessagesAsync(context.RequestMessages, cancellationToken).ConfigureAwait(false);
-
- if (context.ResponseMessages is not null)
+ try
{
- await this.PersistMessagesAsync(context.ResponseMessages, cancellationToken).ConfigureAwait(false);
+ // Persist request and response messages after invocation.
+ await this.PersistMessagesAsync(context.RequestMessages.Concat(context.ResponseMessages ?? []), cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this._logger?.LogError(
+ ex,
+ "Mem0AIContextProvider: Failed to send messages to Mem0 due to error. ApplicationId: '{ApplicationId}', AgentId: '{AgentId}', ThreadId: '{ThreadId}', UserId: '{UserId}'",
+ this.ApplicationId,
+ this.AgentId,
+ this.ThreadId,
+ this.UserId);
}
}
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/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/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 Predicate, HashSet OutgoingIndicies)> _caseMap = [];
private readonly HashSet _defaultIndicies = [];
@@ -30,14 +30,14 @@ public sealed class SwitchBuilder
/// One or more executors to associate with the predicate. Each executor will be invoked if the predicate matches.
/// Cannot be null.
/// The current instance, allowing for method chaining.
- public SwitchBuilder AddCase(Func predicate, params IEnumerable executors)
+ public SwitchBuilder AddCase(Func predicate, params IEnumerable executors)
{
Throw.IfNull(predicate);
Throw.IfNull(executors);
HashSet indicies = [];
- foreach (ExecutorIsh executor in executors)
+ foreach (ExecutorBinding executor in executors)
{
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
{
@@ -60,11 +60,11 @@ public sealed class SwitchBuilder
///
///
///
- public SwitchBuilder WithDefault(params IEnumerable executors)
+ public SwitchBuilder WithDefault(params IEnumerable executors)
{
Throw.IfNull(executors);
- foreach (ExecutorIsh executor in executors)
+ foreach (ExecutorBinding executor in executors)
{
if (!this._executorIndicies.TryGetValue(executor.Id, out int index))
{
@@ -79,7 +79,7 @@ public sealed class SwitchBuilder
return this;
}
- internal WorkflowBuilder ReduceToFanOut(WorkflowBuilder builder, ExecutorIsh source)
+ internal WorkflowBuilder ReduceToFanOut(WorkflowBuilder builder, ExecutorBinding source)
{
List<(Func Predicate, HashSet OutgoingIndicies)> caseMap = this._caseMap;
HashSet defaultIndicies = this._defaultIndicies;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs
index 886002320f..ebf6f08ffb 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Visualization/WorkflowVisualizer.cs
@@ -69,7 +69,7 @@ public static class WorkflowVisualizer
lines.Add($"{indent}\"{MapId(startExecutorId)}\" [fillcolor=lightgreen, label=\"{startExecutorId}\\n(Start)\"];");
// Add other executor nodes
- foreach (var executorId in workflow.Registrations.Keys)
+ foreach (var executorId in workflow.ExecutorBindings.Keys)
{
if (executorId != startExecutorId)
{
@@ -108,7 +108,7 @@ public static class WorkflowVisualizer
private static void EmitSubWorkflowsDigraph(Workflow workflow, List lines, string indent)
{
- foreach (var kvp in workflow.Registrations)
+ foreach (var kvp in workflow.ExecutorBindings)
{
var execId = kvp.Key;
var registration = kvp.Value;
@@ -145,7 +145,7 @@ public static class WorkflowVisualizer
lines.Add($"{indent}{MapId(startExecutorId)}[\"{startExecutorId} (Start)\"];");
// Add other executor nodes
- foreach (var executorId in workflow.Registrations.Keys)
+ foreach (var executorId in workflow.ExecutorBindings.Keys)
{
if (executorId != startExecutorId)
{
@@ -264,9 +264,9 @@ public static class WorkflowVisualizer
#endif
}
- private static bool TryGetNestedWorkflow(ExecutorRegistration registration, [NotNullWhen(true)] out Workflow? workflow)
+ private static bool TryGetNestedWorkflow(ExecutorBinding binding, [NotNullWhen(true)] out Workflow? workflow)
{
- if (registration.RawExecutorishData is Workflow subWorkflow)
+ if (binding.RawValue is Workflow subWorkflow)
{
workflow = subWorkflow;
return true;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
index 3636500438..a4f6be1210 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Workflow.cs
@@ -19,7 +19,7 @@ public class Workflow
///
/// A dictionary of executor providers, keyed by executor ID.
///
- internal Dictionary Registrations { get; init; } = [];
+ internal Dictionary ExecutorBindings { get; init; } = [];
internal Dictionary> Edges { get; init; } = [];
internal HashSet OutputExecutors { get; init; } = [];
@@ -41,7 +41,7 @@ public class Workflow
/// Gets the collection of external request ports, keyed by their ID.
///
///
- /// Each port has a corresponding entry in the dictionary.
+ /// Each port has a corresponding entry in the dictionary.
///
public Dictionary ReflectPorts()
{
@@ -66,10 +66,10 @@ public class Workflow
///
public string? Description { get; internal init; }
- internal bool AllowConcurrent => this.Registrations.Values.All(registration => registration.SupportsConcurrent);
+ internal bool AllowConcurrent => this.ExecutorBindings.Values.All(registration => registration.SupportsConcurrentSharedExecution);
internal IEnumerable NonConcurrentExecutorIds =>
- this.Registrations.Values.Where(r => !r.SupportsConcurrent).Select(r => r.Id);
+ this.ExecutorBindings.Values.Where(r => !r.SupportsConcurrentSharedExecution).Select(r => r.Id);
///
/// Initializes a new instance of the class with the specified starting executor identifier
@@ -86,12 +86,14 @@ public class Workflow
}
private bool _needsReset;
- private bool HasResettable => this.Registrations.Values.Any(registration => registration.SupportsResetting);
+ private bool HasResettableExecutors =>
+ this.ExecutorBindings.Values.Any(registration => registration.SupportsResetting);
+
private async ValueTask TryResetExecutorRegistrationsAsync()
{
- if (this.HasResettable)
+ if (this.HasResettableExecutors)
{
- foreach (ExecutorRegistration registration in this.Registrations.Values)
+ foreach (ExecutorBinding registration in this.ExecutorBindings.Values)
{
// TryResetAsync returns true if the executor does not need resetting
if (!await registration.TryResetAsync().ConfigureAwait(false))
@@ -158,7 +160,7 @@ public class Workflow
});
}
- this._needsReset = this.HasResettable;
+ this._needsReset = this.HasResettableExecutors;
this._ownedAsSubworkflow = subworkflow;
}
@@ -188,7 +190,7 @@ public class Workflow
/// a the protocol this follows.
public async ValueTask DescribeProtocolAsync(CancellationToken cancellationToken = default)
{
- ExecutorRegistration startExecutorRegistration = this.Registrations[this.StartExecutorId];
+ ExecutorBinding startExecutorRegistration = this.ExecutorBindings[this.StartExecutorId];
Executor startExecutor = await startExecutorRegistration.CreateInstanceAsync(string.Empty)
.ConfigureAwait(false);
return startExecutor.DescribeProtocol();
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs
index 9797843fa7..c277a84b36 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs
@@ -19,7 +19,7 @@ namespace Microsoft.Agents.AI.Workflows;
/// Use the WorkflowBuilder to incrementally add executors and edges, including fan-in and fan-out
/// patterns, before building a strongly-typed workflow instance. Executors must be bound before building the workflow.
/// All executors must be bound by calling into if they were intially specified as
-/// .
+/// .
public class WorkflowBuilder
{
private readonly record struct EdgeConnection(string SourceId, string TargetId)
@@ -28,11 +28,11 @@ public class WorkflowBuilder
}
private int _edgeCount;
- private readonly Dictionary _executors = [];
+ private readonly Dictionary _executors = [];
private readonly Dictionary> _edges = [];
private readonly HashSet _unboundExecutors = [];
private readonly HashSet