diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index bdb1f72928..e804fbb389 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -198,7 +198,7 @@ public override async IAsyncEnumerable RunStreamingAsync } /// - public override string Id => this._id ?? base.Id; + protected override string? IdCore => this._id; /// public override string? Name => this._name ?? base.Name; diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs index eba6f84687..4cff385dcc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs @@ -22,9 +22,6 @@ namespace Microsoft.Agents.AI; [DebuggerDisplay("{DisplayName,nq}")] public abstract class AIAgent { - /// Default ID of this agent instance. - private readonly string _id = Guid.NewGuid().ToString("N"); - /// /// Gets the unique identifier for this agent instance. /// @@ -37,7 +34,19 @@ public abstract class AIAgent /// agent instances in multi-agent scenarios. They should remain stable for the lifetime /// of the agent instance. /// - public virtual string Id => this._id; + public string Id { get => this.IdCore ?? field; } = Guid.NewGuid().ToString("N"); + + /// + /// Gets a custom identifier for the agent, which can be overridden by derived classes. + /// + /// + /// A string representing the agent's identifier, or if the default ID should be used. + /// + /// + /// Derived classes can override this property to provide a custom identifier. + /// When is returned, the property will use the default randomly-generated identifier. + /// + protected virtual string? IdCore => null; /// /// Gets the human-readable name of the agent. @@ -61,7 +70,7 @@ public abstract class AIAgent /// This property provides a guaranteed non-null string suitable for display in user interfaces, /// logs, or other contexts where a readable identifier is needed. /// - public virtual string DisplayName => this.Name ?? this.Id ?? this._id; // final fallback to _id in case Id override returns null + public virtual string DisplayName => this.Name ?? this.Id; /// /// Gets a description of the agent's purpose, capabilities, or behavior. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs index 353c82c996..72f1980b1c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/DelegatingAIAgent.cs @@ -54,7 +54,7 @@ protected DelegatingAIAgent(AIAgent innerAgent) protected AIAgent InnerAgent { get; } /// - public override string Id => this.InnerAgent.Id; + protected override string? IdCore => this.InnerAgent.Id; /// public override string? Name => this.InnerAgent.Name; diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs index 34c9208967..8822ebcc39 100644 --- a/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs +++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/EntityAgentWrapper.cs @@ -19,7 +19,7 @@ internal sealed class EntityAgentWrapper( private readonly IServiceProvider? _entityScopedServices = entityScopedServices; // The ID of the agent is always the entity ID. - public override string Id => this._entityContext.Id.ToString(); + protected override string? IdCore => this._entityContext.Id.ToString(); public override async Task RunAsync( IEnumerable messages, diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs index 98dc5903bf..70fcee15df 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs @@ -39,7 +39,7 @@ public WorkflowHostAgent(Workflow workflow, string? id = null, string? name = nu this._describeTask = this._workflow.DescribeProtocolAsync().AsTask(); } - public override string Id => this._id ?? base.Id; + protected override string? IdCore => this._id; public override string? Name { get; } public override string? Description { get; } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index df7477241c..a5a34d24a9 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -121,7 +121,7 @@ public ChatClientAgent(IChatClient chatClient, ChatClientAgentOptions? options, public IChatClient ChatClient { get; } /// - public override string Id => this._agentOptions?.Id ?? base.Id; + protected override string? IdCore => this._agentOptions?.Id; /// public override string? Name => this._agentOptions?.Name; diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs index 5111a97ad1..e3bda2081a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs @@ -214,13 +214,31 @@ public async Task InvokeStreamingWithSingleMessageCallsMockedInvokeWithMessageIn [Fact] public void ValidateAgentIDIsIdempotent() { + // Arrange var agent = new MockAgent(); + // Act string id = agent.Id; + + // Assert Assert.NotNull(id); Assert.Equal(id, agent.Id); } + [Fact] + public void ValidateAgentIDCanBeProvidedByDerivedAgentClass() + { + // Arrange + var agent = new MockAgent(id: "test-agent-id"); + + // Act + string id = agent.Id; + + // Assert + Assert.NotNull(id); + Assert.Equal("test-agent-id", id); + } + #region GetService Method Tests /// @@ -344,6 +362,13 @@ public abstract class TestAgentThread : AgentThread; private sealed class MockAgent : AIAgent { + public MockAgent(string? id = null) + { + this.IdCore = id; + } + + protected override string? IdCore { get; } + public override AgentThread GetNewThread() => throw new NotImplementedException(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs index 4dca99a77c..50271b7eee 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/DelegatingAIAgentTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; +using Moq.Protected; namespace Microsoft.Agents.AI.Abstractions.UnitTests; @@ -31,7 +32,7 @@ public DelegatingAIAgentTests() this._testThread = new TestAgentThread(); // Setup inner agent mock - this._innerAgentMock.Setup(x => x.Id).Returns("test-agent-id"); + this._innerAgentMock.Protected().SetupGet("IdCore").Returns("test-agent-id"); this._innerAgentMock.Setup(x => x.Name).Returns("Test Agent"); this._innerAgentMock.Setup(x => x.Description).Returns("Test Description"); this._innerAgentMock.Setup(x => x.GetNewThread()).Returns(this._testThread); @@ -93,7 +94,7 @@ public void Id_DelegatesToInnerAgent() // Assert Assert.Equal("test-agent-id", id); - this._innerAgentMock.Verify(x => x.Id, Times.Once); + this._innerAgentMock.Protected().VerifyGet("IdCore", Times.Once()); } /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs index 5bc4e8afad..69560421cf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.IntegrationTests/BasicStreamingTests.cs @@ -276,15 +276,9 @@ public async ValueTask DisposeAsync() [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection")] internal sealed class FakeChatClientAgent : AIAgent { - public FakeChatClientAgent() - { - this.Id = "fake-agent"; - this.Description = "A fake agent for testing"; - } - - public override string Id { get; } + protected override string? IdCore => "fake-agent"; - public override string? Description { get; } + public override string? Description => "A fake agent for testing"; public override AgentThread GetNewThread() { @@ -350,15 +344,9 @@ public FakeInMemoryAgentThread(JsonElement serializedThread, JsonSerializerOptio [SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection")] internal sealed class FakeMultiMessageAgent : AIAgent { - public FakeMultiMessageAgent() - { - this.Id = "fake-multi-message-agent"; - this.Description = "A fake agent that sends multiple messages for testing"; - } - - public override string Id { get; } + protected override string? IdCore => "fake-multi-message-agent"; - public override string? Description { get; } + public override string? Description => "A fake agent that sends multiple messages for testing"; public override AgentThread GetNewThread() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs index 78a3048747..3e80a58369 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/AGUIEndpointRouteBuilderExtensionsTests.cs @@ -421,7 +421,7 @@ private static List ParseSseEvents(string responseContent) private sealed class MultiResponseAgent : AIAgent { - public override string Id => "multi-response-agent"; + protected override string? IdCore => "multi-response-agent"; public override string? Description => "Agent that produces multiple text chunks"; @@ -510,7 +510,7 @@ public TestInMemoryAgentThread(JsonElement serializedThreadState, JsonSerializer private sealed class TestAgent : AIAgent { - public override string Id => "test-agent"; + protected override string? IdCore => "test-agent"; public override string? Description => "Test agent"; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs index a0e57006ed..16a51876d0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/Sample/06_GroupChat_Workflow.cs @@ -57,7 +57,7 @@ internal sealed class HelloAgent(string id = nameof(HelloAgent)) : AIAgent public const string Greeting = "Hello World!"; public const string DefaultId = nameof(HelloAgent); - public override string Id => id; + protected override string? IdCore => id; public override string? Name => id; public override AgentThread GetNewThread() diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SpecializedExecutorSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SpecializedExecutorSmokeTests.cs index b93d7862d5..daff2c248e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SpecializedExecutorSmokeTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/SpecializedExecutorSmokeTests.cs @@ -19,7 +19,7 @@ public class SpecializedExecutorSmokeTests { public class TestAIAgent(List? messages = null, string? id = null, string? name = null) : AIAgent { - public override string Id => id ?? base.Id; + protected override string? IdCore => id; public override string? Name => name; public static List ToChatMessages(params string[] messages) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs index 4966585211..9ddc94cf71 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/TestEchoAgent.cs @@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Workflows.UnitTests; internal class TestEchoAgent(string? id = null, string? name = null, string? prefix = null) : AIAgent { - public override string Id => id ?? base.Id; + protected override string? IdCore => id; public override string? Name => name ?? base.Name; public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)