From ae1f94a04e1b81cfa1dc1509d6289e6ae4051c58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:19:19 +0000 Subject: [PATCH 1/2] Initial plan From f10044be7c35f2d93127bf034d4a250fc8b2b4c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:25:36 +0000 Subject: [PATCH 2/2] Update dotnet-microsoft-agent-framework: reflect AgentWorkflowBuilder.BuildConcurrent in simple-concurrent-workflow tutorial snapshot Co-authored-by: KSemenenko <4385716+KSemenenko@users.noreply.github.com> --- .../dotnet-microsoft-agent-framework/SKILL.md | 5 +- .../workflows/simple-concurrent-workflow.md | 219 +++++++++--------- .../references/workflows.md | 12 + 3 files changed, 120 insertions(+), 116 deletions(-) diff --git a/skills/dotnet-microsoft-agent-framework/SKILL.md b/skills/dotnet-microsoft-agent-framework/SKILL.md index 36dc8f9..67fdddd 100644 --- a/skills/dotnet-microsoft-agent-framework/SKILL.md +++ b/skills/dotnet-microsoft-agent-framework/SKILL.md @@ -1,6 +1,6 @@ --- name: dotnet-microsoft-agent-framework -version: "1.4.0" +version: "1.5.0" category: "AI" description: "Build .NET AI agents and multi-agent workflows with Microsoft Agent Framework using the right agent type, threads, tools, workflows, hosting protocols, and enterprise guardrails." compatibility: "Requires preview-era Microsoft Agent Framework packages and a .NET application that truly needs agentic or workflow orchestration." @@ -54,6 +54,7 @@ flowchart LR - `AgentResponse` and `AgentResponseUpdate` are not just text containers. They can include tool calls, tool results, structured output, reasoning-like updates, and response metadata. - `ChatClientAgent` is the safest default when you already have an `IChatClient` and do not need a hosted-agent service. - `Workflow` is an explicit graph of executors and edges. Use it when the control flow must stay inspectable, typed, resumable, or human-steerable. +- `AgentWorkflowBuilder` provides high-level factory methods such as `BuildConcurrent` for common agent orchestration patterns. Use it when you need concurrent or sequential agent pipelines without writing custom executor classes. - Hosting layers such as OpenAI-compatible HTTP, A2A, and AG-UI are adapters over your in-process agent or workflow. They do not replace the core architecture choice. - Durable agents are a hosting and persistence decision for Azure Functions. They are not the default answer for ordinary app-level orchestration. @@ -65,7 +66,7 @@ flowchart LR | OpenAI-style future-facing APIs, background responses, or richer response state | Responses-based agent | Better fit for new OpenAI-compatible integrations | | Simple client-managed chat history | Chat Completions agent | Keeps request/response simple | | Service-hosted agents and service-owned threads/tools | Azure AI Foundry Agent or other hosted agent | Managed runtime is the requirement | -| Typed multi-step orchestration | `Workflow` | Control flow stays explicit and testable | +| Typed multi-step orchestration | `Workflow` or `AgentWorkflowBuilder` helpers | Control flow stays explicit and testable; use `BuildConcurrent` for agent fan-out/fan-in | | Week-long or failure-resilient Azure execution | Durable agent on Azure Functions | Durable Task gives replay and persisted state | | Agent-to-agent interoperability | A2A hosting or A2A proxy agent | This is protocol-level delegation, not local inference | | Browser or web UI protocol integration | AG-UI | Designed for remote UI sync and approval flows | diff --git a/skills/dotnet-microsoft-agent-framework/references/official-docs/tutorials/workflows/simple-concurrent-workflow.md b/skills/dotnet-microsoft-agent-framework/references/official-docs/tutorials/workflows/simple-concurrent-workflow.md index 01d0fee..45ca3fb 100644 --- a/skills/dotnet-microsoft-agent-framework/references/official-docs/tutorials/workflows/simple-concurrent-workflow.md +++ b/skills/dotnet-microsoft-agent-framework/references/official-docs/tutorials/workflows/simple-concurrent-workflow.md @@ -5,13 +5,13 @@ zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial ms.author: taochen -ms.date: 09/29/2025 +ms.date: 03/17/2026 ms.service: agent-framework --- # Create a Simple Concurrent Workflow -This tutorial demonstrates how to create a concurrent workflow using Agent Framework. You'll learn to implement fan-out and fan-in patterns that enable parallel processing, allowing multiple executors or agents to work simultaneously and then aggregate their results. +This tutorial demonstrates how to create a concurrent workflow using Agent Framework. You'll learn to implement fan-out and fan-in patterns that enable parallel processing, allowing multiple agents to work simultaneously on the same input and then aggregate their results. ::: zone pivot="programming-language-csharp" @@ -19,10 +19,10 @@ This tutorial demonstrates how to create a concurrent workflow using Agent Frame You'll create a workflow that: -- Takes a question as input (for example, "What is temperature?") -- Sends the same question to two expert AI agents simultaneously (Physicist and Chemist) -- Collects and combines responses from both agents into a single output -- Demonstrates concurrent execution with AI agents using fan-out/fan-in patterns +- Takes a user message as input (for example, "Hello, world!") +- Sends the same message to multiple translation agents simultaneously +- Collects and aggregates responses from all agents into a single output +- Demonstrates concurrent execution with `AgentWorkflowBuilder.BuildConcurrent` ### Concepts Covered @@ -50,7 +50,7 @@ dotnet add package Microsoft.Agents.AI.Workflows --prerelease dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease ``` -## Step 2: Setup Dependencies and Azure OpenAI +## Step 2: Set Up Dependencies and Azure OpenAI Start by setting up your project with the required NuGet packages and Azure OpenAI client: @@ -70,158 +70,149 @@ public static class Program private static async Task Main() { // Set up the Azure OpenAI client - var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("AZURE_OPENAI_ENDPOINT is not set."); + 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(); ``` -## Step 3: Create Expert AI Agents +## Step 3: Create Specialized AI Agents -Create two specialized AI agents that will provide expert perspectives: +Create multiple specialized agents that will each process the same input concurrently: ```csharp - // Create the AI agents with specialized expertise - ChatClientAgent physicist = new( - chatClient, - name: "Physicist", - instructions: "You are an expert in physics. You answer questions from a physics perspective." - ); - - ChatClientAgent chemist = new( - chatClient, - name: "Chemist", - instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective." - ); + // Helper method to create a translation agent for a target language + static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient client) => + new(client, + $"You are a translation assistant who only responds in {targetLanguage}. " + + $"Respond to any input by outputting the name of the input language and then " + + $"translating the input to {targetLanguage}."); + + // Create translation agents for concurrent processing + var translationAgents = new[] { "French", "Spanish", "English" } + .Select(lang => GetTranslationAgent(lang, chatClient)); ``` -## Step 4: Create the Start Executor +## Step 4: Build the Concurrent Workflow -Create an executor that initiates the concurrent processing by sending input to multiple agents: +Use `AgentWorkflowBuilder.BuildConcurrent` to create the concurrent workflow from the agent collection. The builder automatically handles the fan-out and fan-in logic: ```csharp - var startExecutor = new ConcurrentStartExecutor(); + // Build the concurrent workflow - fan-out and fan-in are handled automatically + var workflow = AgentWorkflowBuilder.BuildConcurrent(translationAgents); ``` -The `ConcurrentStartExecutor` implementation: +## Step 5: Execute the Workflow + +Run the workflow, send the turn token to kick off the agents, and capture the streaming output: ```csharp -/// -/// Executor that starts the concurrent processing by sending messages to the agents. -/// -internal 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 . - /// A task representing the asynchronous operation - public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) - { - // 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(new ChatMessage(ChatRole.User, message), cancellationToken); + // Execute the workflow in streaming mode + var messages = new List { new(ChatRole.User, "Hello, world!") }; + await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages); - // Broadcast the turn token to kick off the agents. - await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken); + // Send a turn token to start agent processing + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + List result = new(); + await foreach (WorkflowEvent evt in run.WatchStreamAsync()) + { + if (evt is AgentResponseUpdateEvent e) + { + Console.WriteLine($"{e.ExecutorId}: {e.Data}"); + } + else if (evt is WorkflowOutputEvent outputEvt) + { + result = (List)outputEvt.Data!; + break; + } + } + + Console.WriteLine("===== Final Aggregated Results ====="); + foreach (var msg in result) + { + Console.WriteLine($"{msg.Role}: {msg.Content}"); + } } } ``` -## Step 5: Create the Aggregation Executor +## How It Works -Create an executor that collects and combines responses from multiple agents: +1. **Fan-Out**: `AgentWorkflowBuilder.BuildConcurrent` distributes the same input to all agents simultaneously. +2. **Parallel Processing**: All agents process the same message concurrently, each providing their unique perspective. +3. **Turn Token**: `TurnToken` signals agents to begin processing the queued message. +4. **Fan-In / Aggregation**: Results from all agents are automatically collected into a `List` output. -```csharp - var aggregationExecutor = new ConcurrentAggregationExecutor(); -``` +## Key Concepts + +- **`AgentWorkflowBuilder.BuildConcurrent(agents)`**: High-level builder method that creates a concurrent workflow from an `IEnumerable`. Handles fan-out and fan-in automatically without requiring custom executor classes. +- **Custom Aggregator**: An optional `Func>, List>` overload lets you provide custom aggregation logic. +- **Turn Tokens**: Use `TurnToken` to signal agents to begin processing queued messages. +- **`AgentResponseUpdateEvent`**: Streaming event for real-time per-agent progress. +- **`WorkflowOutputEvent`**: Terminal event carrying the aggregated `List` from all agents. -The `ConcurrentAggregationExecutor` implementation: +## Advanced: Manual Fan-Out / Fan-In with Custom Executors + +For scenarios requiring fine-grained control over the dispatcher or aggregation logic, you can build the concurrent graph directly with `WorkflowBuilder`: ```csharp -/// -/// Executor that aggregates the results from the concurrent agents. -/// -internal sealed class ConcurrentAggregationExecutor() : - Executor>("ConcurrentAggregationExecutor") +// Custom executor that dispatches the user message and turn token to all connected agents +internal sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor") +{ + public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) + { + await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken); + await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken); + } +} + +// Custom executor that aggregates individual ChatMessage responses from each agent +internal sealed class ConcurrentAggregationExecutor(int agentCount) : + 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 . - /// A task representing the asynchronous operation - public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default) + public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default) { - this._messages.AddRange(message); - - if (this._messages.Count == 2) + this._messages.Add(message); + if (this._messages.Count == agentCount) { - var formattedMessages = string.Join(Environment.NewLine, + var formatted = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.AuthorName}: {m.Text}")); - await context.YieldOutputAsync(formattedMessages, cancellationToken); + await context.YieldOutputAsync(formatted, cancellationToken); } } } ``` -## Step 6: Build the Workflow - -Connect the executors and agents using fan-out and fan-in edge patterns: - -```csharp - // Build the workflow by adding executors and connecting them - var workflow = new WorkflowBuilder(startExecutor) - .AddFanOutEdge(startExecutor, targets: [physicist, chemist]) - .AddFanInEdge(aggregationExecutor, sources: [physicist, chemist]) - .WithOutputFrom(aggregationExecutor) - .Build(); -``` - -## Step 7: Execute the Workflow - -Run the workflow and capture the streaming output: +Build the graph manually: ```csharp - // Execute the workflow in streaming mode - await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, "What is temperature?"); - await foreach (WorkflowEvent evt in run.WatchStreamAsync()) - { - if (evt is WorkflowOutputEvent output) - { - Console.WriteLine($"Workflow completed with results:\n{output.Data}"); - } - } - } -} +var startExecutor = new ConcurrentStartExecutor(); +var aggregationExecutor = new ConcurrentAggregationExecutor(agentCount: 2); + +var physicistAgent = new ChatClientAgent(chatClient, name: "Physicist", + instructions: "You are an expert in physics."); +var chemistAgent = new ChatClientAgent(chatClient, name: "Chemist", + instructions: "You are an expert in chemistry."); + +var workflow = new WorkflowBuilder(startExecutor) + .AddFanOutEdge(startExecutor, targets: [physicistAgent, chemistAgent]) + .AddFanInEdge(aggregationExecutor, sources: [physicistAgent, chemistAgent]) + .WithOutputFrom(aggregationExecutor) + .Build(); ``` -## How It Works - -1. **Fan-Out**: The `ConcurrentStartExecutor` receives the input question and the fan-out edge sends it to both the Physicist and Chemist agents simultaneously. -2. **Parallel Processing**: Both AI agents process the same question concurrently, each providing their expert perspective. -3. **Fan-In**: The `ConcurrentAggregationExecutor` collects `ChatMessage` responses from both agents. -4. **Aggregation**: Once both responses are received, the aggregator combines them into a formatted output. - -## Key Concepts - -- **Fan-Out Edges**: Use `AddFanOutEdge()` to distribute the same input to multiple executors or agents. -- **Fan-In Edges**: Use `AddFanInEdge()` to collect results from multiple source executors. -- **AI Agent Integration**: AI agents can be used directly as executors in workflows. -- **Executor Base Class**: Custom executors inherit from `Executor` and override the `HandleAsync` method. -- **Turn Tokens**: Use `TurnToken` to signal agents to begin processing queued messages. -- **Streaming Execution**: Use `StreamAsync()` to get real-time updates as the workflow progresses. +Use this approach when: +- You need a custom dispatcher that does more than broadcast a message and turn token. +- Your aggregation logic requires domain-specific processing before yielding output. +- The agent count or structure is not known at build time and cannot be expressed with `BuildConcurrent`. ## Complete Implementation -For the complete working implementation of this concurrent workflow with AI agents, see the [Concurrent/Program.cs](https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/GettingStarted/Workflows/Concurrent/Concurrent/Program.cs) sample in the Agent Framework repository. +For the complete working implementation of this concurrent workflow with AI agents, see the [concurrent orchestration sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/GettingStarted/Workflows/Concurrent) in the Agent Framework repository. ::: zone-end diff --git a/skills/dotnet-microsoft-agent-framework/references/workflows.md b/skills/dotnet-microsoft-agent-framework/references/workflows.md index 24e9d1e..99f4629 100644 --- a/skills/dotnet-microsoft-agent-framework/references/workflows.md +++ b/skills/dotnet-microsoft-agent-framework/references/workflows.md @@ -42,6 +42,18 @@ Use `AgentWorkflowBuilder` when: - the orchestration matches built-in agent patterns - you want sequential or concurrent pipeline helpers +### `AgentWorkflowBuilder.BuildConcurrent` + +```csharp +var workflow = AgentWorkflowBuilder.BuildConcurrent(agents); +``` + +- Accepts `IEnumerable` and an optional custom aggregator `Func>, List>`. +- Handles fan-out and fan-in automatically without requiring custom executor classes. +- Default aggregator returns the last message from each responding agent. +- After `InProcessExecution.StreamAsync`, send `TurnToken(emitEvents: true)` via `run.TrySendMessageAsync` to kick off agents. +- Use manual `WorkflowBuilder` with `AddFanOutEdge`/`AddFanInEdge` only when you need a custom dispatcher or aggregation logic beyond what the built-in overload supports. + ## Workflow Patterns | Pattern | Best For | Main Risk |