From 13ab685637158b802c1b3888baf8c66f2a123e65 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:16:04 -0700 Subject: [PATCH 1/2] Clarify HITL options for .Net agent orchestrations (#1066) * update doc to clarify HITL options for .Net agent orchestrations --- agent-framework/workflows/human-in-the-loop.md | 4 +++- .../workflows/orchestrations/sequential.md | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/agent-framework/workflows/human-in-the-loop.md b/agent-framework/workflows/human-in-the-loop.md index a2ba8fce..6ba3a9c3 100644 --- a/agent-framework/workflows/human-in-the-loop.md +++ b/agent-framework/workflows/human-in-the-loop.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial ms.author: taochen -ms.date: 05/27/2026 +ms.date: 07/16/2026 ms.service: agent-framework --- @@ -304,6 +304,8 @@ The `RequestPort` pattern described above works with custom executors and `Workf Agents can use tools that require human approval before execution. When the agent attempts to call an approval-required tool, the workflow pauses and emits a `RequestInfoEvent` just like the `RequestPort` pattern, but the event payload contains a `ToolApprovalRequestContent` (C# and Go) or a `Content` with `type == "function_approval_request"` (Python) instead of a custom request type. +For interactive scenarios where an agent needs to gather more information from the user and iterate before proceeding; rather than only approving or rejecting a tool call; use the **[handoff orchestration](./orchestrations/handoff.md)**. Handoff is interactive by default: when an agent responds without handing off to another agent, control returns to the user for the next input, which enables multi-turn back-and-forth within the orchestration. Sequential, concurrent, and group chat orchestrations do not pause for free-form user input on their own; pair them with a `RequestPort` in a custom `WorkflowBuilder` workflow when you need that control between steps. + > [!TIP] > For complete examples with code, see: > - [Sequential orchestration with HITL](./orchestrations/sequential.md#sequential-orchestration-with-human-in-the-loop) diff --git a/agent-framework/workflows/orchestrations/sequential.md b/agent-framework/workflows/orchestrations/sequential.md index b2d323cb..b4fe9d78 100644 --- a/agent-framework/workflows/orchestrations/sequential.md +++ b/agent-framework/workflows/orchestrations/sequential.md @@ -5,7 +5,7 @@ zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial ms.author: taochen -ms.date: 07/01/2026 +ms.date: 07/16/2026 ms.service: agent-framework --- @@ -19,7 +19,7 @@ ms.service: agent-framework | Set Up the Sequential Orchestration | ✅ | ✅ | ✅ | | | Run the Sequential Workflow | ✅ | ✅ | ✅ | | | Sample Output | ✅ | ✅ | ✅ | | - | Sequential with Human-in-the-Loop | ✅ | ✅ | ✅ | | + | Sequential with Human-in-the-Loop | ✅ | ✅ | ✅ | Python adds a request-info feedback step; C# covers tool approval and links to Handoff for interactive HITL | | Advanced: Mixing Agents with Custom Executors | ❌ | ✅ | ✅ | Python/Go-specific | | Controlling Context Between Agents | ❌ | ✅ | ✅ | Python/Go-specific | | Intermediate Outputs | ❌ | ✅ | ✅ | Python/Go-specific | @@ -207,6 +207,13 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync()) > [!TIP] > For a complete runnable example of this approval flow, see the [`GroupChatToolApproval` sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/03-workflows/Agents/GroupChatToolApproval). The same `RequestInfoEvent` handling pattern applies to other orchestrations. +### Beyond Tool Approval: Interactive Feedback + +Tool approval lets a human accept or reject a specific tool call, but a sequential orchestration does not include a built-in step to pause for free-form user feedback between agents, and it cannot return control to a previous agent. When an agent needs to interactively ask the user for more information and iterate before continuing; for example, collecting booking details before it calls a reservation tool; use one of the following approaches instead: + +- **[Handoff orchestration](./handoff.md)** is interactive by default: when an agent responds without handing off, control returns to the user for the next input, enabling multi-turn back-and-forth within the orchestration. Restrict each agent to a single handoff target to approximate a sequential flow that still pauses for user input. +- A **custom workflow** built with `WorkflowBuilder` and a [`RequestPort`](../human-in-the-loop.md) lets you send a typed request to the user at any point and route the response back to an executor, which you can place before or after your agents in the pipeline. + ## Key Concepts - **Sequential Processing**: Each agent processes the output of the previous agent in order @@ -216,6 +223,7 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync()) - **Event Handling**: Monitor agent progress through `AgentResponseUpdateEvent` and completion through `WorkflowOutputEvent` - **Tool Approval**: Wrap sensitive tools with `ApprovalRequiredAIFunction` to require human approval before execution - **RequestInfoEvent**: Emitted when a tool requires approval; contains `ToolApprovalRequestContent` with the tool call details +- **Interactive HITL**: Sequential orchestration covers tool approval; for interactive back-and-forth where an agent gathers more information from the user, use [handoff orchestration](./handoff.md) or a custom `RequestPort` workflow ::: zone-end From a6ca043582eddd15880ea5e92ae8d4b5f328fffe Mon Sep 17 00:00:00 2001 From: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:16:17 -0700 Subject: [PATCH 2/2] Update documentation for C# handoff orchestration (#1067) --- .../workflows/orchestrations/handoff.md | 127 +++++++++++++++++- 1 file changed, 124 insertions(+), 3 deletions(-) diff --git a/agent-framework/workflows/orchestrations/handoff.md b/agent-framework/workflows/orchestrations/handoff.md index 9494a8f9..5ee9da1f 100644 --- a/agent-framework/workflows/orchestrations/handoff.md +++ b/agent-framework/workflows/orchestrations/handoff.md @@ -4,7 +4,7 @@ description: In-depth look at Handoff Orchestrations in Microsoft Agent Framewor author: TaoChenOSU ms.topic: tutorial ms.author: taochen -ms.date: 05/27/2026 +ms.date: 07/16/2026 ms.service: agent-framework zone_pivot_groups: programming-languages --- @@ -18,8 +18,8 @@ zone_pivot_groups: programming-languages | Define Specialized Agents | ✅ | ✅ | ❌ | | | Configure Handoff Rules | ✅ | ✅ | ❌ | | | Run Interactive Workflow | ✅ | ✅ | ❌ | Different patterns per language | - | Autonomous Mode | ❌ | ✅ | ❌ | Python-specific | - | Tool Approval (HITL) | ❌ | ✅ | ❌ | | + | Autonomous Mode | ✅ | ✅ | ❌ | | + | Tool Approval (HITL) | ✅ | ✅ | ❌ | | | Checkpointing | ❌ | ✅ | ❌ | | | Sample Interaction | ✅ | ✅ | ❌ | | | Context Synchronization | ✅ | ✅ | ❌ | Shared section | @@ -180,6 +180,125 @@ triage_agent: This is another math question. I'll route this to the math tutor. math_tutor: I'd be happy to help with calculus integration! Integration is the reverse of differentiation. The basic power rule for integration is: ∫x^n dx = x^(n+1)/(n+1) + C, where C is the constant of integration. ``` +## Autonomous Mode + +By default, handoff orchestration is interactive: when an agent responds without handing off, the workflow returns control to you for the next user input. Enable **autonomous mode** to let an agent keep working without waiting for user input. When an agent does not hand off, the workflow feeds it a continuation prompt and invokes it again, until the agent hands off, a termination condition is met, or the per-agent turn limit is reached. + +Enable it by calling `WithAutonomousMode()` on the handoff builder: + +```csharp +var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent) + .WithHandoffs(triageAgent, [mathTutor, historyTutor]) + .WithHandoffs([mathTutor, historyTutor], triageAgent) + .WithAutonomousMode() + .Build(); +``` + +By default, each agent runs up to 50 autonomous turns, and each continuation uses the prompt `"User did not respond. Continue assisting autonomously."`. Override the turn limit and prompt as needed: + +```csharp +var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent) + .WithHandoffs(triageAgent, [mathTutor, historyTutor]) + .WithAutonomousMode(turnLimit: 10, continuationPrompt: "Continue assisting the user.") + .Build(); +``` + +Pass a list of agents to the `agents` parameter to enable autonomous mode for only a subset of participants. Agents not in the list always return control after a single response: + +```csharp +var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent) + .WithHandoffs(triageAgent, [mathTutor, historyTutor]) + .WithAutonomousMode(agents: [triageAgent]) // Only triageAgent runs autonomously + .Build(); +``` + +Combine autonomous mode with a termination condition to stop the loop when the conversation reaches a certain state: + +```csharp +var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent) + .WithHandoffs(triageAgent, [mathTutor, historyTutor]) + .WithAutonomousMode(turnLimit: 10) + .WithTerminationCondition(conversation => conversation.Any(m => m.Text?.Contains("RESOLVED") == true)) + .Build(); +``` + +## Advanced: Tool Approval in Handoff Workflows + +Agents in a handoff workflow can use tools that require human approval before they run; useful for sensitive operations such as processing refunds, making purchases, or executing irreversible actions. Wrap the sensitive function with `ApprovalRequiredAIFunction`. When the agent tries to call it, the workflow pauses and emits a `RequestInfoEvent` containing a `ToolApprovalRequestContent`. + +### Define Agents with Approval-Required Tools + +```csharp +ChatClientAgent triageAgent = new(client, + "You are frontline support. Route the customer to the right specialist.", + "triage_agent", + "Routes customers to specialists"); + +ChatClientAgent refundAgent = new(client, + "You process refund requests.", + "refund_agent", + "Handles refund requests", + [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ProcessRefund))]); +``` + +### Handle User Input and Tool Approval Requests + +Two things can pause a handoff workflow: an agent finishing its turn and waiting for the next user message, and an approval-required tool call. Handle both in the same event loop; respond to a `RequestInfoEvent` approval with `SendResponseAsync`, and supply the next user message when the workflow returns control: + +```csharp +var workflow = AgentWorkflowBuilder.CreateHandoffBuilderWith(triageAgent) + .WithHandoffs(triageAgent, [refundAgent]) + .WithHandoffs([refundAgent], triageAgent) + .Build(); + +List messages = []; + +while (true) +{ + Console.Write("You: "); + string userInput = Console.ReadLine()!; + if (userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + messages.Add(new(ChatRole.User, userInput)); + + await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, messages); + await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); + + List newMessages = []; + await foreach (WorkflowEvent evt in run.WatchStreamAsync()) + { + // An approval-required tool call pauses the workflow and emits a RequestInfoEvent. + if (evt is RequestInfoEvent requestEvt && + requestEvt.Request.TryGetDataAs(out ToolApprovalRequestContent? approval)) + { + var toolCall = (FunctionCallContent)approval.ToolCall; + Console.Write($"Approve {toolCall.Name}? (y/n): "); + bool approved = (Console.ReadLine() ?? "n").Trim().Equals("y", StringComparison.OrdinalIgnoreCase); + await run.SendResponseAsync(requestEvt.Request.CreateResponse(approval.CreateResponse(approved))); + } + else if (evt is AgentResponseUpdateEvent update) + { + Console.Write(update.Update.Text); + } + else if (evt is WorkflowOutputEvent outputEvt) + { + newMessages = outputEvt.As>()!; + break; + } + } + + // Control returns here after the agent responds without handing off. Merge the new + // messages into the conversation and loop to collect the next user input. + messages.AddRange(newMessages.Skip(messages.Count)); +} +``` + +> [!NOTE] +> Tool approval works with `CreateHandoffBuilderWith()` out of the box; no extra builder configuration is needed. When an agent calls a tool wrapped with `ApprovalRequiredAIFunction`, the workflow automatically pauses and emits a `RequestInfoEvent`. The same `RequestInfoEvent` handling pattern is used across orchestrations; see the [`GroupChatToolApproval` sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/03-workflows/Agents/GroupChatToolApproval) for a complete runnable project. + ::: zone-end ::: zone pivot="programming-language-python" @@ -635,6 +754,8 @@ After broadcasting the response, the participant then checks whether it needs to - **Context Preservation**: Full conversation history is maintained across all handoffs - **Multi-turn Support**: Supports ongoing conversations with seamless agent switching - **Specialized Expertise**: Each agent focuses on their domain while collaborating through handoffs +- **WithAutonomousMode()**: Lets agents continue without waiting for user input, up to a per-agent turn limit or until a termination condition is met +- **Tool Approval (HITL)**: Wrap sensitive tools with `ApprovalRequiredAIFunction`; the workflow pauses and emits a `RequestInfoEvent` with `ToolApprovalRequestContent`, which you answer via `SendResponseAsync` ::: zone-end