From ec5d9397de09d3f2aeac66b6fe1e330f26ce6f19 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Tue, 17 Mar 2026 15:21:48 -0700 Subject: [PATCH 1/3] Fix source generator bug that silently drops base class handler registrations for protocol-only partial executors --- .../Agents/CustomAgentExecutors/Program.cs | 10 +++++++++- .../Analysis/SemanticAnalyzer.cs | 4 +++- .../Models/ClassProtocolInfo.cs | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) 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..c116587507 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Analysis/SemanticAnalyzer.cs @@ -212,6 +212,7 @@ public static ImmutableArray 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..90e5762890 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Models/ClassProtocolInfo.cs @@ -16,6 +16,7 @@ namespace Microsoft.Agents.AI.Workflows.Generators.Models; /// Whether the class is declared as partial. /// Whether the class derives from Executor. /// Whether the class has a manually defined ConfigureRoutes 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. @@ -29,6 +30,7 @@ internal sealed record ClassProtocolInfo( bool IsPartialClass, bool DerivesFromExecutor, bool HasManualConfigureRoutes, + 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); } From d8dadfb4a48807917870cf09347692e097a632e0 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Tue, 17 Mar 2026 22:26:01 -0700 Subject: [PATCH 2/3] Fixed xml comments and variable naming. --- .../Analysis/SemanticAnalyzer.cs | 6 +- .../Models/ClassProtocolInfo.cs | 6 +- .../Models/MethodAnalysisResult.cs | 4 +- .../ExecutorRouteGeneratorTests.cs | 83 ++++++++++++++++++- 4 files changed, 89 insertions(+), 10 deletions(-) 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 c116587507..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 /// 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,7 @@ 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. @@ -29,7 +29,7 @@ internal sealed record ClassProtocolInfo( string ContainingTypeChain, bool IsPartialClass, bool DerivesFromExecutor, - bool HasManualConfigureRoutes, + bool HasManualConfigureProtocol, bool BaseHasConfigureProtocol, DiagnosticLocationInfo? ClassLocation, string TypeName, 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 From 4e0cb190972343d35d66f16d610ee4de0019e573 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Thu, 19 Mar 2026 13:16:23 -0700 Subject: [PATCH 3/3] Fix workflow samples broken due to routing change --- .../Agents/WorkflowAsAnAgent/WorkflowFactory.cs | 1 + .../Checkpoint/CheckpointAndRehydrate/WorkflowFactory.cs | 3 +++ .../Checkpoint/CheckpointAndResume/WorkflowFactory.cs | 3 +++ .../CheckpointWithHumanInTheLoop/WorkflowFactory.cs | 2 ++ .../samples/03-workflows/Concurrent/Concurrent/Program.cs | 5 ++++- dotnet/samples/03-workflows/Concurrent/MapReduce/Program.cs | 5 +++++ .../ConditionalEdges/01_EdgeCondition/Program.cs | 2 ++ .../03-workflows/ConditionalEdges/02_SwitchCase/Program.cs | 3 +++ .../ConditionalEdges/03_MultiSelection/Program.cs | 3 +++ .../HumanInTheLoop/HumanInTheLoopBasic/WorkflowFactory.cs | 2 ++ dotnet/samples/03-workflows/Loop/Program.cs | 6 ++++-- dotnet/samples/03-workflows/SharedStates/Program.cs | 4 ++++ .../06_MixedWorkflowAgentsAndExecutors/Program.cs | 4 ++++ 13 files changed, 40 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs b/dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs index 669b9ac87c..2fdfe703bf 100644 --- a/dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs +++ b/dotnet/samples/03-workflows/Agents/WorkflowAsAnAgent/WorkflowFactory.cs @@ -41,6 +41,7 @@ private static ChatClientAgent GetLanguageAgent(string targetLanguage, IChatClie /// /// Executor that aggregates the results from the concurrent agents. /// + [YieldsOutput(typeof(string))] private sealed class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor"), IResettableExecutor { diff --git a/dotnet/samples/03-workflows/Checkpoint/CheckpointAndRehydrate/WorkflowFactory.cs b/dotnet/samples/03-workflows/Checkpoint/CheckpointAndRehydrate/WorkflowFactory.cs index 5c55293708..49fb4648c3 100644 --- a/dotnet/samples/03-workflows/Checkpoint/CheckpointAndRehydrate/WorkflowFactory.cs +++ b/dotnet/samples/03-workflows/Checkpoint/CheckpointAndRehydrate/WorkflowFactory.cs @@ -41,6 +41,7 @@ internal enum NumberSignal /// /// Executor that makes a guess based on the current bounds. /// +[SendsMessage(typeof(int))] internal sealed class GuessNumberExecutor() : Executor("Guess") { /// @@ -104,6 +105,8 @@ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext co /// /// Executor that judges the guess and provides feedback. /// +[SendsMessage(typeof(NumberSignal))] +[YieldsOutput(typeof(string))] internal sealed class JudgeExecutor() : Executor("Judge") { private readonly int _targetNumber; diff --git a/dotnet/samples/03-workflows/Checkpoint/CheckpointAndResume/WorkflowFactory.cs b/dotnet/samples/03-workflows/Checkpoint/CheckpointAndResume/WorkflowFactory.cs index aa8b3fd192..60269f6129 100644 --- a/dotnet/samples/03-workflows/Checkpoint/CheckpointAndResume/WorkflowFactory.cs +++ b/dotnet/samples/03-workflows/Checkpoint/CheckpointAndResume/WorkflowFactory.cs @@ -41,6 +41,7 @@ internal enum NumberSignal /// /// Executor that makes a guess based on the current bounds. /// +[SendsMessage(typeof(int))] internal sealed class GuessNumberExecutor() : Executor("Guess") { /// @@ -104,6 +105,8 @@ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext co /// /// Executor that judges the guess and provides feedback. /// +[SendsMessage(typeof(NumberSignal))] +[YieldsOutput(typeof(string))] internal sealed class JudgeExecutor() : Executor("Judge") { private readonly int _targetNumber; diff --git a/dotnet/samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowFactory.cs b/dotnet/samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowFactory.cs index df79a1ee61..8fc1a05236 100644 --- a/dotnet/samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowFactory.cs +++ b/dotnet/samples/03-workflows/Checkpoint/CheckpointWithHumanInTheLoop/WorkflowFactory.cs @@ -53,6 +53,8 @@ public SignalWithNumber(NumberSignal signal, int? number = null) /// /// Executor that judges the guess and provides feedback. /// +[SendsMessage(typeof(SignalWithNumber))] +[YieldsOutput(typeof(string))] internal sealed class JudgeExecutor() : Executor("Judge") { private readonly int _targetNumber; diff --git a/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs b/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs index 8ed879c685..57b650ce82 100644 --- a/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs +++ b/dotnet/samples/03-workflows/Concurrent/Concurrent/Program.cs @@ -72,6 +72,8 @@ private static async Task Main() /// /// Executor that starts the concurrent processing by sending messages to the agents. /// +[SendsMessage(typeof(ChatMessage))] +[SendsMessage(typeof(TurnToken))] internal sealed partial class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor") { @@ -97,7 +99,8 @@ public async ValueTask HandleAsync(string message, IWorkflowContext context, Can /// /// Executor that aggregates the results from the concurrent agents. /// -internal sealed class ConcurrentAggregationExecutor() : +[YieldsOutput(typeof(string))] +internal sealed partial class ConcurrentAggregationExecutor() : Executor>("ConcurrentAggregationExecutor") { private readonly List _messages = []; diff --git a/dotnet/samples/03-workflows/Concurrent/MapReduce/Program.cs b/dotnet/samples/03-workflows/Concurrent/MapReduce/Program.cs index 81fbb6b28a..9049bde982 100644 --- a/dotnet/samples/03-workflows/Concurrent/MapReduce/Program.cs +++ b/dotnet/samples/03-workflows/Concurrent/MapReduce/Program.cs @@ -128,6 +128,7 @@ private static async Task RunWorkflowAsync(Workflow workflow) /// /// Splits data into roughly equal chunks based on the number of mapper nodes. /// +[SendsMessage(typeof(SplitComplete))] internal sealed class Split(string[] mapperIds, string id) : Executor(id) { @@ -186,6 +187,7 @@ private static string[] Preprocess(string data) /// /// Maps each token to a count of 1 and writes pairs to a per-mapper file. /// +[SendsMessage(typeof(MapComplete))] internal sealed class Mapper(string id) : Executor(id) { /// @@ -212,6 +214,7 @@ public override async ValueTask HandleAsync(SplitComplete message, IWorkflowCont /// /// Groups intermediate pairs by key and partitions them across reducers. /// +[SendsMessage(typeof(ShuffleComplete))] internal sealed class Shuffler(string[] reducerIds, string[] mapperIds, string id) : Executor(id) { @@ -311,6 +314,7 @@ async Task ProcessChunkAsync(List<(string key, List values)> chunk, int ind /// /// Sums grouped counts per key for its assigned partition. /// +[SendsMessage(typeof(ReduceComplete))] internal sealed class Reducer(string id) : Executor(id) { /// @@ -352,6 +356,7 @@ public override async ValueTask HandleAsync(ShuffleComplete message, IWorkflowCo /// /// Joins all reducer outputs and yields the final output. /// +[YieldsOutput(typeof(List))] internal sealed class CompletionExecutor(string id) : Executor>(id) { diff --git a/dotnet/samples/03-workflows/ConditionalEdges/01_EdgeCondition/Program.cs b/dotnet/samples/03-workflows/ConditionalEdges/01_EdgeCondition/Program.cs index f22ab6e269..de4f252deb 100644 --- a/dotnet/samples/03-workflows/ConditionalEdges/01_EdgeCondition/Program.cs +++ b/dotnet/samples/03-workflows/ConditionalEdges/01_EdgeCondition/Program.cs @@ -228,6 +228,7 @@ public override async ValueTask HandleAsync(DetectionResult messa /// /// Executor that sends emails. /// +[YieldsOutput(typeof(string))] internal sealed class SendEmailExecutor() : Executor("SendEmailExecutor") { /// @@ -240,6 +241,7 @@ public override async ValueTask HandleAsync(EmailResponse message, IWorkflowCont /// /// Executor that handles spam messages. /// +[YieldsOutput(typeof(string))] internal sealed class HandleSpamExecutor() : Executor("HandleSpamExecutor") { /// diff --git a/dotnet/samples/03-workflows/ConditionalEdges/02_SwitchCase/Program.cs b/dotnet/samples/03-workflows/ConditionalEdges/02_SwitchCase/Program.cs index 69a8ec0826..7dd5927711 100644 --- a/dotnet/samples/03-workflows/ConditionalEdges/02_SwitchCase/Program.cs +++ b/dotnet/samples/03-workflows/ConditionalEdges/02_SwitchCase/Program.cs @@ -252,6 +252,7 @@ public override async ValueTask HandleAsync(DetectionResult messa /// /// Executor that sends emails. /// +[YieldsOutput(typeof(string))] internal sealed class SendEmailExecutor() : Executor("SendEmailExecutor") { /// @@ -264,6 +265,7 @@ public override async ValueTask HandleAsync(EmailResponse message, IWorkflowCont /// /// Executor that handles spam messages. /// +[YieldsOutput(typeof(string))] internal sealed class HandleSpamExecutor() : Executor("HandleSpamExecutor") { /// @@ -285,6 +287,7 @@ public override async ValueTask HandleAsync(DetectionResult message, IWorkflowCo /// /// Executor that handles uncertain emails. /// +[YieldsOutput(typeof(string))] internal sealed class HandleUncertainExecutor() : Executor("HandleUncertainExecutor") { /// diff --git a/dotnet/samples/03-workflows/ConditionalEdges/03_MultiSelection/Program.cs b/dotnet/samples/03-workflows/ConditionalEdges/03_MultiSelection/Program.cs index 22eb589dbb..d41c0ff275 100644 --- a/dotnet/samples/03-workflows/ConditionalEdges/03_MultiSelection/Program.cs +++ b/dotnet/samples/03-workflows/ConditionalEdges/03_MultiSelection/Program.cs @@ -310,6 +310,7 @@ public override async ValueTask HandleAsync(AnalysisResult messag /// /// Executor that sends emails. /// +[YieldsOutput(typeof(string))] internal sealed class SendEmailExecutor() : Executor("SendEmailExecutor") { /// @@ -322,6 +323,7 @@ public override async ValueTask HandleAsync(EmailResponse message, IWorkflowCont /// /// Executor that handles spam messages. /// +[YieldsOutput(typeof(string))] internal sealed class HandleSpamExecutor() : Executor("HandleSpamExecutor") { /// @@ -343,6 +345,7 @@ public override async ValueTask HandleAsync(AnalysisResult message, IWorkflowCon /// /// Executor that handles uncertain messages. /// +[YieldsOutput(typeof(string))] internal sealed class HandleUncertainExecutor() : Executor("HandleUncertainExecutor") { /// diff --git a/dotnet/samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowFactory.cs b/dotnet/samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowFactory.cs index 460de5ede1..5f36faded9 100644 --- a/dotnet/samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowFactory.cs +++ b/dotnet/samples/03-workflows/HumanInTheLoop/HumanInTheLoopBasic/WorkflowFactory.cs @@ -38,6 +38,8 @@ internal enum NumberSignal /// /// Executor that judges the guess and provides feedback. /// +[SendsMessage(typeof(NumberSignal))] +[YieldsOutput(typeof(string))] internal sealed class JudgeExecutor() : Executor("Judge") { private readonly int _targetNumber; diff --git a/dotnet/samples/03-workflows/Loop/Program.cs b/dotnet/samples/03-workflows/Loop/Program.cs index 00f20191b8..dba811d84c 100644 --- a/dotnet/samples/03-workflows/Loop/Program.cs +++ b/dotnet/samples/03-workflows/Loop/Program.cs @@ -56,6 +56,7 @@ internal enum NumberSignal /// /// Executor that makes a guess based on the current bounds. /// +[SendsMessage(typeof(int))] internal sealed class GuessNumberExecutor : Executor { /// @@ -104,6 +105,8 @@ public override async ValueTask HandleAsync(NumberSignal message, IWorkflowConte /// /// Executor that judges the guess and provides feedback. /// +[SendsMessage(typeof(NumberSignal))] +[YieldsOutput(typeof(string))] internal sealed class JudgeExecutor : Executor { private readonly int _targetNumber; @@ -124,8 +127,7 @@ public override async ValueTask HandleAsync(int message, IWorkflowContext contex this._tries++; if (message == this._targetNumber) { - await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken) - ; + await context.YieldOutputAsync($"{this._targetNumber} found in {this._tries} tries!", cancellationToken); } else if (message < this._targetNumber) { diff --git a/dotnet/samples/03-workflows/SharedStates/Program.cs b/dotnet/samples/03-workflows/SharedStates/Program.cs index 1ee842fd84..ebe3aaeb3b 100644 --- a/dotnet/samples/03-workflows/SharedStates/Program.cs +++ b/dotnet/samples/03-workflows/SharedStates/Program.cs @@ -99,6 +99,10 @@ public override async ValueTask HandleAsync(string message, IWorkflow } } +/// +/// The aggregation executor collects results from both executors and yields the final output. +/// +[YieldsOutput(typeof(string))] internal sealed class AggregationExecutor() : Executor("AggregationExecutor") { private readonly List _messages = []; diff --git a/dotnet/samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/Program.cs b/dotnet/samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/Program.cs index 7b961d1a4c..c566054146 100644 --- a/dotnet/samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/Program.cs +++ b/dotnet/samples/03-workflows/_StartHere/06_MixedWorkflowAgentsAndExecutors/Program.cs @@ -205,6 +205,8 @@ public override ValueTask HandleAsync(string message, IWorkflowContext c /// 1. Sending ChatMessage(s) /// 2. Sending a TurnToken to trigger processing /// +[SendsMessage(typeof(ChatMessage))] +[SendsMessage(typeof(TurnToken))] internal sealed class StringToChatMessageExecutor(string id) : Executor(id) { public override async ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) @@ -234,6 +236,8 @@ public override async ValueTask HandleAsync(string message, IWorkflowContext con /// The AIAgentHostExecutor sends response.Messages which has runtime type List<ChatMessage>. /// The message router uses exact type matching via message.GetType(). /// +[SendsMessage(typeof(ChatMessage))] +[SendsMessage(typeof(TurnToken))] internal sealed class JailbreakSyncExecutor() : Executor>("JailbreakSync") { public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default)