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 @@ internal sealed class A2AAgent : AIAgent
}
///
- 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 @@ public class DelegatingAIAgent : AIAgent
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 @@ internal sealed class WorkflowHostAgent : AIAgent
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 sealed partial class ChatClientAgent : AIAgent
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 class AIAgentTests
[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 class AIAgentTests
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;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
+using Moq.Protected;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -31,7 +32,7 @@ public class 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 class DelegatingAIAgentTests
// 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 sealed class BasicStreamingTests : IAsyncDisposable
[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";
- }
+ protected override string? IdCore => "fake-agent";
- public override string Id { get; }
-
- public override string? Description { get; }
+ public override string? Description => "A fake agent for testing";
public override AgentThread GetNewThread()
{
@@ -350,15 +344,9 @@ internal sealed class FakeChatClientAgent : AIAgent
[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";
- }
+ protected override string? IdCore => "fake-multi-message-agent";
- public override string Id { get; }
-
- 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 @@ public sealed class AGUIEndpointRouteBuilderExtensionsTests
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 sealed class AGUIEndpointRouteBuilderExtensionsTests
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)