From 0b7af758f9beae3b0e2f84ff2efa87e849ce7f78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:25:42 +0000 Subject: [PATCH 1/9] Initial plan From e81c62dfba51a488039da926b50a3bf5ba9ec0d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:39:28 +0000 Subject: [PATCH 2/9] Add C# GroupChat tool approval sample implementation Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- .../DeploymentGroupChatManager.cs | 47 ++++ .../GroupChatToolApproval.csproj | 22 ++ .../Agents/GroupChatToolApproval/Program.cs | 209 ++++++++++++++++++ .../Agents/GroupChatToolApproval/README.md | 70 ++++++ .../GettingStarted/Workflows/README.md | 1 + 5 files changed, 349 insertions(+) create mode 100644 dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs create mode 100644 dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj create mode 100644 dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs create mode 100644 dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs new file mode 100644 index 0000000000..19938ae64e --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +namespace WorkflowGroupChatToolApprovalSample; + +/// +/// Custom GroupChatManager that selects the next speaker based on the conversation flow. +/// +/// +/// This simple selector follows a predefined flow: +/// 1. QA Engineer runs tests +/// 2. DevOps Engineer checks staging and creates rollback plan +/// 3. DevOps Engineer deploys to production (triggers approval) +/// +internal sealed class DeploymentGroupChatManager : GroupChatManager +{ + private readonly IReadOnlyList _agents; + + public DeploymentGroupChatManager(IReadOnlyList agents) + { + this._agents = agents; + } + + protected override ValueTask SelectNextAgentAsync( + IReadOnlyList history, + CancellationToken cancellationToken = default) + { + if (history.Count == 0) + { + throw new InvalidOperationException("Conversation is empty; cannot select next speaker."); + } + + // First speaker after initial user message + if (this.IterationCount == 0) + { + AIAgent qaAgent = this._agents.First(a => a.Id == "QAEngineer"); + return new ValueTask(qaAgent); + } + + // Subsequent speakers are DevOps Engineer + AIAgent devopsAgent = this._agents.First(a => a.Id == "DevOpsEngineer"); + return new ValueTask(devopsAgent); + } +} diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj new file mode 100644 index 0000000000..d0c0656ade --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/GroupChatToolApproval.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs new file mode 100644 index 0000000000..479a2ec308 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use GroupChatBuilder with tools that require human +// approval before execution. A group of specialized agents collaborate on a task, and +// sensitive tool calls trigger human-in-the-loop approval. +// +// This sample works as follows: +// 1. A GroupChatBuilder workflow is created with multiple specialized agents. +// 2. A custom manager determines which agent speaks next based on conversation state. +// 3. Agents collaborate on a software deployment task. +// 4. When the deployment agent tries to deploy to production, it triggers an approval request. +// 5. The sample simulates human approval and the workflow completes. +// +// Purpose: +// Show how tool call approvals integrate with multi-agent group chat workflows where +// different agents have different levels of tool access. +// +// Demonstrate: +// - Using custom GroupChatManager with agents that have approval-required tools. +// - Handling FunctionApprovalRequestContent in group chat scenarios. +// - Multi-round group chat with tool approval interruption and resumption. + +using System.ComponentModel; +using System.Text.Json; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +namespace WorkflowGroupChatToolApprovalSample; + +/// +/// This sample demonstrates how to use GroupChatBuilder with tools that require human +/// approval before execution. +/// +/// +/// Pre-requisites: +/// - An Azure OpenAI chat completion deployment must be configured. +/// +public static class Program +{ + private static async Task Main() + { + 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"; + + // 2. Create specialized agents + IChatClient client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + + ChatClientAgent qaEngineer = new( + client, + "You are a QA engineer responsible for running tests before deployment. Run the appropriate test suites and report results clearly.", + "QAEngineer", + "QA engineer who runs tests", + [AIFunctionFactory.Create(RunTests)]); + + ChatClientAgent devopsEngineer = new( + client, + "You are a DevOps engineer responsible for deployments. First check staging status and create a rollback plan, then proceed with production deployment. Always ensure safety measures are in place before deploying.", + "DevOpsEngineer", + "DevOps engineer who handles deployments", + [ + AIFunctionFactory.Create(CheckStagingStatus), + AIFunctionFactory.Create(CreateRollbackPlan), + new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeployToProduction)) + ]); + + // 3. Create custom GroupChatManager with speaker selection logic + DeploymentGroupChatManager manager = new([qaEngineer, devopsEngineer]) + { + MaximumIterationCount = 4 // Limit to 4 rounds + }; + + // 4. Build a group chat workflow with the custom manager + Workflow workflow = AgentWorkflowBuilder + .CreateGroupChatBuilderWith(_ => manager) + .AddParticipants(qaEngineer, devopsEngineer) + .Build(); + + // 5. Start the workflow + Console.WriteLine("Starting group chat workflow for software deployment..."); + Console.WriteLine($"Agents: [{qaEngineer.Id}, {devopsEngineer.Id}]"); + Console.WriteLine(new string('-', 60)); + + List messages = [new(ChatRole.User, "We need to deploy version 2.4.0 to production. Please coordinate the deployment.")]; + + // Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS) + List approvalRequests = []; + await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages); + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + string? lastExecutorId = null; + await foreach (WorkflowEvent evt in run.WatchStreamAsync()) + { + switch (evt) + { + case AgentResponseUpdateEvent e: + if (e.ExecutorId != lastExecutorId) + { + if (lastExecutorId is not null) + { + Console.WriteLine(); + } + Console.Write($"- {e.ExecutorId}: "); + lastExecutorId = e.ExecutorId; + } + + Console.Write(e.Update.Text); + + // Check for user input requests (approval requests) + FunctionApprovalRequestContent? approvalRequest = e.Update.Contents + .OfType() + .FirstOrDefault(); + + if (approvalRequest is not null) + { + approvalRequests.Add(approvalRequest); + Console.WriteLine(); + Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.ExecutorId}"); + Console.WriteLine($" Tool: {approvalRequest.FunctionCall.Name}"); + Console.WriteLine($" Arguments: {JsonSerializer.Serialize(approvalRequest.FunctionCall.Arguments)}"); + } + + break; + + case WorkflowOutputEvent: + // Workflow has completed, check if we have pending approval requests + if (approvalRequests.Count == 0) + { + Console.WriteLine(); + Console.WriteLine("Workflow completed without requiring production deployment approval."); + } + else + { + // We have approval requests to handle + Console.WriteLine(); + Console.WriteLine(new string('=', 60)); + Console.WriteLine("Human review required for production deployment!"); + Console.WriteLine("In a real scenario, you would review the deployment details here."); + Console.WriteLine("Simulating approval for demo purposes..."); + Console.WriteLine(new string('=', 60)); + + // Phase 2: Send approval and continue workflow + List approvalMessages = []; + foreach (FunctionApprovalRequestContent req in approvalRequests) + { + approvalMessages.Add(new ChatMessage(ChatRole.User, [req.CreateResponse(approved: true)])); + } + + messages.AddRange(approvalMessages); + + // Resume workflow with approval responses + await using StreamingRun resumeRun = await InProcessExecution.StreamAsync(workflow, messages); + await resumeRun.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + lastExecutorId = null; + await foreach (WorkflowEvent resumeEvt in resumeRun.WatchStreamAsync()) + { + if (resumeEvt is AgentResponseUpdateEvent e2) + { + if (e2.ExecutorId != lastExecutorId) + { + if (lastExecutorId is not null) + { + Console.WriteLine(); + } + Console.Write($"- {e2.ExecutorId}: "); + lastExecutorId = e2.ExecutorId; + } + + Console.Write(e2.Update.Text); + } + else if (resumeEvt is WorkflowOutputEvent) + { + Console.WriteLine(); + Console.WriteLine(new string('-', 60)); + Console.WriteLine("Deployment workflow completed successfully!"); + Console.WriteLine("All agents have finished their tasks."); + break; + } + } + } + return; + } + } + } + + // 1. Define tools for different agents + [Description("Run automated tests for the application.")] + private static string RunTests([Description("Name of the test suite to run")] string testSuite) + => $"Test suite '{testSuite}' completed: 47 passed, 0 failed, 0 skipped"; + + [Description("Check the current status of the staging environment.")] + private static string CheckStagingStatus() + => "Staging environment: Healthy, Version 2.3.0 deployed, All services running"; + + [Description("Deploy specified components to production. Requires human approval.")] + private static string DeployToProduction( + [Description("The version to deploy")] string version, + [Description("Comma-separated list of components to deploy")] string components) + => $"Production deployment complete: Version {version}, Components: {components}"; + + [Description("Create a rollback plan for the deployment.")] + private static string CreateRollbackPlan([Description("The version being deployed")] string version) + => $"Rollback plan created for version {version}: Automated rollback to v2.2.0 if health checks fail within 5 minutes"; +} diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md new file mode 100644 index 0000000000..9c3d401b90 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md @@ -0,0 +1,70 @@ +# Group Chat with Tool Approval Sample + +This sample demonstrates how to use `GroupChatBuilder` with tools that require human approval before execution. A group of specialized agents collaborate on a task, and sensitive tool calls trigger human-in-the-loop approval. + +## What This Sample Demonstrates + +- Using a custom `GroupChatManager` with agents that have approval-required tools +- Handling `FunctionApprovalRequestContent` in group chat scenarios +- Multi-round group chat with tool approval interruption and resumption +- Integrating tool call approvals with multi-agent workflows where different agents have different levels of tool access + +## How It Works + +1. A `GroupChatBuilder` workflow is created with multiple specialized agents +2. A custom `DeploymentGroupChatManager` determines which agent speaks next based on conversation state +3. Agents collaborate on a software deployment task: + - **QA Engineer**: Runs automated tests + - **DevOps Engineer**: Checks staging status, creates rollback plan, and deploys to production +4. When the deployment agent tries to deploy to production, it triggers an approval request +5. The sample simulates human approval and the workflow completes + +## Key Components + +### Approval-Required Tools + +The `DeployToProduction` function is wrapped with `ApprovalRequiredAIFunction` to require human approval: + +```csharp +new ApprovalRequiredAIFunction(AIFunctionFactory.Create(DeployToProduction)) +``` + +### Custom Group Chat Manager + +The `DeploymentGroupChatManager` implements custom speaker selection logic: +- First iteration: QA Engineer runs tests +- Subsequent iterations: DevOps Engineer handles deployment tasks + +### Approval Handling + +The sample demonstrates two-phase execution: +1. **Phase 1**: Run workflow until approval is needed +2. **Phase 2**: Handle approval requests and resume workflow + +## Prerequisites + +- Azure OpenAI or OpenAI configured with the required environment variables +- `AZURE_OPENAI_ENDPOINT` environment variable set +- `AZURE_OPENAI_DEPLOYMENT_NAME` environment variable (defaults to "gpt-4o-mini") + +## Running the Sample + +```bash +dotnet run +``` + +## Expected Output + +The sample will show: +1. QA Engineer running tests +2. DevOps Engineer checking staging and creating rollback plan +3. An approval request for production deployment +4. Simulated approval response +5. DevOps Engineer completing the deployment +6. Workflow completion message + +## Related Samples + +- [Agent Function Tools with Approvals](../../../Agents/Agent_Step04_UsingFunctionToolsWithApprovals) - Basic function approval pattern +- [Agent Workflow Patterns](../../_Foundational/04_AgentWorkflowPatterns) - Group chat without approvals +- [Human-in-the-Loop Basic](../../HumanInTheLoop/HumanInTheLoopBasic) - Workflow-level human interaction diff --git a/dotnet/samples/GettingStarted/Workflows/README.md b/dotnet/samples/GettingStarted/Workflows/README.md index 072acfa560..f7704d0e42 100644 --- a/dotnet/samples/GettingStarted/Workflows/README.md +++ b/dotnet/samples/GettingStarted/Workflows/README.md @@ -32,6 +32,7 @@ Once completed, please proceed to other samples listed below. | [Foundry Agents in Workflows](./Agents/FoundryAgent) | Demonstrates using Azure Foundry Agents within a workflow | | [Custom Agent Executors](./Agents/CustomAgentExecutors) | Shows how to create a custom agent executor for more complex scenarios | | [Workflow as an Agent](./Agents/WorkflowAsAnAgent) | Illustrates how to encapsulate a workflow as an agent | +| [Group Chat with Tool Approval](./Agents/GroupChatToolApproval) | Shows multi-agent group chat with tool approval requests and human-in-the-loop interaction | ### Concurrent Execution From a9c0d5ddb03502f16b82eff3fc174cae2f9ecb93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:44:19 +0000 Subject: [PATCH 3/9] Fix comment numbering for better code clarity Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- .../Workflows/Agents/GroupChatToolApproval/Program.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs index 479a2ec308..b6ecc16137 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs @@ -45,11 +45,12 @@ private static async Task Main() 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"; - // 2. Create specialized agents + // 1. Create AI client IChatClient client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) .GetChatClient(deploymentName) .AsIChatClient(); + // 2. Create specialized agents with their tools ChatClientAgent qaEngineer = new( client, "You are a QA engineer responsible for running tests before deployment. Run the appropriate test suites and report results clearly.", @@ -188,7 +189,7 @@ private static async Task Main() } } - // 1. Define tools for different agents + // Tool definitions - These are called by the agents during workflow execution [Description("Run automated tests for the application.")] private static string RunTests([Description("Name of the test suite to run")] string testSuite) => $"Test suite '{testSuite}' completed: 47 passed, 0 failed, 0 skipped"; From 237e5a658b81d71a9b15d043070d47a167296e45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:05:31 +0000 Subject: [PATCH 4/9] Add GroupChatToolApproval project to solution Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- dotnet/agent-framework-dotnet.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 4bc586befc..be1816dcdd 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -229,6 +229,7 @@ + From 2535dde08b0e38ff84dbdb99a29d59cf6f964957 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:24:03 +0000 Subject: [PATCH 5/9] Use Name property instead of Id for agent selection Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- .../GroupChatToolApproval/DeploymentGroupChatManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs index 19938ae64e..92c151a03a 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs @@ -36,12 +36,12 @@ protected override ValueTask SelectNextAgentAsync( // First speaker after initial user message if (this.IterationCount == 0) { - AIAgent qaAgent = this._agents.First(a => a.Id == "QAEngineer"); + AIAgent qaAgent = this._agents.First(a => a.Name == "QAEngineer"); return new ValueTask(qaAgent); } // Subsequent speakers are DevOps Engineer - AIAgent devopsAgent = this._agents.First(a => a.Id == "DevOpsEngineer"); + AIAgent devopsAgent = this._agents.First(a => a.Name == "DevOpsEngineer"); return new ValueTask(devopsAgent); } } From 7610a6de165b14aa418f3b195379810a86d09ac7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:44:42 +0000 Subject: [PATCH 6/9] Fix file encoding - add UTF-8 BOM to C# files Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- .../Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs | 2 +- .../Workflows/Agents/GroupChatToolApproval/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs index 92c151a03a..db50333697 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/DeploymentGroupChatManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs index b6ecc16137..041e5cc2a3 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // This sample demonstrates how to use GroupChatBuilder with tools that require human // approval before execution. A group of specialized agents collaborate on a task, and From 6a12f2861b6a8e4ab9dabf3a83d82777bd52d43a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 27 Jan 2026 11:57:20 +0000 Subject: [PATCH 7/9] fix the sample --- .../Agents/GroupChatToolApproval/Program.cs | 89 +++++-------------- 1 file changed, 24 insertions(+), 65 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs index 041e5cc2a3..6511aa089d 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs @@ -83,14 +83,12 @@ private static async Task Main() // 5. Start the workflow Console.WriteLine("Starting group chat workflow for software deployment..."); - Console.WriteLine($"Agents: [{qaEngineer.Id}, {devopsEngineer.Id}]"); + Console.WriteLine($"Agents: [{qaEngineer.Name}, {devopsEngineer.Name}]"); Console.WriteLine(new string('-', 60)); List messages = [new(ChatRole.User, "We need to deploy version 2.4.0 to production. Please coordinate the deployment.")]; - // Phase 1: Run workflow and collect all events (stream ends at IDLE or IDLE_WITH_PENDING_REQUESTS) - List approvalRequests = []; - await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages); + await using StreamingRun run = await InProcessExecution.Lockstep.StreamAsync(workflow, messages); await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); string? lastExecutorId = null; @@ -98,14 +96,28 @@ private static async Task Main() { switch (evt) { + case RequestInfoEvent e: + { + if (e.Request.DataAs() is { } approvalRequestContent) + { + // Approve the tool call request + Console.WriteLine($"Tool: {approvalRequestContent.FunctionCall.Name} approved"); + await run.SendResponseAsync(e.Request.CreateResponse(approvalRequestContent.CreateResponse(approved: true))); + } + + break; + } + case AgentResponseUpdateEvent e: + { if (e.ExecutorId != lastExecutorId) { if (lastExecutorId is not null) { Console.WriteLine(); } - Console.Write($"- {e.ExecutorId}: "); + + Console.WriteLine($"- {e.ExecutorId}: "); lastExecutorId = e.ExecutorId; } @@ -118,75 +130,22 @@ private static async Task Main() if (approvalRequest is not null) { - approvalRequests.Add(approvalRequest); Console.WriteLine(); Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.ExecutorId}"); Console.WriteLine($" Tool: {approvalRequest.FunctionCall.Name}"); Console.WriteLine($" Arguments: {JsonSerializer.Serialize(approvalRequest.FunctionCall.Arguments)}"); - } - - break; - - case WorkflowOutputEvent: - // Workflow has completed, check if we have pending approval requests - if (approvalRequests.Count == 0) - { Console.WriteLine(); - Console.WriteLine("Workflow completed without requiring production deployment approval."); } - else - { - // We have approval requests to handle - Console.WriteLine(); - Console.WriteLine(new string('=', 60)); - Console.WriteLine("Human review required for production deployment!"); - Console.WriteLine("In a real scenario, you would review the deployment details here."); - Console.WriteLine("Simulating approval for demo purposes..."); - Console.WriteLine(new string('=', 60)); - - // Phase 2: Send approval and continue workflow - List approvalMessages = []; - foreach (FunctionApprovalRequestContent req in approvalRequests) - { - approvalMessages.Add(new ChatMessage(ChatRole.User, [req.CreateResponse(approved: true)])); - } - messages.AddRange(approvalMessages); - - // Resume workflow with approval responses - await using StreamingRun resumeRun = await InProcessExecution.StreamAsync(workflow, messages); - await resumeRun.TrySendMessageAsync(new TurnToken(emitEvents: true)); - - lastExecutorId = null; - await foreach (WorkflowEvent resumeEvt in resumeRun.WatchStreamAsync()) - { - if (resumeEvt is AgentResponseUpdateEvent e2) - { - if (e2.ExecutorId != lastExecutorId) - { - if (lastExecutorId is not null) - { - Console.WriteLine(); - } - Console.Write($"- {e2.ExecutorId}: "); - lastExecutorId = e2.ExecutorId; - } - - Console.Write(e2.Update.Text); - } - else if (resumeEvt is WorkflowOutputEvent) - { - Console.WriteLine(); - Console.WriteLine(new string('-', 60)); - Console.WriteLine("Deployment workflow completed successfully!"); - Console.WriteLine("All agents have finished their tasks."); - break; - } - } - } - return; + break; + } } } + + Console.WriteLine(); + Console.WriteLine(new string('-', 60)); + Console.WriteLine("Deployment workflow completed successfully!"); + Console.WriteLine("All agents have finished their tasks."); } // Tool definitions - These are called by the agents during workflow execution From 897769a7da57a376305911b7815bf7aa39e3867a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:05:52 +0000 Subject: [PATCH 8/9] Update dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Workflows/Agents/GroupChatToolApproval/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md index 9c3d401b90..84c6baa83c 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/README.md @@ -37,9 +37,9 @@ The `DeploymentGroupChatManager` implements custom speaker selection logic: ### Approval Handling -The sample demonstrates two-phase execution: -1. **Phase 1**: Run workflow until approval is needed -2. **Phase 2**: Handle approval requests and resume workflow +The sample demonstrates continuous event-driven execution with inline approval handling: +- The workflow runs in a single event loop. +- When an approval-required tool is invoked, the loop surfaces an approval request, processes the (simulated) human response, and then continues execution without starting a separate phase. ## Prerequisites From 62b3891b6d56e21ed6955afa7815035a9e6ca49b Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 27 Jan 2026 18:54:27 +0000 Subject: [PATCH 9/9] address PR review feedback --- .../Agents/GroupChatToolApproval/Program.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs index 6511aa089d..267c6d7ce5 100644 --- a/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Agents/GroupChatToolApproval/Program.cs @@ -98,8 +98,14 @@ private static async Task Main() { case RequestInfoEvent e: { - if (e.Request.DataAs() is { } approvalRequestContent) + if (e.Request.DataIs(out FunctionApprovalRequestContent? approvalRequestContent)) { + Console.WriteLine(); + Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.Request.PortInfo.PortId}"); + Console.WriteLine($" Tool: {approvalRequestContent.FunctionCall.Name}"); + Console.WriteLine($" Arguments: {JsonSerializer.Serialize(approvalRequestContent.FunctionCall.Arguments)}"); + Console.WriteLine(); + // Approve the tool call request Console.WriteLine($"Tool: {approvalRequestContent.FunctionCall.Name} approved"); await run.SendResponseAsync(e.Request.CreateResponse(approvalRequestContent.CreateResponse(approved: true))); @@ -123,20 +129,6 @@ private static async Task Main() Console.Write(e.Update.Text); - // Check for user input requests (approval requests) - FunctionApprovalRequestContent? approvalRequest = e.Update.Contents - .OfType() - .FirstOrDefault(); - - if (approvalRequest is not null) - { - Console.WriteLine(); - Console.WriteLine($"[APPROVAL REQUIRED] From agent: {e.ExecutorId}"); - Console.WriteLine($" Tool: {approvalRequest.FunctionCall.Name}"); - Console.WriteLine($" Arguments: {JsonSerializer.Serialize(approvalRequest.FunctionCall.Arguments)}"); - Console.WriteLine(); - } - break; } }