diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs index 4c93414c63..ba21f9322b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/HandoffWorkflowBuilder.cs @@ -219,13 +219,17 @@ public class HandoffWorkflowBuilderCore where TBuilder : HandoffWorkfl if (string.IsNullOrWhiteSpace(handoffReason)) { - handoffReason = to.Description ?? to.Name ?? (to as ChatClientAgent)?.Instructions; + handoffReason = (string.IsNullOrWhiteSpace(to.Description) ? null : to.Description) + ?? (string.IsNullOrWhiteSpace(to.Name) ? null : $"handoff to {to.Name}") + ?? to.GetService()?.Instructions; + if (string.IsNullOrWhiteSpace(handoffReason)) { Throw.ArgumentException( nameof(to), - $"The provided target agent '{to.Name ?? to.Id}' has no description, name, or instructions, and no handoff description has been provided. " + - "At least one of these is required to register a handoff so that the appropriate target agent can be chosen."); + $"The provided target agent '{(string.IsNullOrWhiteSpace(to.Name) ? to.Id : to.Name)}' has no description, name, or instructions, and no " + + "handoff description has been provided. At least one of these is required to register a handoff so that the appropriate target agent can " + + "be chosen."); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs index c857811b08..fc984a9963 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AgentWorkflowBuilderTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; @@ -11,7 +12,9 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Agents.AI.Workflows.InProc; +using Microsoft.Agents.AI.Workflows.Specialized; using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; #pragma warning disable SYSLIB1045 // Use GeneratedRegex #pragma warning disable RCS1186 // Use Regex instance instead of static method @@ -52,6 +55,51 @@ public class AgentWorkflowBuilderTests var noDescriptionAgent = new ChatClientAgent(new MockChatClient(delegate { return new(); })); Assert.Throws("to", () => handoffs.WithHandoff(agent, noDescriptionAgent)); + + var emptyDescriptionAgent = new MockChatClient(delegate { return new(); }).AsAIAgent(description: ""); + Assert.Throws("to", () => handoffs.WithHandoff(agent, emptyDescriptionAgent)); + + var emptyNameAgent = new MockChatClient(delegate { return new(); }).AsAIAgent(name: ""); + Assert.Throws("to", () => handoffs.WithHandoff(agent, emptyNameAgent)); + } + + private sealed class NullLogger : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return false; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + } + } + + [Fact] + public void BuildHandoffs_DelegatingAIAgent_DoesNotThrow() + { + DoubleEchoAgent agent = new("agent"); + HandoffWorkflowBuilder handoffs = AgentWorkflowBuilder.CreateHandoffBuilderWith(agent); + Assert.NotNull(handoffs); + + ChatClientAgent instructionsOnlyAgent = new MockChatClient(delegate { return new(); }).AsAIAgent(instructions: "instructions"); + LoggingAgent delegatingAgent = new(instructionsOnlyAgent, new NullLogger()); + + handoffs.WithHandoff(agent, delegatingAgent); + + // get the _targets field from the HandoffWorkflowBuilder (need to use the base type) + FieldInfo field = typeof(HandoffWorkflowBuilder).BaseType!.GetField("_targets", BindingFlags.Instance | BindingFlags.NonPublic)!; + Dictionary>? targets = field.GetValue(handoffs) as Dictionary>; + + targets.Should().NotBeNull(); + + HandoffTarget target = targets[agent].Single(); + target.Reason.Should().Be("instructions"); } [Fact]