From 2c9f54310089d767d30efcf3fabee18ad565395e Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Mon, 13 Apr 2026 13:38:30 -0400 Subject: [PATCH 1/2] feat: Add Handoff sample --- dotnet/agent-framework-dotnet.slnx | 5 +- .../Orchestration/Handoff/AgentRegistry.cs | 72 ++++++++++ .../Orchestration/Handoff/Handoff.csproj | 29 ++++ .../Orchestration/Handoff/Program.cs | 125 ++++++++++++++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs create mode 100644 dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj create mode 100644 dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 24b596509e..a9112fcb15 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -243,6 +243,9 @@ + + + @@ -288,7 +291,7 @@ - + diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs b/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs new file mode 100644 index 0000000000..3a21dd8d28 --- /dev/null +++ b/dotnet/samples/03-workflows/Orchestration/Handoff/AgentRegistry.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +/// +/// The registry of agents used in the workflow. +/// +/// The to use as the agent backend. +internal sealed class AgentRegistry(IChatClient chatClient) +{ + internal const string IntakeAgentName = "Assistant"; + public AIAgent IntakeAgent { get; } = chatClient.AsAIAgent( + instructions: + """ + You receive a user request and are responsible for routing to the correct initial expert agent. + """, + IntakeAgentName + ); + + internal const string LiquidityAnalysisAgentName = "Liquidity Analysis"; + public AIAgent LiquidityAnalysisAgent { get; } = chatClient.AsAIAgent( + instructions: + """ + You are responsible for Liquidity Analysis. + """, + LiquidityAnalysisAgentName + ); + + internal const string TaxAnalysisAgentName = "Tax Analysis"; + public AIAgent TaxAnalysisAgent { get; } = chatClient.AsAIAgent( + instructions: + """ + You are responsible for Tax Analysis. + """, + TaxAnalysisAgentName + ); + + internal const string ForeignExchangeAgentName = "Foreign Exchange Analysis"; + public AIAgent ForeignExchangeAgent { get; } = chatClient.AsAIAgent( + instructions: + """ + You are responsible for Foreign Exchange Analysis. + """, + ForeignExchangeAgentName + ); + + internal const string EquityAgentName = "Equity Analysis"; + public AIAgent EquityAgent { get; } = chatClient.AsAIAgent( + instructions: + """ + You are responsible for Equity Analysis. + """, + EquityAgentName + ); + + public IEnumerable Experts => [this.LiquidityAnalysisAgent, this.TaxAnalysisAgent, this.ForeignExchangeAgent, this.EquityAgent]; + + public HashSet All + { + get + { + if (field == null) + { + field = [this.IntakeAgent, .. this.Experts]; + } + + return field; + } + } +} diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj b/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj new file mode 100644 index 0000000000..5fe709e505 --- /dev/null +++ b/dotnet/samples/03-workflows/Orchestration/Handoff/Handoff.csproj @@ -0,0 +1,29 @@ + + + + Exe + net10.0 + + enable + enable + + MAAIW001 + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs new file mode 100644 index 0000000000..69cf8c168b --- /dev/null +++ b/dotnet/samples/03-workflows/Orchestration/Handoff/Program.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Projects; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +AIProjectClient projectClient = new(new Uri(endpoint), new DefaultAzureCredential()); + +IChatClient chatClient = projectClient.ProjectOpenAIClient + .GetChatClient(deploymentName) + .AsIChatClient(); + +Workflow workflow = CreateWorkflow(chatClient); + +await RunWorkflowAsync(workflow).ConfigureAwait(false); + +static Workflow CreateWorkflow(IChatClient chatClient) +{ + AgentRegistry agents = new(chatClient); + + HandoffWorkflowBuilder handoffBuilder = AgentWorkflowBuilder.CreateHandoffBuilderWith(agents.IntakeAgent); + + // Add a handoff to each of the experts from every agent in the registry (experts + Intake) + foreach (AIAgent expert in agents.Experts) + { + handoffBuilder.WithHandoffs(agents.All.Except([expert]), expert); + } + + // Let agents request more user information and return to the asking agent (rather than going back to the intake agent) + handoffBuilder.EnableReturnToPrevious(); + + return handoffBuilder.Build(); +} + +static async Task RunWorkflowAsync(Workflow workflow) +{ + using CancellationTokenSource cts = CreateConsoleCancelKeySource(); + await using StreamingRun run = await InProcessExecution.OpenStreamingAsync(workflow, cancellationToken: cts.Token) + .ConfigureAwait(false); + + bool hadError = false; + do + { + Console.Write("> "); + string userInput = Console.ReadLine() ?? string.Empty; + + if (userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + await run.TrySendMessageAsync(userInput); + string? speakingAgent = null; + await foreach (WorkflowEvent evt in run.WatchStreamAsync(cts.Token)) + { + switch (evt) + { + case AgentResponseUpdateEvent update: + { + if (speakingAgent == null || speakingAgent != update.Update.AuthorName) + { + speakingAgent = update.Update.AuthorName; + Console.Write($"\n{speakingAgent}: "); + } + + Console.Write(update.Update.Text); + break; + } + + case WorkflowErrorEvent workflowError: + { + Console.ForegroundColor = ConsoleColor.Red; + + if (workflowError.Exception != null) + { + Console.WriteLine($"\nWorkflow error: {workflowError.Exception}"); + } + else + { + Console.WriteLine("\nUnknown workflow error occurred."); + } + + Console.ResetColor(); + + hadError = true; + break; + } + + case WorkflowWarningEvent workflowWarning when workflowWarning.Data is string message: + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(message); + Console.ResetColor(); + break; + } + } + } + } while (!hadError); +} + +static CancellationTokenSource CreateConsoleCancelKeySource() +{ + CancellationTokenSource cts = new(); + + // Normally, support a way to detach events, but in this case this is a termination signal, so cleanup will happen + // as part of application shutdown. + Console.CancelKeyPress += (s, args) => + { + cts.Cancel(); + + // We handle cleanup + termination ourselves + args.Cancel = true; + }; + + return cts; +} From f8533706210171d4770e600ef4e53396ce63b691 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 16 Apr 2026 12:42:28 -0400 Subject: [PATCH 2/2] docs: Add Handoff sample to readme --- dotnet/samples/03-workflows/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dotnet/samples/03-workflows/README.md b/dotnet/samples/03-workflows/README.md index d17148d60d..600a4c70ca 100644 --- a/dotnet/samples/03-workflows/README.md +++ b/dotnet/samples/03-workflows/README.md @@ -56,3 +56,9 @@ Once completed, please proceed to the other samples listed below. | [Edge Conditions](./ConditionalEdges/01_EdgeCondition) | Introduces conditional edges for dynamic routing based on executor outputs | | [Switch-Case Routing](./ConditionalEdges/02_SwitchCase) | Extends conditional edges with switch-case routing for multiple paths | | [Multi-Selection Routing](./ConditionalEdges/03_MultiSelection) | Demonstrates multi-selection routing where one executor can trigger multiple downstream executors | + +### Orchestration Patterns + +| Sample | Concepts | +|--------|----------| +| [Handoff Orchestration](./Orchestration/Handoff) | Introduces the Handoff Orchestration pattern |