Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,17 +280,16 @@ public static AnalysisResult CombineOutputOnlyResults(IEnumerable<ClassProtocolI
if (!first.DerivesFromExecutor)
{
allDiagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.NotAnExecutor,
DiagnosticDescriptors.ProtocolClassNotAnExecutor,
classLocation,
first.ClassName,
first.ClassName));
return AnalysisResult.WithDiagnostics(allDiagnostics.ToImmutable());
}

if (!first.IsPartialClass)
{
allDiagnostics.Add(Diagnostic.Create(
DiagnosticDescriptors.ClassMustBePartial,
DiagnosticDescriptors.ProtocolClassMustBePartial,
classLocation,
first.ClassName));
return AnalysisResult.WithDiagnostics(allDiagnostics.ToImmutable());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,26 @@ private static DiagnosticDescriptor Register(DiagnosticDescriptor descriptor)
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true));

/// <summary>
/// MAFGENWF008: Executor with protocol attributes must be partial.
/// </summary>
public static readonly DiagnosticDescriptor ProtocolClassMustBePartial = Register(new(
id: "MAFGENWF008",
title: "Executor with protocol attributes must be partial",
messageFormat: "Class '{0}' uses [SendsMessage] or [YieldsOutput] but is not declared as partial",
category: Category,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true));

/// <summary>
/// MAFGENWF009: Protocol attributes on non-Executor class.
/// </summary>
public static readonly DiagnosticDescriptor ProtocolClassNotAnExecutor = Register(new(
id: "MAFGENWF009",
title: "Protocol attributes on non-Executor class",
messageFormat: "Class '{0}' uses [SendsMessage] or [YieldsOutput] but does not derive from Executor",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true));
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ namespace Microsoft.Agents.AI.Workflows;
/// This attribute can be applied multiple times to declare multiple message types.
/// It is inherited by derived classes, allowing base executors to declare common message types.
/// </para>
/// <para>
/// When this attribute is applied to an executor class and the workflows source generator is referenced,
/// the class must be declared <c>partial</c> so the generator can add its protocol configuration.
/// </para>
/// </remarks>
/// <example>
/// <code>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ namespace Microsoft.Agents.AI.Workflows;
/// This attribute can be applied multiple times to declare multiple output types.
/// It is inherited by derived classes, allowing base executors to declare common output types.
/// </para>
/// <para>
/// When this attribute is applied to an executor class and the workflows source generator is referenced,
/// the class must be declared <c>partial</c> so the generator can add its protocol configuration.
/// </para>
/// </remarks>
/// <example>
/// <code>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -927,10 +927,12 @@ public TestExecutor() : base("test") { }
.And.RegisterSentMessageType("global::TestNamespace.MessageC");
}

[Fact]
public void ProtocolOnly_NonPartialClass_ProducesDiagnostic()
[Theory]
[InlineData("SendsMessage")]
[InlineData("YieldsOutput")]
public void ProtocolOnly_NonPartialClass_ProducesProtocolDiagnostic(string attributeName)
{
var source = """
var source = $$"""
using System;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -940,7 +942,7 @@ namespace TestNamespace;

public class BroadcastMessage { }

[SendsMessage(typeof(BroadcastMessage))]
[{{attributeName}}(typeof(BroadcastMessage))]
public class TestExecutor : Executor
{
public TestExecutor() : base("test") { }
Expand All @@ -949,15 +951,57 @@ public TestExecutor() : base("test") { }

var result = GeneratorTestHelper.RunGenerator(source);

// Should produce MAFGENWF003 diagnostic (class must be partial)
result.RunResult.Diagnostics.Should().Contain(d => d.Id == "MAFGENWF003");
result.RunResult.Diagnostics.Should().ContainSingle();
var diagnostic = result.RunResult.Diagnostics.Single();
diagnostic.Id.Should().Be("MAFGENWF008");
diagnostic.GetMessage().Should().Be(
"Class 'TestExecutor' uses [SendsMessage] or [YieldsOutput] but is not declared as partial");
result.RunResult.GeneratedTrees.Should().BeEmpty();
}

[Fact]
public void ProtocolOnly_NonExecutorClass_ProducesDiagnostic()
public void ProtocolOnly_NonPartialExecutorOfT_ProducesProtocolDiagnostic()
{
var source = """
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows;

namespace TestNamespace;

[YieldsOutput(typeof(List<string>))]
internal sealed class CompletionExecutor(string id) : Executor<List<ReduceComplete>>(id)
{
public override async ValueTask HandleAsync(
List<ReduceComplete> message,
IWorkflowContext context,
CancellationToken cancellationToken = default)
{
List<string> filePaths = message.ConvertAll(result => result.FilePath);
await context.YieldOutputAsync(filePaths, cancellationToken);
}
}

internal sealed record ReduceComplete(string FilePath);
""";

var result = GeneratorTestHelper.RunGenerator(source);

result.RunResult.Diagnostics.Should().ContainSingle();
var diagnostic = result.RunResult.Diagnostics.Single();
diagnostic.Id.Should().Be("MAFGENWF008");
diagnostic.GetMessage().Should().Be(
"Class 'CompletionExecutor' uses [SendsMessage] or [YieldsOutput] but is not declared as partial");
result.RunResult.GeneratedTrees.Should().BeEmpty();
}

[Theory]
[InlineData("SendsMessage")]
[InlineData("YieldsOutput")]
public void ProtocolOnly_NonExecutorClass_ProducesProtocolDiagnostic(string attributeName)
{
var source = $$"""
using System;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -967,16 +1011,19 @@ namespace TestNamespace;

public class BroadcastMessage { }

[SendsMessage(typeof(BroadcastMessage))]
[{{attributeName}}(typeof(BroadcastMessage))]
public partial class NotAnExecutor
{
}
""";

var result = GeneratorTestHelper.RunGenerator(source);

// Should produce MAFGENWF004 diagnostic (must derive from Executor)
result.RunResult.Diagnostics.Should().Contain(d => d.Id == "MAFGENWF004");
result.RunResult.Diagnostics.Should().ContainSingle();
var diagnostic = result.RunResult.Diagnostics.Single();
diagnostic.Id.Should().Be("MAFGENWF009");
diagnostic.GetMessage().Should().Be(
"Class 'NotAnExecutor' uses [SendsMessage] or [YieldsOutput] but does not derive from Executor");
result.RunResult.GeneratedTrees.Should().BeEmpty();
}

Expand Down
Loading