diff --git a/dotnet/samples/03-workflows/Agents/CustomAgentExecutors/Program.cs b/dotnet/samples/03-workflows/Agents/CustomAgentExecutors/Program.cs
index e2dec8505b..41b622fdad 100644
--- a/dotnet/samples/03-workflows/Agents/CustomAgentExecutors/Program.cs
+++ b/dotnet/samples/03-workflows/Agents/CustomAgentExecutors/Program.cs
@@ -61,6 +61,12 @@ private static async Task Main()
{
Console.WriteLine($"{outputEvent}");
}
+
+ if (evt is WorkflowErrorEvent errorEvent)
+ {
+ Console.WriteLine($"Workflow error: {errorEvent.Exception?.Message}");
+ Console.WriteLine($"Details: {errorEvent.Exception}");
+ }
}
}
}
@@ -175,7 +181,9 @@ internal sealed class FeedbackEvent(FeedbackResult feedbackResult) : WorkflowEve
///
/// A custom executor that uses an AI agent to provide feedback on a slogan.
///
-internal sealed class FeedbackExecutor : Executor
+[SendsMessage(typeof(FeedbackResult))]
+[YieldsOutput(typeof(string))]
+internal sealed partial class FeedbackExecutor : Executor
{
private readonly AIAgent _agent;
private AgentSession? _session;
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs
index b62377a971..66b67bffdf 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs
@@ -68,7 +68,7 @@ public static MethodAnalysisResult AnalyzeHandlerMethod(
string classKey = GetClassKey(classSymbol);
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
- bool configureProtocol = HasConfigureProtocolDefined(classSymbol);
+ bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);
// Extract class metadata
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
@@ -97,7 +97,7 @@ public static MethodAnalysisResult AnalyzeHandlerMethod(
return new MethodAnalysisResult(
classKey, @namespace, className, genericParameters, isNested, containingTypeChain,
baseHasConfigureProtocol, classSendTypes, classYieldTypes,
- isPartialClass, derivesFromExecutor, configureProtocol,
+ isPartialClass, derivesFromExecutor, hasManualConfigureProtocol,
classLocation,
handler,
Diagnostics: new ImmutableEquatableArray(methodDiagnostics.ToImmutable()));
@@ -149,7 +149,7 @@ public static AnalysisResult CombineHandlerMethodResults(IEnumerable AnalyzeClassProtocolAttribute(
bool isPartialClass = IsPartialClass(classSymbol, cancellationToken);
bool derivesFromExecutor = DerivesFromExecutor(classSymbol);
bool hasManualConfigureProtocol = HasConfigureProtocolDefined(classSymbol);
+ bool baseHasConfigureProtocol = BaseHasConfigureProtocol(classSymbol);
string? @namespace = classSymbol.ContainingNamespace?.IsGlobalNamespace == true
? null
@@ -241,6 +242,7 @@ public static ImmutableArray AnalyzeClassProtocolAttribute(
isPartialClass,
derivesFromExecutor,
hasManualConfigureProtocol,
+ baseHasConfigureProtocol,
classLocation,
typeName,
attributeKind));
@@ -321,7 +323,7 @@ public static AnalysisResult CombineOutputOnlyResults(IEnumerable.Empty,
ClassSendTypes: new ImmutableEquatableArray(sendTypes.ToImmutable()),
ClassYieldTypes: new ImmutableEquatableArray(yieldTypes.ToImmutable()));
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs
index df9205cc5f..1039855ea5 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs
@@ -5,7 +5,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
///
/// Represents protocol type information extracted from class-level [SendsMessage] or [YieldsOutput] attributes.
/// Used by the incremental generator pipeline to capture classes that declare protocol types
-/// but may not have [MessageHandler] methods (e.g., when ConfigureRoutes is manually implemented).
+/// but may not have [MessageHandler] methods (e.g., when ConfigureProtocol is manually implemented).
///
/// Unique identifier for the class (fully qualified name).
/// The namespace of the class.
@@ -15,7 +15,8 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
/// The chain of containing types for nested classes. Empty if not nested.
/// Whether the class is declared as partial.
/// Whether the class derives from Executor.
-/// Whether the class has a manually defined ConfigureRoutes method.
+/// Whether the class has a manually defined ConfigureProtocol method.
+/// Whether a base class already overrides ConfigureProtocol.
/// Location info for diagnostics.
/// The fully qualified type name from the attribute.
/// Whether this is from a SendsMessage or YieldsOutput attribute.
@@ -28,7 +29,8 @@ internal sealed record ClassProtocolInfo(
string ContainingTypeChain,
bool IsPartialClass,
bool DerivesFromExecutor,
- bool HasManualConfigureRoutes,
+ bool HasManualConfigureProtocol,
+ bool BaseHasConfigureProtocol,
DiagnosticLocationInfo? ClassLocation,
string TypeName,
ProtocolAttributeKind AttributeKind)
@@ -38,5 +40,5 @@ internal sealed record ClassProtocolInfo(
///
public static ClassProtocolInfo Empty { get; } = new(
string.Empty, null, string.Empty, null, false, string.Empty,
- false, false, false, null, string.Empty, ProtocolAttributeKind.Send);
+ false, false, false, false, null, string.Empty, ProtocolAttributeKind.Send);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/MethodAnalysisResult.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/MethodAnalysisResult.cs
index fb3fafc6c2..4b6df3f7a5 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/MethodAnalysisResult.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/MethodAnalysisResult.cs
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models;
/// Uses value-equatable types to support incremental generator caching.
///
///
-/// Class-level validation (IsPartialClass, DerivesFromExecutor, HasManualConfigureRoutes)
+/// Class-level validation (IsPartialClass, DerivesFromExecutor, HasManualConfigureProtocol)
/// is extracted here but validated once per class in CombineMethodResults to avoid
/// redundant validation work when a class has multiple handlers.
///
@@ -29,7 +29,7 @@ internal sealed record MethodAnalysisResult(
// Class-level facts (used for validation in CombineMethodResults)
bool IsPartialClass,
bool DerivesFromExecutor,
- bool HasManualConfigureRoutes,
+ bool HasManualConfigureProtocol,
// Class location for diagnostics (value-equatable)
DiagnosticLocationInfo? ClassLocation,
diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/ExecutorRouteGeneratorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/ExecutorRouteGeneratorTests.cs
index d2160486cc..8433dd5e3e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/ExecutorRouteGeneratorTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Generators.UnitTests/ExecutorRouteGeneratorTests.cs
@@ -651,7 +651,7 @@ private void HandleFromFile2(int message, IWorkflowContext context) { }
}
[Fact]
- public void PartialClass_SendsYieldsInBothFiles_GeneratesAlOverrides()
+ public void PartialClass_SendsYieldsInBothFiles_GeneratesAllOverrides()
{
// File 1: Partial with one handler
var file1 = """
@@ -700,7 +700,7 @@ private void HandleFromFile2(int message, IWorkflowContext context) { }
generated.Should().RegisterSentMessageType("string")
.And.RegisterSentMessageType("int")
.And.RegisterYieldedOutputType("string")
- .And.RegisterYieldedOutputType("string");
+ .And.RegisterYieldedOutputType("int");
}
#endregion
@@ -1046,6 +1046,85 @@ public GenericExecutor() : base("generic") { }
.And.RegisterSentMessageType("global::TestNamespace.BroadcastMessage");
}
+ [Fact]
+ public void ProtocolOnly_DerivesFromExecutorOfT_GeneratesBaseCall()
+ {
+ // A protocol-only partial executor deriving from Executor
+ // has a base class that already overrides ConfigureProtocol. The generator must emit
+ // "return base.ConfigureProtocol(protocolBuilder)" so inherited handler registrations
+ // are preserved — not "return protocolBuilder" which silently drops them.
+ var source = """
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Agents.AI.Workflows;
+
+ namespace TestNamespace;
+
+ public class FeedbackResult { }
+
+ [SendsMessage(typeof(FeedbackResult))]
+ [YieldsOutput(typeof(string))]
+ public partial class FeedbackExecutor : Executor
+ {
+ public FeedbackExecutor() : base("feedback") { }
+
+ public override System.Threading.Tasks.ValueTask HandleAsync(string message, IWorkflowContext context, System.Threading.CancellationToken cancellationToken = default)
+ => default;
+ }
+ """;
+
+ var result = GeneratorTestHelper.RunGenerator(source);
+
+ result.RunResult.GeneratedTrees.Should().HaveCount(1);
+ result.RunResult.Diagnostics.Should().BeEmpty();
+
+ var generated = result.RunResult.GeneratedTrees[0].ToString();
+
+ // Base class Executor overrides ConfigureProtocol, so the generated override
+ // must chain to base to preserve the inherited handler registration.
+ generated.Should().Contain("return base.ConfigureProtocol(protocolBuilder)",
+ because: "Executor overrides ConfigureProtocol, so base must be called to preserve its handler registration");
+ generated.Should().Contain(".SendsMessage()");
+ generated.Should().Contain(".YieldsOutput()");
+ }
+
+ [Fact]
+ public void ProtocolOnly_DerivesDirectlyFromExecutor_DoesNotGenerateBaseCall()
+ {
+ // A protocol-only partial executor deriving directly from Executor (abstract base
+ // with no non-abstract ConfigureProtocol override) should generate "return protocolBuilder"
+ // rather than "return base.ConfigureProtocol(protocolBuilder)".
+ var source = """
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Agents.AI.Workflows;
+
+ namespace TestNamespace;
+
+ public class BroadcastMessage { }
+
+ [SendsMessage(typeof(BroadcastMessage))]
+ public partial class BroadcastExecutor : Executor
+ {
+ public BroadcastExecutor() : base("broadcast") { }
+ }
+ """;
+
+ var result = GeneratorTestHelper.RunGenerator(source);
+
+ result.RunResult.GeneratedTrees.Should().HaveCount(1);
+ result.RunResult.Diagnostics.Should().BeEmpty();
+
+ var generated = result.RunResult.GeneratedTrees[0].ToString();
+
+ // Executor's ConfigureProtocol is abstract — no base call needed.
+ generated.Should().Contain("return protocolBuilder",
+ because: "Executor base class has no non-abstract ConfigureProtocol, so no base call is needed");
+ generated.Should().NotContain("base.ConfigureProtocol");
+ }
+
#endregion
#region Generic Executor Tests